Merge tag '1.2.0' into debian/liberty
python-cinderclient 1.2.0 release
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = cinderclient
|
||||
omit = cinderclient/openstack/*
|
||||
omit = cinderclient/openstack/*,cinderclient/tests/*
|
||||
|
||||
[report]
|
||||
ignore-errors = True
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
12
CONTRIBUTING.md
Normal 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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
@@ -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'])
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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):
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
195
cinderclient/tests/unit/test_client.py
Normal file
195
cinderclient/tests/unit/test_client.py
Normal 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)
|
||||
@@ -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, [])
|
||||
@@ -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
|
||||
368
cinderclient/tests/unit/test_shell.py
Normal file
368
cinderclient/tests/unit/test_shell.py
Normal 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'])
|
||||
@@ -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):
|
||||
@@ -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 = [
|
||||
@@ -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
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import mock
|
||||
|
||||
from cinderclient.tests import utils
|
||||
from cinderclient.tests.unit import utils
|
||||
from cinderclient.v1 import limits
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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):
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
0
cinderclient/tests/unit/v2/contrib/__init__.py
Normal file
0
cinderclient/tests/unit/v2/contrib/__init__.py
Normal 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 = [
|
||||
@@ -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})
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import mock
|
||||
|
||||
from cinderclient.tests import utils
|
||||
from cinderclient.tests.unit import utils
|
||||
from cinderclient.v2 import limits
|
||||
|
||||
|
||||
44
cinderclient/tests/unit/v2/test_pools.py
Normal file
44
cinderclient/tests/unit/v2/test_pools.py
Normal 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"))
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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):
|
||||
42
cinderclient/tests/unit/v2/test_type_access.py
Normal file
42
cinderclient/tests/unit/v2/test_type_access.py
Normal 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}})
|
||||
88
cinderclient/tests/unit/v2/test_types.py
Normal file
88
cinderclient/tests/unit/v2/test_types.py
Normal 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')
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
62
cinderclient/v2/pools.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
51
cinderclient/v2/volume_type_access.py
Normal file
51
cinderclient/v2/volume_type_access.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user