Merge tag '1.1.1' into debian/experimental

Tag release version 1.1.1 of python-cinderclient
This commit is contained in:
Thomas Goirand
2014-09-30 14:49:39 +08:00
93 changed files with 4478 additions and 1423 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ cover
*.pyc
AUTHORS
ChangeLog
doc/build
build
dist
cinderclient/versioninfo

View File

@@ -13,3 +13,4 @@ Andy Smith <github@anarkystic.com> termie <github@anarkystic.com>
<matt.dietz@rackspace.com> <matthew.dietz@gmail.com>
Nikolay Sokolov <nsokolov@griddynamics.com> Nokolay Sokolov <nsokolov@griddynamics.com>
Nikolay Sokolov <nsokolov@griddynamics.com> Nokolay Sokolov <chemikadze@gmail.com>
<skanddh@gmail.com> <seungkyu.ahn@samsung.com>

View File

@@ -1,5 +1,3 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may

143
cinderclient/auth_plugin.py Normal file
View File

@@ -0,0 +1,143 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import pkg_resources
import six
from cinderclient import exceptions
from cinderclient import utils
logger = logging.getLogger(__name__)
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
ep_name = 'openstack.client.auth_plugin'
for ep in pkg_resources.iter_entry_points(ep_name):
try:
auth_plugin = ep.load()
except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e:
logger.debug("ERROR: Cannot load auth plugin %s" % ep.name)
logger.debug(e, exc_info=1)
else:
_discovered_plugins[ep.name] = auth_plugin
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
for name, auth_plugin in six.iteritems(_discovered_plugins):
add_opts_fn = getattr(auth_plugin, "add_opts", None)
if add_opts_fn:
group = parser.add_argument_group("Auth-system '%s' options" %
name)
add_opts_fn(group)
def load_plugin(auth_system):
if auth_system in _discovered_plugins:
return _discovered_plugins[auth_system]()
# NOTE(aloga): If we arrive here, the plugin will be an old-style one,
# so we have to create a fake AuthPlugin for it.
return DeprecatedAuthPlugin(auth_system)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
def __init__(self):
self.opts = {}
def get_auth_url(self):
"""Return the auth url for the plugin (if any)."""
return None
@staticmethod
def add_opts(parser):
"""Populate and return the parser with the options for this plugin.
If the plugin does not need any options, it should return the same
parser untouched.
"""
return parser
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute self.opts with a
dict containing the options and values needed to make authentication.
If the dict is empty, the client should assume that it needs the same
options as the 'keystone' auth system (i.e. os_username and
os_password).
Returns the self.opts dict.
"""
return self.opts
def authenticate(self, cls, auth_url):
"""Authenticate using plugin defined method."""
raise exceptions.AuthSystemNotFound(self.auth_system)
class DeprecatedAuthPlugin(object):
"""Class to mimic the AuthPlugin class for deprecated auth systems.
Old auth systems only define two entry points: openstack.client.auth_url
and openstack.client.authenticate. This class will load those entry points
into a class similar to a valid AuthPlugin.
"""
def __init__(self, auth_system):
self.auth_system = auth_system
def authenticate(cls, auth_url):
raise exceptions.AuthSystemNotFound(self.auth_system)
self.opts = {}
self.get_auth_url = lambda: None
self.authenticate = authenticate
self._load_endpoints()
def _load_endpoints(self):
ep_name = 'openstack.client.auth_url'
fn = utils._load_entry_point(ep_name, name=self.auth_system)
if fn:
self.get_auth_url = fn
ep_name = 'openstack.client.authenticate'
fn = utils._load_entry_point(ep_name, name=self.auth_system)
if fn:
self.authenticate = fn
def parse_opts(self, args):
return self.opts

View File

@@ -26,9 +26,13 @@ import os
import six
from cinderclient import exceptions
from cinderclient.openstack.common.apiclient import base as common_base
from cinderclient import utils
Resource = common_base.Resource
# Python 2.4 compat
try:
all
@@ -215,88 +219,3 @@ class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)):
continue
return found
class Resource(object):
"""
A resource represents a particular instance of an object (server, flavor,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
HUMAN_ID = False
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
# enter an infinite loop of __getattr__ -> get -> __init__ ->
# __getattr__ -> ...
if 'id' in self.__dict__ and len(str(self.id)) == 36:
self.manager.write_to_completion_cache('uuid', self.id)
human_id = self.human_id
if human_id:
self.manager.write_to_completion_cache('human_id', human_id)
@property
def human_id(self):
"""Subclasses may override this provide a pretty ID which can be used
for bash completion.
"""
if 'name' in self.__dict__ and self.HUMAN_ID:
return utils.slugify(self.name)
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__ if k[0] != '_'
and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val

View File

@@ -14,7 +14,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
@@ -23,6 +22,16 @@ from __future__ import print_function
import logging
from keystoneclient import access
from keystoneclient import adapter
from keystoneclient.auth.identity import base
import requests
from cinderclient import exceptions
from cinderclient.openstack.common import strutils
from cinderclient import utils
try:
import urlparse
except ImportError:
@@ -43,27 +52,113 @@ if not hasattr(urlparse, 'parse_qsl'):
import cgi
urlparse.parse_qsl = cgi.parse_qsl
import requests
_VALID_VERSIONS = ['v1', 'v2']
from cinderclient import exceptions
from cinderclient import service_catalog
from cinderclient import utils
def get_volume_api_from_url(url):
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
components = path.split("/")
for version in _VALID_VERSIONS:
if version in components:
return version[1:]
msg = "Invalid client version '%s'. must be one of: %s" % (
(version, ', '.join(valid_versions)))
raise exceptions.UnsupportedVersion(msg)
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):
kwargs.setdefault('authenticated', False)
return super(SessionClient, self).request(*args, **kwargs)
def _cs_request(self, url, method, **kwargs):
# this function is mostly redundant but makes compatibility easier
kwargs.setdefault('authenticated', True)
return self.request(url, method, **kwargs)
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self._cs_request(url, 'PUT', **kwargs)
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())
def authenticate(self, auth=None):
self._invalidate(auth)
return self._get_token(auth)
@property
def service_catalog(self):
# NOTE(jamielennox): This is ugly and should be deprecated.
auth = self.auth or self.session.auth
if isinstance(auth, base.BaseIdentityPlugin):
return auth.get_access(self.session).service_catalog
raise AttributeError('There is no service catalog for this type of '
'auth plugin.')
class HTTPClient(object):
USER_AGENT = 'python-cinderclient'
def __init__(self, user, password, projectid, auth_url, insecure=False,
timeout=None, tenant_id=None, proxy_tenant_id=None,
proxy_token=None, region_name=None,
def __init__(self, user, password, projectid, auth_url=None,
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,
http_log_debug=False, cacert=None):
http_log_debug=False, cacert=None,
auth_system='keystone', auth_plugin=None):
self.user = user
self.password = password
self.projectid = projectid
self.tenant_id = tenant_id
if auth_system and auth_system != 'keystone' and not auth_plugin:
raise exceptions.AuthSystemNotFound(auth_system)
if not auth_url and auth_system and auth_system != 'keystone':
auth_url = auth_plugin.get_auth_url()
if not auth_url:
raise exceptions.EndpointNotFound()
self.auth_url = auth_url.rstrip('/')
self.version = 'v1'
self.region_name = region_name
@@ -88,13 +183,10 @@ class HTTPClient(object):
else:
self.verify_cert = True
self.auth_system = auth_system
self.auth_plugin = auth_plugin
self._logger = logging.getLogger(__name__)
if self.http_log_debug and not self._logger.handlers:
ch = logging.StreamHandler()
self._logger.setLevel(logging.DEBUG)
self._logger.addHandler(ch)
if hasattr(requests, 'logging'):
requests.logging.getLogger(requests.__name__).addHandler(ch)
def http_log_req(self, args, kwargs):
if not self.http_log_debug:
@@ -112,7 +204,11 @@ class HTTPClient(object):
string_parts.append(header)
if 'data' in kwargs:
string_parts.append(" -d '%s'" % (kwargs['data']))
if "password" in kwargs['data']:
data = strutils.mask_password(kwargs['data'])
else:
data = kwargs['data']
string_parts.append(" -d '%s'" % (data))
self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
def http_log_resp(self, resp):
@@ -192,10 +288,10 @@ class HTTPClient(object):
else:
raise
except requests.exceptions.ConnectionError as e:
# Catch a connection refused from requests.request
self._logger.debug("Connection refused: %s" % e)
msg = 'Unable to establish connection: %s' % e
raise exceptions.ConnectionError(msg)
self._logger.debug("Connection error: %s" % e)
if attempts > self.retries:
msg = 'Unable to establish connection: %s' % e
raise exceptions.ConnectionError(msg)
self._logger.debug(
"Failed attempt(%s of %s), retrying in %s seconds" %
(attempts, self.retries, backoff))
@@ -214,6 +310,9 @@ class HTTPClient(object):
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
def get_volume_api_version_from_endpoint(self):
return get_volume_api_from_url(self.management_url)
def _extract_service_catalog(self, url, resp, body, extract_token=True):
"""See what the auth service told us and process the response.
We may get redirected to another site, fail or actually get
@@ -223,19 +322,16 @@ class HTTPClient(object):
if resp.status_code == 200: # content must always present
try:
self.auth_url = url
self.service_catalog = \
service_catalog.ServiceCatalog(body)
self.auth_ref = access.AccessInfo.factory(resp, body)
self.service_catalog = self.auth_ref.service_catalog
if extract_token:
self.auth_token = self.service_catalog.get_token()
self.auth_token = self.auth_ref.auth_token
management_url = self.service_catalog.url_for(
attr='region',
filter_value=self.region_name,
region_name=self.region_name,
endpoint_type=self.endpoint_type,
service_type=self.service_type,
service_name=self.service_name,
volume_service_name=self.volume_service_name)
service_type=self.service_type)
self.management_url = management_url.rstrip('/')
return None
except exceptions.AmbiguousEndpoints:
@@ -295,7 +391,10 @@ class HTTPClient(object):
auth_url = self.auth_url
if self.version == "v2.0":
while auth_url:
auth_url = self._v2_auth(auth_url)
if not self.auth_system or self.auth_system == 'keystone':
auth_url = self._v2_auth(auth_url)
else:
auth_url = self._plugin_auth(auth_url)
# Are we acting on behalf of another user via an
# existing token? If so, our actual endpoints may
@@ -341,6 +440,9 @@ class HTTPClient(object):
else:
raise exceptions.from_response(resp, body)
def _plugin_auth(self, auth_url):
return self.auth_plugin.authenticate(self, auth_url)
def _v2_auth(self, url):
"""Authenticate against a v2.0 auth service."""
body = {"auth": {
@@ -367,17 +469,52 @@ class HTTPClient(object):
return self._extract_service_catalog(url, resp, body)
def get_volume_api_version_from_endpoint(self):
magic_tuple = urlparse.urlsplit(self.management_url)
scheme, netloc, path, query, frag = magic_tuple
components = path.split("/")
valid_versions = ['v1', 'v2']
for version in valid_versions:
if version in components:
return version[1:]
msg = "Invalid client version '%s'. must be one of: %s" % (
(version, ', '.join(valid_versions)))
raise exceptions.UnsupportedVersion(msg)
def _construct_http_client(username=None, password=None, project_id=None,
auth_url=None, insecure=False, timeout=None,
proxy_tenant_id=None, proxy_token=None,
region_name=None, endpoint_type='publicURL',
service_type='volume',
service_name=None, volume_service_name=None,
retries=None,
http_log_debug=False,
auth_system='keystone', auth_plugin=None,
cacert=None, tenant_id=None,
session=None,
auth=None,
**kwargs):
if session:
kwargs.setdefault('interface', endpoint_type)
return SessionClient(session=session,
auth=auth,
service_type=service_type,
service_name=service_name,
region_name=region_name,
**kwargs)
else:
# FIXME(jamielennox): username and password are now optional. Need
# to test that they were provided in this mode.
return HTTPClient(username,
password,
projectid=project_id,
auth_url=auth_url,
insecure=insecure,
timeout=timeout,
tenant_id=tenant_id,
proxy_token=proxy_token,
proxy_tenant_id=proxy_tenant_id,
region_name=region_name,
endpoint_type=endpoint_type,
service_type=service_type,
service_name=service_name,
volume_service_name=volume_service_name,
retries=retries,
http_log_debug=http_log_debug,
cacert=cacert,
auth_system=auth_system,
auth_plugin=auth_plugin,
)
def get_client_class(version):

View File

@@ -41,6 +41,15 @@ class NoUniqueMatch(Exception):
pass
class AuthSystemNotFound(Exception):
"""When the user specify a AuthSystem but not installed."""
def __init__(self, auth_system):
self.auth_system = auth_system
def __str__(self):
return "AuthSystemNotFound: %s" % repr(self.auth_system)
class NoTokenLookupException(Exception):
"""This form of authentication does not support looking up
endpoints from an existing token.
@@ -172,4 +181,5 @@ def from_response(response, body):
return cls(code=response.status_code, message=message, details=details,
request_id=request_id)
else:
return cls(code=response.status_code, request_id=request_id)
return cls(code=response.status_code, request_id=request_id,
message=response.reason)

View File

@@ -0,0 +1,17 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))

View File

@@ -26,9 +26,9 @@ Base utilities to build API operation managers and objects on top of.
import abc
import six
from six.moves.urllib import parse
from cinderclient.openstack.common.apiclient import exceptions
from cinderclient.openstack.common.py3kcompat import urlutils
from cinderclient.openstack.common import strutils
@@ -327,7 +327,7 @@ class CrudManager(BaseManager):
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '',
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
@@ -366,7 +366,7 @@ class CrudManager(BaseManager):
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '',
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)

View File

@@ -28,9 +28,9 @@ import json
import requests
import six
from six.moves.urllib import parse
from cinderclient.openstack.common.apiclient import client
from cinderclient.openstack.common.py3kcompat import urlutils
def assert_has_keys(dct, required=[], optional=[]):
@@ -147,7 +147,7 @@ class FakeHTTPClient(client.HTTPClient):
"text": fixture[1]})
# Call the method
args = urlutils.parse_qsl(urlutils.urlparse(url)[4])
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')

View File

@@ -19,7 +19,7 @@ gettext for openstack-common modules.
Usual usage in an openstack.common module:
from cinderclient.openstack.common.gettextutils import _
from openstack.common.gettextutils import _
"""
import copy
@@ -27,18 +27,119 @@ import gettext
import locale
from logging import handlers
import os
import re
from babel import localedata
import six
_localedir = os.environ.get('cinderclient'.upper() + '_LOCALEDIR')
_t = gettext.translation('cinderclient', localedir=_localedir, fallback=True)
_AVAILABLE_LANGUAGES = {}
# FIXME(dhellmann): Remove this when moving to oslo.i18n.
USE_LAZY = False
class TranslatorFactory(object):
"""Create translator functions
"""
def __init__(self, domain, localedir=None):
"""Establish a set of translation functions for the domain.
:param domain: Name of translation domain,
specifying a message catalog.
:type domain: str
:param lazy: Delays translation until a message is emitted.
Defaults to False.
:type lazy: Boolean
:param localedir: Directory with translation catalogs.
:type localedir: str
"""
self.domain = domain
if localedir is None:
localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
self.localedir = localedir
def _make_translation_func(self, domain=None):
"""Return a new translation function ready for use.
Takes into account whether or not lazy translation is being
done.
The domain can be specified to override the default from the
factory, but the localedir from the factory is always used
because we assume the log-level translation catalogs are
installed in the same directory as the main application
catalog.
"""
if domain is None:
domain = self.domain
t = gettext.translation(domain,
localedir=self.localedir,
fallback=True)
# Use the appropriate method of the translation object based
# on the python version.
m = t.gettext if six.PY3 else t.ugettext
def f(msg):
"""oslo.i18n.gettextutils translation function."""
if USE_LAZY:
return Message(msg, domain=domain)
return m(msg)
return f
@property
def primary(self):
"The default translation function."
return self._make_translation_func()
def _make_log_translation_func(self, level):
return self._make_translation_func(self.domain + '-log-' + level)
@property
def log_info(self):
"Translate info-level log messages."
return self._make_log_translation_func('info')
@property
def log_warning(self):
"Translate warning-level log messages."
return self._make_log_translation_func('warning')
@property
def log_error(self):
"Translate error-level log messages."
return self._make_log_translation_func('error')
@property
def log_critical(self):
"Translate critical-level log messages."
return self._make_log_translation_func('critical')
# NOTE(dhellmann): When this module moves out of the incubator into
# oslo.i18n, these global variables can be moved to an integration
# module within each application.
# Create the global translation functions.
_translators = TranslatorFactory('cinderclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
# NOTE(dhellmann): End of globals that will move to the application's
# integration module.
def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
@@ -51,16 +152,7 @@ def enable_lazy():
USE_LAZY = True
def _(msg):
if USE_LAZY:
return Message(msg, domain='cinderclient')
else:
if six.PY3:
return _t.gettext(msg)
return _t.ugettext(msg)
def install(domain, lazy=False):
def install(domain):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
@@ -71,43 +163,14 @@ def install(domain, lazy=False):
a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR).
Note that to enable lazy translation, enable_lazy must be
called.
:param domain: the translation domain
:param lazy: indicates whether or not to install the lazy _() function.
The lazy _() introduces a way to do deferred translation
of messages by installing a _ that builds Message objects,
instead of strings, which can then be lazily translated into
any available locale.
"""
if lazy:
# NOTE(mrodden): Lazy gettext functionality.
#
# The following introduces a deferred way to do translations on
# messages in OpenStack. We override the standard _() function
# and % (format string) operation to build Message objects that can
# later be translated when we have more information.
def _lazy_gettext(msg):
"""Create and return a Message object.
Lazy gettext function for a given domain, it is a factory method
for a project/module to get a lazy gettext function for its own
translation domain (i.e. nova, glance, cinder, etc.)
Message encapsulates a string so that we can translate
it later when needed.
"""
return Message(msg, domain=domain)
from six import moves
moves.builtins.__dict__['_'] = _lazy_gettext
else:
localedir = '%s_LOCALEDIR' % domain.upper()
if six.PY3:
gettext.install(domain,
localedir=os.environ.get(localedir))
else:
gettext.install(domain,
localedir=os.environ.get(localedir),
unicode=True)
from six import moves
tf = TranslatorFactory(domain)
moves.builtins.__dict__['_'] = tf.primary
class Message(six.text_type):
@@ -214,47 +277,22 @@ class Message(six.text_type):
if other is None:
params = (other,)
elif isinstance(other, dict):
params = self._trim_dictionary_parameters(other)
# Merge the dictionaries
# Copy each item in case one does not support deep copy.
params = {}
if isinstance(self.params, dict):
for key, val in self.params.items():
params[key] = self._copy_param(val)
for key, val in other.items():
params[key] = self._copy_param(val)
else:
params = self._copy_param(other)
return params
def _trim_dictionary_parameters(self, dict_param):
"""Return a dict that only has matching entries in the msgid."""
# NOTE(luisg): Here we trim down the dictionary passed as parameters
# to avoid carrying a lot of unnecessary weight around in the message
# object, for example if someone passes in Message() % locals() but
# only some params are used, and additionally we prevent errors for
# non-deepcopyable objects by unicoding() them.
# Look for %(param) keys in msgid;
# Skip %% and deal with the case where % is first character on the line
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
# If we don't find any %(param) keys but have a %s
if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
# Apparently the full dictionary is the parameter
params = self._copy_param(dict_param)
else:
params = {}
# Save our existing parameters as defaults to protect
# ourselves from losing values if we are called through an
# (erroneous) chain that builds a valid Message with
# arguments, and then does something like "msg % kwds"
# where kwds is an empty dictionary.
src = {}
if isinstance(self.params, dict):
src.update(self.params)
src.update(dict_param)
for key in keys:
params[key] = self._copy_param(src[key])
return params
def _copy_param(self, param):
try:
return copy.deepcopy(param)
except TypeError:
except Exception:
# Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied
return six.text_type(param)
@@ -266,13 +304,14 @@ class Message(six.text_type):
def __radd__(self, other):
return self.__add__(other)
def __str__(self):
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
# and it expects specifically a UnicodeError in order to proceed.
msg = _('Message objects do not support str() because they may '
'contain non-ascii characters. '
'Please use unicode() or translate() instead.')
raise UnicodeError(msg)
if six.PY2:
def __str__(self):
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
# and it expects specifically a UnicodeError in order to proceed.
msg = _('Message objects do not support str() because they may '
'contain non-ascii characters. '
'Please use unicode() or translate() instead.')
raise UnicodeError(msg)
def get_available_languages(domain):
@@ -315,8 +354,8 @@ def get_available_languages(domain):
'zh_Hant_HK': 'zh_HK',
'zh_Hant': 'zh_TW',
'fil': 'tl_PH'}
for (locale, alias) in six.iteritems(aliases):
if locale in language_list and alias not in language_list:
for (locale_, alias) in six.iteritems(aliases):
if locale_ in language_list and alias not in language_list:
language_list.append(alias)
_AVAILABLE_LANGUAGES[domain] = language_list

View File

@@ -1,67 +0,0 @@
#
# Copyright 2013 Canonical Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Python2/Python3 compatibility layer for OpenStack
"""
import six
if six.PY3:
# python3
import urllib.error
import urllib.parse
import urllib.request
urlencode = urllib.parse.urlencode
urljoin = urllib.parse.urljoin
quote = urllib.parse.quote
quote_plus = urllib.parse.quote_plus
parse_qsl = urllib.parse.parse_qsl
unquote = urllib.parse.unquote
unquote_plus = urllib.parse.unquote_plus
urlparse = urllib.parse.urlparse
urlsplit = urllib.parse.urlsplit
urlunsplit = urllib.parse.urlunsplit
SplitResult = urllib.parse.SplitResult
urlopen = urllib.request.urlopen
URLError = urllib.error.URLError
pathname2url = urllib.request.pathname2url
else:
# python2
import urllib
import urllib2
import urlparse
urlencode = urllib.urlencode
quote = urllib.quote
quote_plus = urllib.quote_plus
unquote = urllib.unquote
unquote_plus = urllib.unquote_plus
parse = urlparse
parse_qsl = parse.parse_qsl
urljoin = parse.urljoin
urlparse = parse.urlparse
urlsplit = parse.urlsplit
urlunsplit = parse.urlunsplit
SplitResult = parse.SplitResult
urlopen = urllib2.urlopen
URLError = urllib2.URLError
pathname2url = urllib.pathname2url

View File

@@ -17,6 +17,7 @@
System-level utilities and helper functions.
"""
import math
import re
import sys
import unicodedata
@@ -26,16 +27,21 @@ import six
from cinderclient.openstack.common.gettextutils import _
# Used for looking up extensions of text
# to their 'multiplied' byte amount
BYTE_MULTIPLIERS = {
'': 1,
't': 1024 ** 4,
'g': 1024 ** 3,
'm': 1024 ** 2,
'k': 1024,
UNIT_PREFIX_EXPONENT = {
'k': 1,
'K': 1,
'Ki': 1,
'M': 2,
'Mi': 2,
'G': 3,
'Gi': 3,
'T': 4,
'Ti': 4,
}
UNIT_SYSTEM_INFO = {
'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')),
'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')),
}
BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
@@ -44,6 +50,28 @@ SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
# NOTE(flaper87): The following 3 globals are used by `mask_password`
_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
# NOTE(ldbragst): Let's build a list of regex objects using the list of
# _SANITIZE_KEYS we already have. This way, we only have to add the new key
# to the list of _SANITIZE_KEYS and we can generate regular expressions
# for XML and JSON automatically.
_SANITIZE_PATTERNS = []
_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
r'(<%(key)s>).*?(</%(key)s>)',
r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])',
r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?[\'"])'
'.*?([\'"])',
r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)']
for key in _SANITIZE_KEYS:
for pattern in _FORMAT_PATTERNS:
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
_SANITIZE_PATTERNS.append(reg_ex)
def int_from_bool_as_string(subject):
"""Interpret a string as a boolean and return either 1 or 0.
@@ -72,7 +100,7 @@ def bool_from_string(subject, strict=False, default=False):
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
"""
if not isinstance(subject, six.string_types):
subject = str(subject)
subject = six.text_type(subject)
lowered = subject.strip().lower()
@@ -92,7 +120,8 @@ def bool_from_string(subject, strict=False, default=False):
def safe_decode(text, incoming=None, errors='strict'):
"""Decodes incoming str using `incoming` if they're not already unicode.
"""Decodes incoming text/bytes string using `incoming` if they're not
already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
@@ -101,7 +130,7 @@ def safe_decode(text, incoming=None, errors='strict'):
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, six.string_types):
if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, six.text_type):
@@ -131,7 +160,7 @@ def safe_decode(text, incoming=None, errors='strict'):
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""Encodes incoming str/unicode using `encoding`.
"""Encodes incoming text/bytes string using `encoding`.
If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`)
@@ -144,7 +173,7 @@ def safe_encode(text, incoming=None,
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, six.string_types):
if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
@@ -152,49 +181,59 @@ def safe_encode(text, incoming=None,
sys.getdefaultencoding())
if isinstance(text, six.text_type):
if six.PY3:
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
if six.PY3:
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
return text
return text.encode(encoding, errors)
else:
return text
def to_bytes(text, default=0):
"""Converts a string into an integer of bytes.
def string_to_bytes(text, unit_system='IEC', return_int=False):
"""Converts a string into an float representation of bytes.
Looks at the last characters of the text to determine
what conversion is needed to turn the input text into a byte number.
Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive)
The units supported for IEC ::
Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
KB, KiB, MB, MiB, GB, GiB, TB, TiB
The units supported for SI ::
kb(it), Mb(it), Gb(it), Tb(it)
kB, MB, GB, TB
Note that the SI unit system does not support capital letter 'K'
:param text: String input for bytes size conversion.
:param default: Default return value when text is blank.
:param unit_system: Unit system for byte size conversion.
:param return_int: If True, returns integer representation of text
in bytes. (default: decimal)
:returns: Numerical representation of text in bytes.
:raises ValueError: If text has an invalid value.
"""
match = BYTE_REGEX.search(text)
try:
base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
except KeyError:
msg = _('Invalid unit system: "%s"') % unit_system
raise ValueError(msg)
match = reg_ex.match(text)
if match:
magnitude = int(match.group(1))
mult_key_org = match.group(2)
if not mult_key_org:
return magnitude
elif text:
msg = _('Invalid string format: %s') % text
raise TypeError(msg)
magnitude = float(match.group(1))
unit_prefix = match.group(2)
if match.group(3) in ['b', 'bit']:
magnitude /= 8
else:
return default
mult_key = mult_key_org.lower().replace('b', '', 1)
multiplier = BYTE_MULTIPLIERS.get(mult_key)
if multiplier is None:
msg = _('Unknown byte multiplier: %s') % mult_key_org
raise TypeError(msg)
return magnitude * multiplier
msg = _('Invalid string format: %s') % text
raise ValueError(msg)
if not unit_prefix:
res = magnitude
else:
res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
if return_int:
return int(math.ceil(res))
return res
def to_slug(value, incoming=None, errors="strict"):
@@ -220,3 +259,37 @@ def to_slug(value, incoming=None, errors="strict"):
"ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value)
def mask_password(message, secret="***"):
"""Replace password with 'secret' in message.
:param message: The string which includes security information.
:param secret: value with which to replace passwords.
:returns: The unicode value of message with the password fields masked.
For example:
>>> mask_password("'adminPass' : 'aaaaa'")
"'adminPass' : '***'"
>>> mask_password("'admin_pass' : 'aaaaa'")
"'admin_pass' : '***'"
>>> mask_password('"password" : "aaaaa"')
'"password" : "***"'
>>> mask_password("'original_password' : 'aaaaa'")
"'original_password' : '***'"
>>> mask_password("u'original_password' : u'aaaaa'")
"u'original_password' : u'***'"
"""
message = six.text_type(message)
# NOTE(ldbragst): Check to see if anything in message contains any key
# specified in _SANITIZE_KEYS, if not then just return the message since
# we don't have to mask any passwords.
if not any(key in message for key in _SANITIZE_KEYS):
return message
secret = r'\g<1>' + secret + r'\g<2>'
for pattern in _SANITIZE_PATTERNS:
message = re.sub(pattern, secret, message)
return message

View File

@@ -79,7 +79,10 @@ class ServiceCatalog(object):
if not matching_endpoints:
raise cinderclient.exceptions.EndpointNotFound()
elif len(matching_endpoints) > 1:
raise cinderclient.exceptions.AmbiguousEndpoints(
endpoints=matching_endpoints)
try:
eplist = [ep[attr] for ep in matching_endpoints]
except KeyError:
eplist = matching_endpoints
raise cinderclient.exceptions.AmbiguousEndpoints(endpoints=eplist)
else:
return matching_endpoints[0][endpoint_type]

View File

@@ -1,5 +1,5 @@
# Copyright 2011 OpenStack LLC.
# Copyright 2011-2014 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -24,19 +24,31 @@ import argparse
import glob
import imp
import itertools
import logging
import os
import pkgutil
import sys
import logging
import requests
from cinderclient import client
from cinderclient import exceptions as exc
from cinderclient import utils
import cinderclient.auth_plugin
import cinderclient.extension
from cinderclient.openstack.common import strutils
from cinderclient import utils
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 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
import six.moves.urllib.parse as urlparse
DEFAULT_OS_VOLUME_API_VERSION = "1"
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
DEFAULT_CINDER_SERVICE_TYPE = 'volume'
@@ -57,7 +69,7 @@ class CinderClientArgumentParser(argparse.ArgumentParser):
exits.
"""
self.print_usage(sys.stderr)
#FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
# FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
choose_from = ' (choose from'
progparts = self.prog.partition(' ')
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
@@ -66,6 +78,29 @@ class CinderClientArgumentParser(argparse.ArgumentParser):
'mainp': progparts[0],
'subp': progparts[2]})
def _get_option_tuples(self, option_string):
"""Avoid ambiguity in argument abbreviation.
The idea of this method is to override the default behaviour to
avoid ambiguity in the abbreviation feature of argparse.
In the case that the ambiguity is generated by 2 or more parameters
and only one is visible in the help and the others are with
help=argparse.SUPPRESS, the ambiguity is solved by taking the visible
one.
The use case is for parameters that are left hidden for backward
compatibility.
"""
result = super(CinderClientArgumentParser, self)._get_option_tuples(
option_string)
if len(result) > 1:
aux = [x for x in result if x[0].help != argparse.SUPPRESS]
if len(aux) == 1:
result = aux
return result
class OpenStackCinderShell(object):
@@ -73,8 +108,7 @@ class OpenStackCinderShell(object):
parser = CinderClientArgumentParser(
prog='cinder',
description=__doc__.strip(),
epilog='See "cinder help COMMAND" '
'for help on a specific command.',
epilog='Run "cinder help SUBCOMMAND" for help on a subcommand.',
add_help=False,
formatter_class=OpenStackHelpFormatter,
)
@@ -92,73 +126,35 @@ class OpenStackCinderShell(object):
action='store_true',
default=utils.env('CINDERCLIENT_DEBUG',
default=False),
help="Print debugging output")
help="Shows debugging output.")
parser.add_argument('--os-username',
metavar='<auth-user-name>',
default=utils.env('OS_USERNAME',
'CINDER_USERNAME'),
help='Defaults to env[OS_USERNAME].')
parser.add_argument('--os_username',
help=argparse.SUPPRESS)
parser.add_argument('--os-password',
metavar='<auth-password>',
default=utils.env('OS_PASSWORD',
'CINDER_PASSWORD'),
help='Defaults to env[OS_PASSWORD].')
parser.add_argument('--os_password',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-name',
metavar='<auth-tenant-name>',
default=utils.env('OS_TENANT_NAME',
'CINDER_PROJECT_ID'),
help='Defaults to env[OS_TENANT_NAME].')
parser.add_argument('--os_tenant_name',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-id',
metavar='<auth-tenant-id>',
default=utils.env('OS_TENANT_ID',
'CINDER_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID].')
parser.add_argument('--os_tenant_id',
help=argparse.SUPPRESS)
parser.add_argument('--os-auth-url',
metavar='<auth-url>',
default=utils.env('OS_AUTH_URL',
'CINDER_URL'),
help='Defaults to env[OS_AUTH_URL].')
parser.add_argument('--os_auth_url',
help=argparse.SUPPRESS)
parser.add_argument('--os-region-name',
metavar='<region-name>',
default=utils.env('OS_REGION_NAME',
'CINDER_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
parser.add_argument('--os_region_name',
parser.add_argument('--os-auth-system',
metavar='<auth-system>',
default=utils.env('OS_AUTH_SYSTEM'),
help='Defaults to env[OS_AUTH_SYSTEM].')
parser.add_argument('--os_auth_system',
help=argparse.SUPPRESS)
parser.add_argument('--service-type',
metavar='<service-type>',
help='Defaults to volume for most actions')
help='Service type. '
'For most actions, default is volume.')
parser.add_argument('--service_type',
help=argparse.SUPPRESS)
parser.add_argument('--service-name',
metavar='<service-name>',
default=utils.env('CINDER_SERVICE_NAME'),
help='Defaults to env[CINDER_SERVICE_NAME]')
help='Service name. '
'Default=env[CINDER_SERVICE_NAME].')
parser.add_argument('--service_name',
help=argparse.SUPPRESS)
parser.add_argument('--volume-service-name',
metavar='<volume-service-name>',
default=utils.env('CINDER_VOLUME_SERVICE_NAME'),
help='Defaults to env[CINDER_VOLUME_SERVICE_NAME]')
help='Volume service name. '
'Default=env[CINDER_VOLUME_SERVICE_NAME].')
parser.add_argument('--volume_service_name',
help=argparse.SUPPRESS)
@@ -166,67 +162,220 @@ class OpenStackCinderShell(object):
metavar='<endpoint-type>',
default=utils.env('CINDER_ENDPOINT_TYPE',
default=DEFAULT_CINDER_ENDPOINT_TYPE),
help='Defaults to env[CINDER_ENDPOINT_TYPE] or '
help='Endpoint type, which is publicURL or '
'internalURL. '
'Default=nova env[CINDER_ENDPOINT_TYPE] or '
+ DEFAULT_CINDER_ENDPOINT_TYPE + '.')
parser.add_argument('--endpoint_type',
help=argparse.SUPPRESS)
parser.add_argument('--os-volume-api-version',
metavar='<volume-api-ver>',
default=utils.env('OS_VOLUME_API_VERSION',
default=None),
help='Accepts 1 or 2,defaults '
'to env[OS_VOLUME_API_VERSION].')
default=None),
help='Block Storage API version. '
'Valid values are 1 or 2. '
'Default=env[OS_VOLUME_API_VERSION].')
parser.add_argument('--os_volume_api_version',
help=argparse.SUPPRESS)
parser.add_argument('--os-cacert',
metavar='<ca-certificate>',
default=utils.env('OS_CACERT', default=None),
help='Specify a CA bundle file to use in '
'verifying a TLS (https) server certificate. '
'Defaults to env[OS_CACERT]')
parser.add_argument('--insecure',
default=utils.env('CINDERCLIENT_INSECURE',
default=False),
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--retries',
metavar='<retries>',
type=int,
default=0,
help='Number of retries.')
# FIXME(dtroyer): The args below are here for diablo compatibility,
# remove them in folsum cycle
self._append_global_identity_args(parser)
# alias for --os-username, left in for backwards compatibility
parser.add_argument('--username',
help=argparse.SUPPRESS)
# alias for --os-region_name, left in for backwards compatibility
parser.add_argument('--region_name',
help=argparse.SUPPRESS)
# alias for --os-password, left in for backwards compatibility
parser.add_argument('--apikey', '--password', dest='apikey',
default=utils.env('CINDER_API_KEY'),
help=argparse.SUPPRESS)
# alias for --os-tenant-name, left in for backward compatibility
parser.add_argument('--projectid', '--tenant_name', dest='projectid',
default=utils.env('CINDER_PROJECT_ID'),
help=argparse.SUPPRESS)
# alias for --os-auth-url, left in for backward compatibility
parser.add_argument('--url', '--auth_url', dest='url',
default=utils.env('CINDER_URL'),
help=argparse.SUPPRESS)
# The auth-system-plugins might require some extra options
cinderclient.auth_plugin.discover_auth_systems()
cinderclient.auth_plugin.load_auth_system_opts(parser)
return parser
def _append_global_identity_args(self, parser):
# FIXME(bklei): these are global identity (Keystone) arguments which
# should be consistent and shared by all service clients. Therefore,
# they should be provided by python-keystoneclient. We will need to
# refactor this code once this functionality is available in
# python-keystoneclient.
parser.add_argument(
'--os-auth-strategy', metavar='<auth-strategy>',
default=utils.env('OS_AUTH_STRATEGY', default='keystone'),
help=_('Authentication strategy (Env: OS_AUTH_STRATEGY'
', default keystone). For now, any other value will'
' disable the authentication'))
parser.add_argument(
'--os_auth_strategy',
help=argparse.SUPPRESS)
parser.add_argument('--os-username',
metavar='<auth-user-name>',
default=utils.env('OS_USERNAME',
'CINDER_USERNAME'),
help='OpenStack user name. '
'Default=env[OS_USERNAME].')
parser.add_argument('--os_username',
help=argparse.SUPPRESS)
parser.add_argument('--os-password',
metavar='<auth-password>',
default=utils.env('OS_PASSWORD',
'CINDER_PASSWORD'),
help='Password for OpenStack user. '
'Default=env[OS_PASSWORD].')
parser.add_argument('--os_password',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-name',
metavar='<auth-tenant-name>',
default=utils.env('OS_TENANT_NAME',
'CINDER_PROJECT_ID'),
help='Tenant name. '
'Default=env[OS_TENANT_NAME].')
parser.add_argument('--os_tenant_name',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-id',
metavar='<auth-tenant-id>',
default=utils.env('OS_TENANT_ID',
'CINDER_TENANT_ID'),
help='ID for the tenant. '
'Default=env[OS_TENANT_ID].')
parser.add_argument('--os_tenant_id',
help=argparse.SUPPRESS)
parser.add_argument('--os-auth-url',
metavar='<auth-url>',
default=utils.env('OS_AUTH_URL',
'CINDER_URL'),
help='URL for the authentication service. '
'Default=env[OS_AUTH_URL].')
parser.add_argument('--os_auth_url',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-user-id', metavar='<auth-user-id>',
default=utils.env('OS_USER_ID'),
help=_('Authentication user ID (Env: OS_USER_ID)'))
parser.add_argument(
'--os_user_id',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-user-domain-id',
metavar='<auth-user-domain-id>',
default=utils.env('OS_USER_DOMAIN_ID'),
help='OpenStack user domain ID. '
'Defaults to env[OS_USER_DOMAIN_ID].')
parser.add_argument(
'--os_user_domain_id',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-user-domain-name',
metavar='<auth-user-domain-name>',
default=utils.env('OS_USER_DOMAIN_NAME'),
help='OpenStack user domain name. '
'Defaults to env[OS_USER_DOMAIN_NAME].')
parser.add_argument(
'--os_user_domain_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-project-id',
metavar='<auth-project-id>',
default=utils.env('OS_PROJECT_ID'),
help='Another way to specify tenant ID. '
'This option is mutually exclusive with '
' --os-tenant-id. '
'Defaults to env[OS_PROJECT_ID].')
parser.add_argument(
'--os_project_id',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-project-name',
metavar='<auth-project-name>',
default=utils.env('OS_PROJECT_NAME'),
help='Another way to specify tenant name. '
'This option is mutually exclusive with '
' --os-tenant-name. '
'Defaults to env[OS_PROJECT_NAME].')
parser.add_argument(
'--os_project_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-project-domain-id',
metavar='<auth-project-domain-id>',
default=utils.env('OS_PROJECT_DOMAIN_ID'),
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
parser.add_argument(
'--os-project-domain-name',
metavar='<auth-project-domain-name>',
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
parser.add_argument(
'--os-cert',
metavar='<certificate>',
default=utils.env('OS_CERT'),
help='Defaults to env[OS_CERT].')
parser.add_argument(
'--os-key',
metavar='<key>',
default=utils.env('OS_KEY'),
help='Defaults to env[OS_KEY].')
parser.add_argument('--os-region-name',
metavar='<region-name>',
default=utils.env('OS_REGION_NAME',
'CINDER_REGION_NAME'),
help='Region name. '
'Default=env[OS_REGION_NAME].')
parser.add_argument('--os_region_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-token', metavar='<token>',
default=utils.env('OS_TOKEN'),
help=_('Defaults to env[OS_TOKEN]'))
parser.add_argument(
'--os_token',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-url', metavar='<url>',
default=utils.env('OS_URL'),
help=_('Defaults to env[OS_URL]'))
parser.add_argument(
'--os_url',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-cacert',
metavar='<ca-certificate>',
default=utils.env('OS_CACERT', default=None),
help=_("Specify a CA bundle file to use in "
"verifying a TLS (https) server certificate. "
"Defaults to env[OS_CACERT]"))
parser.add_argument('--insecure',
default=utils.env('CINDERCLIENT_INSECURE',
default=False),
action='store_true',
help=argparse.SUPPRESS)
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
@@ -331,12 +480,24 @@ class OpenStackCinderShell(object):
logger.setLevel(logging.WARNING)
logger.addHandler(streamhandler)
client_logger = logging.getLogger(client.__name__)
ch = logging.StreamHandler()
client_logger.setLevel(logging.DEBUG)
client_logger.addHandler(ch)
if hasattr(requests, 'logging'):
requests.logging.getLogger(requests.__name__).addHandler(ch)
# required for logging when using a keystone session
ks_logger = logging.getLogger("keystoneclient")
ks_logger.setLevel(logging.DEBUG)
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
self.options = options
if not options.os_volume_api_version:
# Environment variable OS_VOLUME_API_VERSION was
@@ -372,15 +533,19 @@ 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,
username, apikey, projectid, url, region_name, cacert) = (
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.username,
args.apikey, args.projectid,
args.url, args.region_name, args.os_cacert)
args.volume_service_name, 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:
auth_plugin = None
if not endpoint_type:
endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE
@@ -389,54 +554,71 @@ class OpenStackCinderShell(object):
service_type = DEFAULT_CINDER_SERVICE_TYPE
service_type = utils.get_service_type(args.func) or service_type
#FIXME(usrleon): Here should be restrict for project id same as
# FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not.
if not utils.isunauthenticated(args.func):
if not os_username:
if not username:
raise exc.CommandError(
"You must provide a username "
"via either --os-username or env[OS_USERNAME]")
else:
os_username = username
if auth_plugin:
auth_plugin.parse_opts(args)
if not auth_plugin or not auth_plugin.opts:
if not os_username:
raise exc.CommandError("You must provide a user name "
"through --os-username or "
"env[OS_USERNAME].")
if not os_password:
if not apikey:
raise exc.CommandError("You must provide a password "
"via either --os-password or via "
"env[OS_PASSWORD]")
else:
os_password = apikey
raise exc.CommandError("You must provide a password "
"through --os-password or "
"env[OS_PASSWORD].")
if not (os_tenant_name or os_tenant_id):
if not projectid:
raise exc.CommandError("You must provide a tenant_id "
"via either --os-tenant-id or "
"env[OS_TENANT_ID]")
else:
os_tenant_name = projectid
raise exc.CommandError("You must provide a tenant ID "
"through --os-tenant-id or "
"env[OS_TENANT_ID].")
# V3 stuff
project_info_provided = self.options.os_tenant_name or \
self.options.os_tenant_id or \
(self.options.os_project_name and
(self.options.project_domain_name or
self.options.project_domain_id)) or \
self.options.os_project_id
if (not project_info_provided):
raise exc.CommandError(
_("You must provide a tenant_name, tenant_id, "
"project_id or project_name (with "
"project_domain_name or project_domain_id) via "
" --os-tenant-name (env[OS_TENANT_NAME]),"
" --os-tenant-id (env[OS_TENANT_ID]),"
" --os-project-id (env[OS_PROJECT_ID])"
" --os-project-name (env[OS_PROJECT_NAME]),"
" --os-project-domain-id "
"(env[OS_PROJECT_DOMAIN_ID])"
" --os-project-domain-name "
"(env[OS_PROJECT_DOMAIN_NAME])"))
if not os_auth_url:
if not url:
raise exc.CommandError(
"You must provide an auth url "
"via either --os-auth-url or env[OS_AUTH_URL]")
else:
os_auth_url = url
if os_auth_system and os_auth_system != 'keystone':
os_auth_url = auth_plugin.get_auth_url()
if not os_region_name and region_name:
os_region_name = region_name
if not os_auth_url:
raise exc.CommandError(
"You must provide an authentication URL "
"through --os-auth-url or env[OS_AUTH_URL].")
if not (os_tenant_name or os_tenant_id):
raise exc.CommandError(
"You must provide a tenant_id "
"via either --os-tenant-id or env[OS_TENANT_ID]")
"You must provide a tenant ID "
"through --os-tenant-id or env[OS_TENANT_ID].")
if not os_auth_url:
raise exc.CommandError(
"You must provide an auth url "
"via either --os-auth-url or env[OS_AUTH_URL]")
"You must provide an authentication URL "
"through --os-auth-url or env[OS_AUTH_URL].")
auth_session = self._get_keystone_session()
self.cs = client.Client(options.os_volume_api_version, os_username,
os_password, os_tenant_name, os_auth_url,
@@ -449,15 +631,17 @@ class OpenStackCinderShell(object):
volume_service_name=volume_service_name,
retries=options.retries,
http_log_debug=args.debug,
cacert=cacert)
cacert=cacert, auth_system=os_auth_system,
auth_plugin=auth_plugin,
session=auth_session)
try:
if not utils.isunauthenticated(args.func):
self.cs.authenticate()
except exc.Unauthorized:
raise exc.CommandError("Invalid OpenStack Cinder credentials.")
raise exc.CommandError("OpenStack credentials are not valid.")
except exc.AuthorizationFailure:
raise exc.CommandError("Unable to authorize user")
raise exc.CommandError("Unable to authorize user.")
endpoint_api_version = None
# Try to get the API version from the endpoint URL. If that fails fall
@@ -468,37 +652,37 @@ class OpenStackCinderShell(object):
endpoint_api_version = \
self.cs.get_volume_api_version_from_endpoint()
if endpoint_api_version != options.os_volume_api_version:
msg = (("Volume API version is set to %s "
msg = (("OpenStack Block Storage API version is set to %s "
"but you are accessing a %s endpoint. "
"Change its value via either --os-volume-api-version "
"or env[OS_VOLUME_API_VERSION]")
"Change its value through --os-volume-api-version "
"or env[OS_VOLUME_API_VERSION].")
% (options.os_volume_api_version, endpoint_api_version))
raise exc.InvalidAPIVersion(msg)
except exc.UnsupportedVersion:
endpoint_api_version = options.os_volume_api_version
if api_version_input:
logger.warning("Unable to determine the API version via "
"endpoint URL. Falling back to user "
"specified version: %s" %
logger.warning("Cannot determine the API version from "
"the endpoint URL. Falling back to the "
"user-specified version: %s" %
endpoint_api_version)
else:
logger.warning("Unable to determine the API version from "
"endpoint URL or user input. Falling back to "
"default API version: %s" %
logger.warning("Cannot determine the API version from the "
"endpoint URL or user input. Falling back "
"to the default API version: %s" %
endpoint_api_version)
args.func(self.cs, args)
def _run_extension_hooks(self, hook_type, *args, **kwargs):
"""Run hooks for all registered extensions."""
"""Runs hooks for all registered extensions."""
for extension in self.extensions:
extension.run_hooks(hook_type, *args, **kwargs)
def do_bash_completion(self, args):
"""Print arguments for bash_completion.
"""Prints arguments for bash_completion.
Prints all of the commands and options to stdout so that the
cinder.bash_completion script doesn't have to hard code them.
Prints all commands and options to stdout so that the
cinder.bash_completion script does not have to hard code them.
"""
commands = set()
options = set()
@@ -512,10 +696,10 @@ class OpenStackCinderShell(object):
print(' '.join(commands | options))
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
help='Shows help for <subcommand>.')
def do_help(self, args):
"""
Display help about this program or one of its subcommands.
Shows help about this program or one of its subcommands.
"""
if args.command:
if args.command in self.subcommands:
@@ -526,9 +710,127 @@ class OpenStackCinderShell(object):
else:
self.parser.print_help()
def get_v2_auth(self, v2_auth_url):
username = self.options.os_username
password = self.options.os_password
tenant_id = self.options.os_tenant_id
tenant_name = self.options.os_tenant_name
return v2_auth.Password(
v2_auth_url,
username=username,
password=password,
tenant_id=tenant_id,
tenant_name=tenant_name)
def get_v3_auth(self, v3_auth_url):
username = self.options.os_username
user_id = self.options.os_user_id
user_domain_name = self.options.os_user_domain_name
user_domain_id = self.options.os_user_domain_id
password = self.options.os_password
project_id = self.options.os_project_id or self.options.os_tenant_id
project_name = (self.options.os_project_name
or self.options.os_tenant_name)
project_domain_name = self.options.os_project_domain_name
project_domain_id = self.options.os_project_domain_id
return v3_auth.Password(
v3_auth_url,
username=username,
password=password,
user_id=user_id,
user_domain_name=user_domain_name,
user_domain_id=user_domain_id,
project_id=project_id,
project_name=project_name,
project_domain_name=project_domain_name,
project_domain_id=project_domain_id,
)
def _discover_auth_versions(self, session, auth_url):
# discover the API versions the server is supporting based on the
# given URL
v2_auth_url = None
v3_auth_url = None
try:
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:
# Discovery response mismatch. Raise the error
raise
except Exception:
# Some public clouds throw some other exception or doesn't support
# discovery. In that case try to determine version from auth_url
# API version from the original URL
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
v3_auth_url = auth_url
elif path.startswith('/v2'):
v2_auth_url = auth_url
else:
raise exc.CommandError('Unable to determine the Keystone'
' version to authenticate with '
'using the given auth_url.')
return (v2_auth_url, v3_auth_url)
def _get_keystone_session(self, **kwargs):
# first create a Keystone session
cacert = self.options.os_cacert or None
cert = self.options.os_cert or None
insecure = self.options.insecure or False
if insecure:
verify = False
else:
verify = cacert or True
ks_session = session.Session(verify=verify, cert=cert)
# discover the supported keystone versions using the given url
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
session=ks_session,
auth_url=self.options.os_auth_url)
username = self.options.os_username or None
user_domain_name = self.options.os_user_domain_name or None
user_domain_id = self.options.os_user_domain_id or None
auth = None
if v3_auth_url and v2_auth_url:
# support both v2 and v3 auth. Use v3 if possible.
if username:
if user_domain_name or user_domain_id:
# use v3 auth
auth = self.get_v3_auth(v3_auth_url)
else:
# use v2 auth
auth = self.get_v2_auth(v2_auth_url)
elif v3_auth_url:
# support only v3
auth = self.get_v3_auth(v3_auth_url)
elif v2_auth_url:
# support only v2
auth = self.get_v2_auth(v2_auth_url)
else:
raise exc.CommandError('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url.')
ks_session.auth = auth
return ks_session
# I'm picky about my shell help.
class OpenStackHelpFormatter(argparse.HelpFormatter):
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
@@ -541,7 +843,7 @@ def main():
OpenStackCinderShell().main(sys.argv[1:])
else:
OpenStackCinderShell().main(map(strutils.safe_decode,
sys.argv[1:]))
sys.argv[1:]))
except KeyboardInterrupt:
print("... terminating cinder client", file=sys.stderr)
sys.exit(130)
@@ -552,4 +854,5 @@ def main():
if __name__ == "__main__":
main()

View File

@@ -34,7 +34,22 @@ def assert_has_keys(dict, required=[], optional=[]):
class FakeClient(object):
def assert_called(self, method, url, body=None, pos=-1, **kwargs):
def _dict_match(self, partial, real):
result = True
try:
for key, value in partial.items():
if type(value) is dict:
result = self._dict_match(value, real[key])
else:
assert real[key] == value
result = True
except (AssertionError, KeyError):
result = False
return result
def assert_called(self, method, url, body=None,
partial_body=None, pos=-1, **kwargs):
"""
Assert than an API method was just called.
"""
@@ -50,7 +65,17 @@ class FakeClient(object):
if body is not None:
assert self.client.callstack[pos][2] == body
def assert_called_anytime(self, method, url, body=None):
if partial_body is not None:
try:
assert self._dict_match(partial_body,
self.client.callstack[pos][2])
except AssertionError:
print(self.client.callstack[pos][2])
print("does not contain")
print(partial_body)
raise
def assert_called_anytime(self, method, url, body=None, partial_body=None):
"""
Assert than an API method was called anytime in the test.
"""
@@ -77,6 +102,15 @@ class FakeClient(object):
print(body)
raise
if partial_body is not None:
try:
assert self._dict_match(partial_body, entry[2])
except AssertionError:
print(entry[2])
print("does not contain")
print(partial_body)
raise
def clear_callstack(self):
self.client.callstack = []

View File

@@ -0,0 +1,80 @@
# 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 datetime import datetime
from cinderclient.tests.fixture_data import base
# FIXME(jamielennox): use timeutils from oslo
FORMAT = '%Y-%m-%d %H:%M:%S'
class Fixture(base.Fixture):
base_url = 'os-availability-zone'
def setUp(self):
super(Fixture, self).setUp()
get_availability = {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": None,
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
}
self.requests.register_uri('GET', self.url(), json=get_availability)
updated_1 = datetime(2012, 12, 26, 14, 45, 25, 0).strftime(FORMAT)
updated_2 = datetime(2012, 12, 26, 14, 45, 24, 0).strftime(FORMAT)
get_detail = {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-volume": {
"active": True,
"available": True,
"updated_at": updated_1,
}
}
}
},
{
"zoneName": "internal",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-sched": {
"active": True,
"available": True,
"updated_at": updated_2,
}
}
}
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
}
self.requests.register_uri('GET', self.url('detail'), json=get_detail)

View File

@@ -0,0 +1,38 @@
# 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 fixtures
IDENTITY_URL = 'http://identityserver:5000/v2.0'
VOLUME_URL = 'http://volume.host'
class Fixture(fixtures.Fixture):
base_url = None
json_headers = {'Content-Type': 'application/json'}
def __init__(self, requests,
volume_url=VOLUME_URL,
identity_url=IDENTITY_URL):
super(Fixture, self).__init__()
self.requests = requests
self.volume_url = volume_url
self.identity_url = identity_url
def url(self, *args):
url_args = [self.volume_url]
if self.base_url:
url_args.append(self.base_url)
return '/'.join(str(a).strip('/') for a in tuple(url_args) + args)

View File

@@ -0,0 +1,64 @@
# 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 keystoneclient import fixture
from cinderclient.tests.fixture_data import base
from cinderclient.v1 import client as v1client
from cinderclient.v2 import client as v2client
class Base(base.Fixture):
def __init__(self, *args, **kwargs):
super(Base, self).__init__(*args, **kwargs)
self.token = fixture.V2Token()
self.token.set_scope()
def setUp(self):
super(Base, self).setUp()
auth_url = '%s/tokens' % self.identity_url
self.requests.register_uri('POST', auth_url,
json=self.token,
headers=self.json_headers)
class V1(Base):
def __init__(self, *args, **kwargs):
super(V1, self).__init__(*args, **kwargs)
svc = self.token.add_service('volume')
svc.add_endpoint(self.volume_url)
def new_client(self):
return v1client.Client(username='xx',
api_key='xx',
project_id='xx',
auth_url=self.identity_url)
class V2(Base):
def __init__(self, *args, **kwargs):
super(V2, self).__init__(*args, **kwargs)
svc = self.token.add_service('volumev2')
svc.add_endpoint(self.volume_url)
def new_client(self):
return v2client.Client(username='xx',
api_key='xx',
project_id='xx',
auth_url=self.identity_url)

View File

@@ -0,0 +1,230 @@
# 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 copy
import json
import uuid
# these are copied from python-keystoneclient tests
BASE_HOST = 'http://keystone.example.com'
BASE_URL = "%s:5000/" % BASE_HOST
UPDATED = '2013-03-06T00:00:00Z'
V2_URL = "%sv2.0" % BASE_URL
V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/'
'openstack-identity-service/2.0/content/',
'rel': 'describedby',
'type': 'text/html'}
V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident'
'ity-service/2.0/identity-dev-guide-2.0.pdf',
'rel': 'describedby',
'type': 'application/pdf'}
V2_VERSION = {'id': 'v2.0',
'links': [{'href': V2_URL, 'rel': 'self'},
V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF],
'status': 'stable',
'updated': UPDATED}
V3_URL = "%sv3" % BASE_URL
V3_MEDIA_TYPES = [{'base': 'application/json',
'type': 'application/vnd.openstack.identity-v3+json'},
{'base': 'application/xml',
'type': 'application/vnd.openstack.identity-v3+xml'}]
V3_VERSION = {'id': 'v3.0',
'links': [{'href': V3_URL, 'rel': 'self'}],
'media-types': V3_MEDIA_TYPES,
'status': 'stable',
'updated': UPDATED}
WRONG_VERSION_RESPONSE = {'id': 'v2.0',
'links': [V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF],
'status': 'stable',
'updated': UPDATED}
def _create_version_list(versions):
return json.dumps({'versions': {'values': versions}})
def _create_single_version(version):
return json.dumps({'version': version})
V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
V2_VERSION_LIST = _create_version_list([V2_VERSION])
V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
V2_VERSION_ENTRY = _create_single_version(V2_VERSION)
CINDER_ENDPOINT = 'http://www.cinder.com/v1'
def _get_normalized_token_data(**kwargs):
ref = copy.deepcopy(kwargs)
# normalized token data
ref['user_id'] = ref.get('user_id', uuid.uuid4().hex)
ref['username'] = ref.get('username', uuid.uuid4().hex)
ref['project_id'] = ref.get('project_id',
ref.get('tenant_id', uuid.uuid4().hex))
ref['project_name'] = ref.get('tenant_name',
ref.get('tenant_name', uuid.uuid4().hex))
ref['user_domain_id'] = ref.get('user_domain_id', uuid.uuid4().hex)
ref['user_domain_name'] = ref.get('user_domain_name', uuid.uuid4().hex)
ref['project_domain_id'] = ref.get('project_domain_id', uuid.uuid4().hex)
ref['project_domain_name'] = ref.get('project_domain_name',
uuid.uuid4().hex)
ref['roles'] = ref.get('roles', [{'name': uuid.uuid4().hex,
'id': uuid.uuid4().hex}])
ref['roles_link'] = ref.get('roles_link', [])
ref['cinder_url'] = ref.get('cinder_url', CINDER_ENDPOINT)
return ref
def generate_v2_project_scoped_token(**kwargs):
"""Generate a Keystone V2 token based on auth request."""
ref = _get_normalized_token_data(**kwargs)
token = uuid.uuid4().hex
o = {'access': {'token': {'id': token,
'expires': '2099-05-22T00:02:43.941430Z',
'issued_at': '2013-05-21T00:02:43.941473Z',
'tenant': {'enabled': True,
'id': ref.get('project_id'),
'name': ref.get('project_id')
}
},
'user': {'id': ref.get('user_id'),
'name': uuid.uuid4().hex,
'username': ref.get('username'),
'roles': ref.get('roles'),
'roles_links': ref.get('roles_links')
}
}}
# we only care about Neutron and Keystone endpoints
o['access']['serviceCatalog'] = [
{'endpoints': [
{'publicURL': 'public_' + ref.get('cinder_url'),
'internalURL': 'internal_' + ref.get('cinder_url'),
'adminURL': 'admin_' + (ref.get('auth_url') or ""),
'id': uuid.uuid4().hex,
'region': 'RegionOne'
}],
'endpoints_links': [],
'name': 'Neutron',
'type': 'network'},
{'endpoints': [
{'publicURL': ref.get('auth_url'),
'adminURL': ref.get('auth_url'),
'internalURL': ref.get('auth_url'),
'id': uuid.uuid4().hex,
'region': 'RegionOne'
}],
'endpoint_links': [],
'name': 'keystone',
'type': 'identity'}]
return token, o
def generate_v3_project_scoped_token(**kwargs):
"""Generate a Keystone V3 token based on auth request."""
ref = _get_normalized_token_data(**kwargs)
o = {'token': {'expires_at': '2099-05-22T00:02:43.941430Z',
'issued_at': '2013-05-21T00:02:43.941473Z',
'methods': ['password'],
'project': {'id': ref.get('project_id'),
'name': ref.get('project_name'),
'domain': {'id': ref.get('project_domain_id'),
'name': ref.get(
'project_domain_name')
}
},
'user': {'id': ref.get('user_id'),
'name': ref.get('username'),
'domain': {'id': ref.get('user_domain_id'),
'name': ref.get('user_domain_name')
}
},
'roles': ref.get('roles')
}}
# we only care about Neutron and Keystone endpoints
o['token']['catalog'] = [
{'endpoints': [
{
'id': uuid.uuid4().hex,
'interface': 'public',
'region': 'RegionOne',
'url': 'public_' + ref.get('cinder_url')
},
{
'id': uuid.uuid4().hex,
'interface': 'internal',
'region': 'RegionOne',
'url': 'internal_' + ref.get('cinder_url')
},
{
'id': uuid.uuid4().hex,
'interface': 'admin',
'region': 'RegionOne',
'url': 'admin_' + ref.get('cinder_url')
}],
'id': uuid.uuid4().hex,
'type': 'network'},
{'endpoints': [
{
'id': uuid.uuid4().hex,
'interface': 'public',
'region': 'RegionOne',
'url': ref.get('auth_url')
},
{
'id': uuid.uuid4().hex,
'interface': 'admin',
'region': 'RegionOne',
'url': ref.get('auth_url')
}],
'id': uuid.uuid4().hex,
'type': 'identity'}]
# token ID is conveyed via the X-Subject-Token header so we are generating
# one to stash there
token_id = uuid.uuid4().hex
return token_id, o
def keystone_request_callback(request, context):
context.headers['Content-Type'] = 'application/json'
if request.url == BASE_URL:
return V3_VERSION_LIST
elif request.url == BASE_URL + "/v2.0":
token_id, token_data = generate_v2_project_scoped_token()
return token_data
elif request.url == BASE_URL + "/v3":
token_id, token_data = generate_v3_project_scoped_token()
context.headers["X-Subject-Token"] = token_id
context.status_code = 201
return token_data
elif "WrongDiscoveryResponse.discovery.com" in request.url:
return str(WRONG_VERSION_RESPONSE)
else:
context.status_code = 500
return str(WRONG_VERSION_RESPONSE)

View File

@@ -0,0 +1,56 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from cinderclient.tests.fixture_data import base
def _stub_snapshot(**kwargs):
snapshot = {
"created_at": "2012-08-28T16:30:31.000000",
"display_description": None,
"display_name": None,
"id": '11111111-1111-1111-1111-111111111111',
"size": 1,
"status": "available",
"volume_id": '00000000-0000-0000-0000-000000000000',
}
snapshot.update(kwargs)
return snapshot
class Fixture(base.Fixture):
base_url = 'snapshots'
def setUp(self):
super(Fixture, self).setUp()
snapshot_1234 = _stub_snapshot(id='1234')
self.requests.register_uri('GET', self.url('1234'),
json={'snapshot': snapshot_1234})
def action_1234(request, context):
return ''
body = json.loads(request.body.decode('utf-8'))
assert len(list(body)) == 1
action = list(body)[0]
if action == 'os-reset_status':
assert 'status' in body['os-reset_status']
elif action == 'os-update_snapshot_status':
assert 'status' in body['os-update_snapshot_status']
else:
raise AssertionError("Unexpected action: %s" % action)
return ''
self.requests.register_uri('POST', self.url('1234', 'action'),
text=action_1234, status_code=202)

View File

@@ -0,0 +1,346 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import mock
import pkg_resources
import requests
try:
import json
except ImportError:
import simplejson as json
from cinderclient import auth_plugin
from cinderclient import exceptions
from cinderclient.tests import utils
from cinderclient.v1 import client
def mock_http_request(resp=None):
"""Mock an HTTP Request."""
if not resp:
resp = {
"access": {
"token": {
"expires": "12345",
"id": "FAKE_ID",
"tenant": {
"id": "FAKE_TENANT_ID",
}
},
"serviceCatalog": [
{
"type": "volume",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://localhost:8774/v1.1",
"internalURL": "http://localhost:8774/v1.1",
"publicURL": "http://localhost:8774/v1.1/",
},
],
},
],
},
}
auth_response = utils.TestResponse({
"status_code": 200,
"text": json.dumps(resp),
})
return mock.Mock(return_value=(auth_response))
def requested_headers(cs):
"""Return requested passed headers."""
return {
'User-Agent': cs.client.USER_AGENT,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
class DeprecatedAuthPluginTest(utils.TestCase):
def test_auth_system_success(self):
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return self.authenticate
def authenticate(self, cls, auth_url):
cls._authenticate(auth_url, {"fake": "me"})
def mock_iter_entry_points(_type, name):
if _type == 'openstack.client.authenticate':
return [MockEntrypoint("fake", "fake", ["fake"])]
else:
return []
mock_request = mock_http_request()
@mock.patch.object(pkg_resources, "iter_entry_points",
mock_iter_entry_points)
@mock.patch.object(requests, "request", mock_request)
def test_auth_call():
plugin = auth_plugin.DeprecatedAuthPlugin("fake")
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", auth_system="fake",
auth_plugin=plugin)
cs.client.authenticate()
headers = requested_headers(cs)
token_url = cs.client.auth_url + "/tokens"
mock_request.assert_called_with(
"POST",
token_url,
headers=headers,
data='{"fake": "me"}',
allow_redirects=True,
**self.TEST_REQUEST_BASE)
test_auth_call()
def test_auth_system_not_exists(self):
def mock_iter_entry_points(_t, name=None):
return [pkg_resources.EntryPoint("fake", "fake", ["fake"])]
mock_request = mock_http_request()
@mock.patch.object(pkg_resources, "iter_entry_points",
mock_iter_entry_points)
@mock.patch.object(requests.Session, "request", mock_request)
def test_auth_call():
auth_plugin.discover_auth_systems()
plugin = auth_plugin.DeprecatedAuthPlugin("notexists")
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", auth_system="notexists",
auth_plugin=plugin)
self.assertRaises(exceptions.AuthSystemNotFound,
cs.client.authenticate)
test_auth_call()
def test_auth_system_defining_auth_url(self):
class MockAuthUrlEntrypoint(pkg_resources.EntryPoint):
def load(self):
return self.auth_url
def auth_url(self):
return "http://faked/v2.0"
class MockAuthenticateEntrypoint(pkg_resources.EntryPoint):
def load(self):
return self.authenticate
def authenticate(self, cls, auth_url):
cls._authenticate(auth_url, {"fake": "me"})
def mock_iter_entry_points(_type, name):
if _type == 'openstack.client.auth_url':
return [MockAuthUrlEntrypoint("fakewithauthurl",
"fakewithauthurl",
["auth_url"])]
elif _type == 'openstack.client.authenticate':
return [MockAuthenticateEntrypoint("fakewithauthurl",
"fakewithauthurl",
["authenticate"])]
else:
return []
mock_request = mock_http_request()
@mock.patch.object(pkg_resources, "iter_entry_points",
mock_iter_entry_points)
@mock.patch.object(requests.Session, "request", mock_request)
def test_auth_call():
plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl")
cs = client.Client("username", "password", "project_id",
auth_system="fakewithauthurl",
auth_plugin=plugin)
cs.client.authenticate()
self.assertEqual(cs.client.auth_url, "http://faked/v2.0")
test_auth_call()
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_client_raises_exc_without_auth_url(self, mock_iter_entry_points):
class MockAuthUrlEntrypoint(pkg_resources.EntryPoint):
def load(self):
return self.auth_url
def auth_url(self):
return None
mock_iter_entry_points.side_effect = lambda _t, name: [
MockAuthUrlEntrypoint("fakewithauthurl",
"fakewithauthurl",
["auth_url"])]
plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl")
self.assertRaises(
exceptions.EndpointNotFound,
client.Client, "username", "password", "project_id",
auth_system="fakewithauthurl", auth_plugin=plugin)
class AuthPluginTest(utils.TestCase):
@mock.patch.object(requests, "request")
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_auth_system_success(self, mock_iter_entry_points, mock_request):
"""Test that we can authenticate using the auth system."""
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return FakePlugin
class FakePlugin(auth_plugin.BaseAuthPlugin):
def authenticate(self, cls, auth_url):
cls._authenticate(auth_url, {"fake": "me"})
mock_iter_entry_points.side_effect = lambda _t: [
MockEntrypoint("fake", "fake", ["FakePlugin"])]
mock_request.side_effect = mock_http_request()
auth_plugin.discover_auth_systems()
plugin = auth_plugin.load_plugin("fake")
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", auth_system="fake",
auth_plugin=plugin)
cs.client.authenticate()
headers = requested_headers(cs)
token_url = cs.client.auth_url + "/tokens"
mock_request.assert_called_with(
"POST",
token_url,
headers=headers,
data='{"fake": "me"}',
allow_redirects=True,
**self.TEST_REQUEST_BASE)
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_discover_auth_system_options(self, mock_iter_entry_points):
"""Test that we can load the auth system options."""
class FakePlugin(auth_plugin.BaseAuthPlugin):
@staticmethod
def add_opts(parser):
parser.add_argument('--auth_system_opt',
default=False,
action='store_true',
help="Fake option")
return parser
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return FakePlugin
mock_iter_entry_points.side_effect = lambda _t: [
MockEntrypoint("fake", "fake", ["FakePlugin"])]
parser = argparse.ArgumentParser()
auth_plugin.discover_auth_systems()
auth_plugin.load_auth_system_opts(parser)
opts, args = parser.parse_known_args(['--auth_system_opt'])
self.assertTrue(opts.auth_system_opt)
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_parse_auth_system_options(self, mock_iter_entry_points):
"""Test that we can parse the auth system options."""
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return FakePlugin
class FakePlugin(auth_plugin.BaseAuthPlugin):
def __init__(self):
self.opts = {"fake_argument": True}
def parse_opts(self, args):
return self.opts
mock_iter_entry_points.side_effect = lambda _t: [
MockEntrypoint("fake", "fake", ["FakePlugin"])]
auth_plugin.discover_auth_systems()
plugin = auth_plugin.load_plugin("fake")
plugin.parse_opts([])
self.assertIn("fake_argument", plugin.opts)
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_auth_system_defining_url(self, mock_iter_entry_points):
"""Test the auth_system defining an url."""
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return FakePlugin
class FakePlugin(auth_plugin.BaseAuthPlugin):
def get_auth_url(self):
return "http://faked/v2.0"
mock_iter_entry_points.side_effect = lambda _t: [
MockEntrypoint("fake", "fake", ["FakePlugin"])]
auth_plugin.discover_auth_systems()
plugin = auth_plugin.load_plugin("fake")
cs = client.Client("username", "password", "project_id",
auth_system="fakewithauthurl",
auth_plugin=plugin)
self.assertEqual(cs.client.auth_url, "http://faked/v2.0")
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_exception_if_no_authenticate(self, mock_iter_entry_points):
"""Test that no authenticate raises a proper exception."""
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return FakePlugin
class FakePlugin(auth_plugin.BaseAuthPlugin):
pass
mock_iter_entry_points.side_effect = lambda _t: [
MockEntrypoint("fake", "fake", ["FakePlugin"])]
auth_plugin.discover_auth_systems()
plugin = auth_plugin.load_plugin("fake")
self.assertRaises(
exceptions.EndpointNotFound,
client.Client, "username", "password", "project_id",
auth_system="fake", auth_plugin=plugin)
@mock.patch.object(pkg_resources, "iter_entry_points")
def test_exception_if_no_url(self, mock_iter_entry_points):
"""Test that no auth_url at all raises exception."""
class MockEntrypoint(pkg_resources.EntryPoint):
def load(self):
return FakePlugin
class FakePlugin(auth_plugin.BaseAuthPlugin):
pass
mock_iter_entry_points.side_effect = lambda _t: [
MockEntrypoint("fake", "fake", ["FakePlugin"])]
auth_plugin.discover_auth_systems()
plugin = auth_plugin.load_plugin("fake")
self.assertRaises(
exceptions.EndpointNotFound,
client.Client, "username", "password", "project_id",
auth_system="fake", auth_plugin=plugin)

View File

@@ -25,14 +25,14 @@ class BaseTest(utils.TestCase):
def test_resource_repr(self):
r = base.Resource(None, dict(foo="bar", baz="spam"))
self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
self.assertEqual("<Resource baz=spam, foo=bar>", repr(r))
def test_getid(self):
self.assertEqual(base.getid(4), 4)
self.assertEqual(4, base.getid(4))
class TmpObject(object):
id = 4
self.assertEqual(base.getid(TmpObject), 4)
self.assertEqual(4, base.getid(TmpObject))
def test_eq(self):
# Two resources of the same type with the same id: equal

View File

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

View File

@@ -24,7 +24,14 @@ fake_response = utils.TestResponse({
"status_code": 200,
"text": '{"hi": "there"}',
})
fake_response_empty = utils.TestResponse({
"status_code": 200,
"text": '{"access": {}}'
})
mock_request = mock.Mock(return_value=(fake_response))
mock_request_empty = mock.Mock(return_value=(fake_response_empty))
bad_400_response = utils.TestResponse({
"status_code": 400,
@@ -44,6 +51,9 @@ bad_500_response = utils.TestResponse({
})
bad_500_request = mock.Mock(return_value=(bad_500_response))
connection_error_request = mock.Mock(
side_effect=requests.exceptions.ConnectionError)
def get_client(retries=0):
cl = client.HTTPClient("username", "password",
@@ -77,7 +87,7 @@ class ClientTest(utils.TestCase):
headers=headers,
**self.TEST_REQUEST_BASE)
# Automatic JSON parsing
self.assertEqual(body, {"hi": "there"})
self.assertEqual({"hi": "there"}, body)
test_get_call()
@@ -101,13 +111,30 @@ class ClientTest(utils.TestCase):
resp, body = cl.get("/hi")
test_get_call()
self.assertEqual(self.requests, [])
self.assertEqual([], self.requests)
def test_get_retry_500(self):
cl = get_authed_client(retries=1)
self.requests = [bad_500_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)
def test_get_retry_connection_error(self):
cl = get_authed_client(retries=1)
self.requests = [connection_error_request, mock_request]
def request(*args, **kwargs):
next_request = self.requests.pop(0)
return next_request(*args, **kwargs)
@@ -135,7 +162,7 @@ class ClientTest(utils.TestCase):
resp, body = cl.get("/hi")
self.assertRaises(exceptions.ClientException, test_get_call)
self.assertEqual(self.requests, [mock_request])
self.assertEqual([mock_request], self.requests)
def test_get_no_retry_400(self):
cl = get_authed_client(retries=0)
@@ -152,7 +179,7 @@ class ClientTest(utils.TestCase):
resp, body = cl.get("/hi")
self.assertRaises(exceptions.BadRequest, test_get_call)
self.assertEqual(self.requests, [mock_request])
self.assertEqual([mock_request], self.requests)
def test_get_retry_400_socket(self):
cl = get_authed_client(retries=1)
@@ -169,7 +196,7 @@ class ClientTest(utils.TestCase):
resp, body = cl.get("/hi")
test_get_call()
self.assertEqual(self.requests, [])
self.assertEqual([], self.requests)
def test_post(self):
cl = get_authed_client()
@@ -197,8 +224,20 @@ class ClientTest(utils.TestCase):
cl = get_client()
# response must not have x-server-management-url header
@mock.patch.object(requests, "request", mock_request)
@mock.patch.object(requests, "request", mock_request_empty)
def test_auth_call():
self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate)
self.assertRaises(exceptions.AuthorizationFailure,
cl.authenticate)
test_auth_call()
def test_auth_not_implemented(self):
cl = get_client()
# response must not have x-server-management-url header
# {'hi': 'there'} is neither V2 or V3
@mock.patch.object(requests, "request", mock_request)
def test_auth_call():
self.assertRaises(NotImplementedError, cl.authenticate)
test_auth_call()

View File

@@ -240,10 +240,10 @@ class ServiceCatalogTest(utils.TestCase):
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
service_type='compute')
self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'),
"https://compute1.host/v1/1234")
self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'),
"https://compute1.host/v1/3456")
self.assertEqual("https://compute1.host/v1/1234",
sc.url_for('tenantId', '1', service_type='compute'))
self.assertEqual("https://compute1.host/v1/3456",
sc.url_for('tenantId', '2', service_type='compute'))
self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
"region", "South", service_type='compute')
@@ -253,15 +253,15 @@ class ServiceCatalogTest(utils.TestCase):
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
service_type='volume')
self.assertEqual(sc.url_for('tenantId', '1', service_type='volume'),
"https://volume1.host/v1/1234")
self.assertEqual(sc.url_for('tenantId', '2', service_type='volume'),
"https://volume1.host/v1/3456")
self.assertEqual("https://volume1.host/v1/1234",
sc.url_for('tenantId', '1', service_type='volume'))
self.assertEqual("https://volume1.host/v1/3456",
sc.url_for('tenantId', '2', service_type='volume'))
self.assertEqual(sc.url_for('tenantId', '2', service_type='volumev2'),
"https://volume1.host/v2/3456")
self.assertEqual(sc.url_for('tenantId', '2', service_type='volumev2'),
"https://volume1.host/v2/3456")
self.assertEqual("https://volume1.host/v2/3456",
sc.url_for('tenantId', '2', service_type='volumev2'))
self.assertEqual("https://volume1.host/v2/3456",
sc.url_for('tenantId', '2', service_type='volumev2'))
self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
"region", "North", service_type='volume')
@@ -269,7 +269,7 @@ class ServiceCatalogTest(utils.TestCase):
def test_compatibility_service_type(self):
sc = service_catalog.ServiceCatalog(SERVICE_COMPATIBILITY_CATALOG)
self.assertEqual(sc.url_for('tenantId', '1', service_type='volume'),
"https://volume1.host/v2/1234")
self.assertEqual(sc.url_for('tenantId', '2', service_type='volume'),
"https://volume1.host/v2/3456")
self.assertEqual("https://volume1.host/v2/1234",
sc.url_for('tenantId', '1', service_type='volume'))
self.assertEqual("https://volume1.host/v2/3456",
sc.url_for('tenantId', '2', service_type='volume'))

View File

@@ -11,16 +11,20 @@
# 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
import cinderclient.shell
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):
@@ -43,11 +47,11 @@ class ShellTest(utils.TestCase):
orig = sys.stdout
try:
sys.stdout = moves.StringIO()
_shell = cinderclient.shell.OpenStackCinderShell()
_shell = shell.OpenStackCinderShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertEqual(exc_value.code, 0)
self.assertEqual(0, exc_value.code)
finally:
out = sys.stdout.getvalue()
sys.stdout.close()
@@ -61,8 +65,8 @@ class ShellTest(utils.TestCase):
def test_help(self):
required = [
'.*?^usage: ',
'.*?(?m)^\s+create\s+Add a new volume.',
'.*?(?m)^See "cinder help COMMAND" for help on a specific command',
'.*?(?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:
@@ -72,9 +76,79 @@ class ShellTest(utils.TestCase):
def test_help_on_subcommand(self):
required = [
'.*?^usage: cinder list',
'.*?(?m)^List all the volumes.',
'.*?(?m)^Lists all volumes.',
]
help_text = self.shell('help list')
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
def register_keystone_auth_fixture(self, mocker, url):
mocker.register_uri('GET', url,
text=keystone_client.keystone_request_callback)
@requests_mock.Mocker()
def test_version_discovery(self, mocker):
_shell = shell.OpenStackCinderShell()
os_auth_url = "https://WrongDiscoveryResponse.discovery.com:35357/v2.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
self.assertRaises(DiscoveryFailure, _shell._discover_auth_versions,
None, auth_url=os_auth_url)
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v2.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
v2_url, v3_url = _shell._discover_auth_versions(
None, auth_url=os_auth_url)
self.assertEqual(v2_url, os_auth_url, "Expected v2 url")
self.assertEqual(v3_url, None, "Expected no v3 url")
os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v3.0"
self.register_keystone_auth_fixture(mocker, os_auth_url)
v2_url, v3_url = _shell._discover_auth_versions(
None, auth_url=os_auth_url)
self.assertEqual(v3_url, os_auth_url, "Expected v3 url")
self.assertEqual(v2_url, None, "Expected no v2 url")
class CinderClientArgumentParserTest(utils.TestCase):
def test_ambiguity_solved_for_one_visible_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest='visible_param',
action='store_true')
parser.add_argument('--test_parameter',
dest='hidden_param',
action='store_true',
help=argparse.SUPPRESS)
opts = parser.parse_args(['--test'])
# visible argument must be set
self.assertTrue(opts.visible_param)
self.assertFalse(opts.hidden_param)
def test_raise_ambiguity_error_two_visible_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest="visible_param1",
action='store_true')
parser.add_argument('--test_parameter',
dest="visible_param2",
action='store_true')
self.assertRaises(SystemExit, parser.parse_args, ['--test'])
def test_raise_ambiguity_error_two_hidden_argument(self):
parser = shell.CinderClientArgumentParser(add_help=False)
parser.add_argument('--test-parameter',
dest="hidden_param1",
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--test_parameter',
dest="hidden_param2",
action='store_true',
help=argparse.SUPPRESS)
self.assertRaises(SystemExit, parser.parse_args, ['--test'])

View File

@@ -73,23 +73,23 @@ class FindResourceTestCase(test_utils.TestCase):
def test_find_by_integer_id(self):
output = utils.find_resource(self.manager, 1234)
self.assertEqual(output, self.manager.get('1234'))
self.assertEqual(self.manager.get('1234'), output)
def test_find_by_str_id(self):
output = utils.find_resource(self.manager, '1234')
self.assertEqual(output, self.manager.get('1234'))
self.assertEqual(self.manager.get('1234'), output)
def test_find_by_uuid(self):
output = utils.find_resource(self.manager, UUID)
self.assertEqual(output, self.manager.get(UUID))
self.assertEqual(self.manager.get(UUID), output)
def test_find_by_str_name(self):
output = utils.find_resource(self.manager, 'entity_one')
self.assertEqual(output, self.manager.get('1234'))
self.assertEqual(self.manager.get('1234'), output)
def test_find_by_str_displayname(self):
output = utils.find_resource(self.manager, 'entity_three')
self.assertEqual(output, self.manager.get('4242'))
self.assertEqual(self.manager.get('4242'), output)
class CaptureStdout(object):
@@ -113,14 +113,14 @@ class PrintListTestCase(test_utils.TestCase):
to_print = [Row(a=1, b=2), Row(a=3, b=4)]
with CaptureStdout() as cso:
utils.print_list(to_print, ['a', 'b'])
self.assertEqual(cso.read(), """\
self.assertEqual("""\
+---+---+
| a | b |
+---+---+
| 1 | 2 |
| 3 | 4 |
+---+---+
""")
""", cso.read())
def test_print_list_with_generator(self):
Row = collections.namedtuple('Row', ['a', 'b'])
@@ -130,11 +130,11 @@ class PrintListTestCase(test_utils.TestCase):
yield row
with CaptureStdout() as cso:
utils.print_list(gen_rows(), ['a', 'b'])
self.assertEqual(cso.read(), """\
self.assertEqual("""\
+---+---+
| a | b |
+---+---+
| 1 | 2 |
| 3 | 4 |
+---+---+
""")
""", cso.read())

View File

@@ -11,10 +11,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import fixtures
import requests
from requests_mock.contrib import fixture as requests_mock_fixture
import six
import testtools
@@ -35,6 +38,42 @@ class TestCase(testtools.TestCase):
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class FixturedTestCase(TestCase):
client_fixture_class = None
data_fixture_class = None
def setUp(self):
super(FixturedTestCase, self).setUp()
self.requests = self.useFixture(requests_mock_fixture.Fixture())
self.data_fixture = None
self.client_fixture = None
self.cs = None
if self.client_fixture_class:
fix = self.client_fixture_class(self.requests)
self.client_fixture = self.useFixture(fix)
self.cs = self.client_fixture.new_client()
if self.data_fixture_class:
fix = self.data_fixture_class(self.requests)
self.data_fixture = self.useFixture(fix)
def assert_called(self, method, path, body=None):
self.assertEqual(self.requests.last_request.method, method)
self.assertEqual(self.requests.last_request.path_url, path)
if body:
req_data = self.requests.last_request.body
if isinstance(req_data, six.binary_type):
req_data = req_data.decode('utf-8')
if not isinstance(body, six.string_types):
# json load if the input body to match against is not a string
req_data = json.loads(req_data)
self.assertEqual(req_data, body)
class TestResponse(requests.Response):
"""Class used to wrap requests.Response and provide some
convenience to initialize with a dict.
@@ -46,6 +85,7 @@ class TestResponse(requests.Response):
if isinstance(data, dict):
self.status_code = data.get('status_code', None)
self.headers = data.get('headers', None)
self.reason = data.get('reason', '')
# Fake the text attribute to streamline Response creation
self._text = data.get('text', None)
else:

View File

@@ -285,6 +285,9 @@ class FakeHTTPClient(base_client.HTTPClient):
def delete_snapshots_1234(self, **kw):
return (202, {}, {})
def delete_snapshots_5678(self, **kw):
return (202, {}, {})
#
# Volumes
#
@@ -354,6 +357,8 @@ class FakeHTTPClient(base_client.HTTPClient):
assert 'force_host_copy' in body[action]
elif action == 'os-update_readonly_flag':
assert list(body[action]) == ['readonly']
elif action == 'os-set_bootable':
assert list(body[action]) == ['bootable']
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
@@ -401,6 +406,12 @@ class FakeHTTPClient(base_client.HTTPClient):
'snapshots': 2,
'gigabytes': 1}})
def delete_os_quota_sets_1234(self, **kw):
return (200, {}, {})
def delete_os_quota_sets_test(self, **kw):
return (200, {}, {})
#
# Quota Classes
#
@@ -467,13 +478,13 @@ class FakeHTTPClient(base_client.HTTPClient):
def get_types_1_encryption(self, **kw):
return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test',
'cipher': 'test', 'key_size': 1,
'control_location': 'front'})
'control_location': 'front-end'})
def get_types_2_encryption(self, **kw):
return (200, {}, {})
def post_types_2_encryption(self, body, **kw):
return (200, {}, {'encryption': {}})
return (200, {}, {'encryption': body})
def put_types_1_encryption_1(self, body, **kw):
return (200, {}, {})
@@ -708,6 +719,11 @@ class FakeHTTPClient(base_client.HTTPClient):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled'})
def put_os_services_disable_log_reason(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled',
'disabled_reason': body['disabled_reason']})
def get_os_availability_zone(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [

View File

@@ -28,7 +28,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp = {
"access": {
"token": {
"expires": "12345",
"expires": "2014-11-01T03:32:15-05:00",
"id": "FAKE_ID",
},
"serviceCatalog": [
@@ -82,9 +82,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/')
self.assertEqual(cs.client.management_url, public_url)
self.assertEqual(public_url, cs.client.management_url)
token_id = resp["access"]["token"]["id"]
self.assertEqual(cs.client.auth_token, token_id)
self.assertEqual(token_id, cs.client.auth_token)
test_auth_call()
@@ -95,7 +95,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp = {
"access": {
"token": {
"expires": "12345",
"expires": "2014-11-01T03:32:15-05:00",
"id": "FAKE_ID",
"tenant": {
"description": None,
@@ -155,11 +155,11 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/')
self.assertEqual(cs.client.management_url, public_url)
self.assertEqual(public_url, cs.client.management_url)
token_id = resp["access"]["token"]["id"]
self.assertEqual(cs.client.auth_token, token_id)
self.assertEqual(token_id, cs.client.auth_token)
tenant_id = resp["access"]["token"]["tenant"]["id"]
self.assertEqual(cs.client.tenant_id, tenant_id)
self.assertEqual(tenant_id, cs.client.tenant_id)
test_auth_call()
@@ -186,7 +186,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
dict_correct_response = {
"access": {
"token": {
"expires": "12345",
"expires": "2014-11-01T03:32:15-05:00",
"id": "FAKE_ID",
},
"serviceCatalog": [
@@ -258,59 +258,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp = dict_correct_response
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/')
self.assertEqual(cs.client.management_url, public_url)
self.assertEqual(public_url, cs.client.management_url)
token_id = resp["access"]["token"]["id"]
self.assertEqual(cs.client.auth_token, token_id)
test_auth_call()
def test_ambiguous_endpoints(self):
cs = client.Client("username", "password", "project_id",
"http://localhost:8776/v1", service_type='volume')
resp = {
"access": {
"token": {
"expires": "12345",
"id": "FAKE_ID",
},
"serviceCatalog": [
{
"adminURL": "http://localhost:8776/v1",
"type": "volume",
"name": "Cinder Volume Service",
"endpoints": [
{
"region": "RegionOne",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
{
"adminURL": "http://localhost:8776/v1",
"type": "volume",
"name": "Cinder Volume Cloud Service",
"endpoints": [
{
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
],
},
}
auth_response = utils.TestResponse({
"status_code": 200,
"text": json.dumps(resp),
})
mock_request = mock.Mock(return_value=(auth_response))
@mock.patch.object(requests, "request", mock_request)
def test_auth_call():
self.assertRaises(exceptions.AmbiguousEndpoints,
cs.client.authenticate)
self.assertEqual(token_id, cs.client.auth_token)
test_auth_call()
@@ -344,10 +294,10 @@ class AuthenticationTests(utils.TestCase):
headers=headers,
**self.TEST_REQUEST_BASE)
self.assertEqual(cs.client.management_url,
auth_response.headers['x-server-management-url'])
self.assertEqual(cs.client.auth_token,
auth_response.headers['x-auth-token'])
self.assertEqual(auth_response.headers['x-server-management-url'],
cs.client.management_url)
self.assertEqual(auth_response.headers['x-auth-token'],
cs.client.auth_token)
test_auth_call()

View File

@@ -18,26 +18,27 @@ 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.v1 import fakes
cs = fakes.FakeClient()
class AvailabilityZoneTest(utils.FixturedTestCase):
class AvailabilityZoneTest(utils.TestCase):
client_fixture_class = client.V1
data_fixture_class = azfixture.Fixture
def _assertZone(self, zone, name, status):
self.assertEqual(zone.zoneName, name)
self.assertEqual(zone.zoneState, status)
self.assertEqual(name, zone.zoneName)
self.assertEqual(status, zone.zoneState)
def test_list_availability_zone(self):
zones = cs.availability_zones.list(detailed=False)
cs.assert_called('GET', '/os-availability-zone')
zones = self.cs.availability_zones.list(detailed=False)
self.assert_called('GET', '/os-availability-zone')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertIsInstance(zone,
availability_zones.AvailabilityZone)
self.assertEqual(2, len(zones))
@@ -47,18 +48,18 @@ class AvailabilityZoneTest(utils.TestCase):
z0 = shell._treeizeAvailabilityZone(zones[0])
z1 = shell._treeizeAvailabilityZone(zones[1])
self.assertEqual((len(z0), len(z1)), (1, 1))
self.assertEqual((1, 1), (len(z0), len(z1)))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z1[0], l1[0], l1[1])
def test_detail_availability_zone(self):
zones = cs.availability_zones.list(detailed=True)
cs.assert_called('GET', '/os-availability-zone/detail')
zones = self.cs.availability_zones.list(detailed=True)
self.assert_called('GET', '/os-availability-zone/detail')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertIsInstance(zone,
availability_zones.AvailabilityZone)
self.assertEqual(3, len(zones))
@@ -76,7 +77,7 @@ class AvailabilityZoneTest(utils.TestCase):
z1 = shell._treeizeAvailabilityZone(zones[1])
z2 = shell._treeizeAvailabilityZone(zones[2])
self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1))
self.assertEqual((3, 3, 1), (len(z0), len(z1), len(z2)))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z0[1], l1[0], l1[1])

View File

@@ -77,49 +77,49 @@ class TestLimits(utils.TestCase):
l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2",
"unit2", "next2")
for item in l.rate:
self.assertTrue(item in [l1, l2])
self.assertIn(item, [l1, l2])
class TestRateLimit(utils.TestCase):
def test_equal(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit()
self.assertTrue(l1 == l2)
self.assertEqual(l1, l2)
def test_not_equal_verbs(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(verb="verb2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_uris(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(uri="uri2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_regexps(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(regex="regex2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_values(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(value="value2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_remains(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(remain="remain2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_units(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(unit="unit2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_next_available(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(next_available="next2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_repr(self):
l1 = _get_default_RateLimit()
@@ -130,17 +130,17 @@ class TestAbsoluteLimit(utils.TestCase):
def test_equal(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value1")
self.assertTrue(l1 == l2)
self.assertEqual(l1, l2)
def test_not_equal_values(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_names(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name2", "value1")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_repr(self):
l1 = limits.AbsoluteLimit("name1", "value1")
@@ -161,4 +161,4 @@ class TestLimitsManager(utils.TestCase):
self.assertIsInstance(lim, limits.Limits)
for l in lim.absolute:
self.assertEqual(l, l1)
self.assertEqual(l1, l)

View File

@@ -50,3 +50,8 @@ class QuotaSetsTest(utils.TestCase):
q2.get()
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
def test_delete_quota(self):
tenant_id = 'test'
cs.quotas.delete(tenant_id)
cs.assert_called('DELETE', '/os-quota-sets/test')

View File

@@ -26,41 +26,50 @@ class ServicesTest(utils.TestCase):
def test_list_services(self):
svs = cs.services.list()
cs.assert_called('GET', '/os-services')
self.assertEqual(len(svs), 3)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
self.assertEqual(3, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
def test_list_services_with_hostname(self):
svs = cs.services.list(host='host2')
cs.assert_called('GET', '/os-services?host=host2')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
self.assertEqual(2, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
[self.assertEqual('host2', s.host) for s in svs]
def test_list_services_with_binary(self):
svs = cs.services.list(binary='cinder-volume')
cs.assert_called('GET', '/os-services?binary=cinder-volume')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
self.assertEqual(2, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
[self.assertEqual('cinder-volume', s.binary) for s in svs]
def test_list_services_with_host_binary(self):
svs = cs.services.list('host2', 'cinder-volume')
cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume')
self.assertEqual(len(svs), 1)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
self.assertEqual(1, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
[self.assertEqual('host2', s.host) for s in svs]
[self.assertEqual('cinder-volume', s.binary) for s in svs]
def test_services_enable(self):
s = cs.services.enable('host1', 'cinder-volume')
values = {"host": "host1", 'binary': 'cinder-volume'}
cs.assert_called('PUT', '/os-services/enable', values)
self.assertTrue(isinstance(s, services.Service))
self.assertEqual(s.status, 'enabled')
self.assertIsInstance(s, services.Service)
self.assertEqual('enabled', s.status)
def test_services_disable(self):
s = cs.services.disable('host1', 'cinder-volume')
values = {"host": "host1", 'binary': 'cinder-volume'}
cs.assert_called('PUT', '/os-services/disable', values)
self.assertTrue(isinstance(s, services.Service))
self.assertEqual(s.status, 'disabled')
self.assertIsInstance(s, services.Service)
self.assertEqual('disabled', s.status)
def test_services_disable_log_reason(self):
s = cs.services.disable_log_reason(
'host1', 'cinder-volume', 'disable bad host')
values = {"host": "host1", 'binary': 'cinder-volume',
"disabled_reason": "disable bad host"}
cs.assert_called('PUT', '/os-services/disable-log-reason', values)
self.assertIsInstance(s, services.Service)
self.assertEqual('disabled', s.status)

View File

@@ -16,12 +16,15 @@
# under the License.
import fixtures
from requests_mock.contrib import fixture as requests_mock_fixture
from cinderclient import client
from cinderclient import exceptions
from cinderclient import shell
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):
@@ -31,7 +34,7 @@ class ShellTest(utils.TestCase):
'CINDER_PASSWORD': 'password',
'CINDER_PROJECT_ID': 'project_id',
'OS_VOLUME_API_VERSION': '1',
'CINDER_URL': 'http://no.where',
'CINDER_URL': keystone_client.BASE_URL,
}
# Patch os.environ to avoid required auth info.
@@ -44,10 +47,15 @@ class ShellTest(utils.TestCase):
self.shell = shell.OpenStackCinderShell()
#HACK(bcwaldon): replace this when we start using stubs
# HACK(bcwaldon): replace this when we start using stubs
self.old_get_client_class = client.get_client_class
client.get_client_class = lambda *_: fakes.FakeClient
self.requests = self.useFixture(requests_mock_fixture.Fixture())
self.requests.register_uri(
'GET', keystone_client.BASE_URL,
text=keystone_client.keystone_request_callback)
def tearDown(self):
# For some method like test_image_meta_bad_action we are
# testing a SystemExit to be thrown and object self.shell has
@@ -56,7 +64,7 @@ class ShellTest(utils.TestCase):
if hasattr(self.shell, 'cs'):
self.shell.cs.clear_callstack()
#HACK(bcwaldon): replace this when we start using stubs
# HACK(bcwaldon): replace this when we start using stubs
client.get_client_class = self.old_get_client_class
super(ShellTest, self).tearDown()
@@ -72,6 +80,7 @@ class ShellTest(utils.TestCase):
def test_extract_metadata(self):
# mimic the result of argparse's parse_args() method
class Arguments:
def __init__(self, metadata=[]):
self.metadata = metadata
@@ -86,7 +95,17 @@ class ShellTest(utils.TestCase):
for input in inputs:
args = Arguments(metadata=input[0])
self.assertEqual(shell_v1._extract_metadata(args), input[1])
self.assertEqual(input[1], shell_v1._extract_metadata(args))
def test_translate_volume_keys(self):
cs = fakes.FakeClient()
v = cs.volumes.list()[0]
setattr(v, 'os-vol-tenant-attr:tenant_id', 'fake_tenant')
setattr(v, '_info', {'attachments': [{'server_id': 1234}],
'id': 1234, 'name': 'sample-volume',
'os-vol-tenant-attr:tenant_id': 'fake_tenant'})
shell_v1._translate_volume_keys([v])
self.assertEqual(v.tenant_id, 'fake_tenant')
def test_list(self):
self.run_command('list')
@@ -220,6 +239,17 @@ class ShellTest(utils.TestCase):
self.assert_called_anytime('POST', '/volumes/5678/action',
body=expected)
def test_reset_state_two_with_one_nonexistent(self):
cmd = 'reset-state 1234 123456789'
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called_anytime('POST', '/volumes/1234/action',
body=expected)
def test_reset_state_one_with_one_nonexistent(self):
cmd = 'reset-state 123456789'
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
def test_snapshot_reset_state(self):
self.run_command('snapshot-reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
@@ -273,7 +303,7 @@ class ShellTest(utils.TestCase):
"""
expected = {'encryption': {'cipher': None, 'key_size': None,
'provider': 'TestProvider',
'control_location': None}}
'control_location': 'front-end'}}
self.run_command('encryption-type-create 2 TestProvider')
self.assert_called('POST', '/types/2/encryption', body=expected)
self.assert_called_anytime('GET', '/types/2')
@@ -347,7 +377,14 @@ class ShellTest(utils.TestCase):
self.assert_called('PUT', '/os-services/disable',
{"binary": "cinder-volume", "host": "host"})
def test_service_disable(self):
def test_services_disable_with_reason(self):
cmd = 'service-disable host cinder-volume --reason no_reason'
self.run_command(cmd)
body = {'host': 'host', 'binary': 'cinder-volume',
'disabled_reason': 'no_reason'}
self.assert_called('PUT', '/os-services/disable-log-reason', body)
def test_service_enable(self):
self.run_command('service-enable host cinder-volume')
self.assert_called('PUT', '/os-services/enable',
{"binary": "cinder-volume", "host": "host"})
@@ -355,3 +392,11 @@ class ShellTest(utils.TestCase):
def test_snapshot_delete(self):
self.run_command('snapshot-delete 1234')
self.assert_called('DELETE', '/snapshots/1234')
def test_quota_delete(self):
self.run_command('quota-delete 1234')
self.assert_called('DELETE', '/os-quota-sets/1234')
def test_snapshot_delete_multiple(self):
self.run_command('snapshot-delete 1234 5678')
self.assert_called('DELETE', '/snapshots/5678')

View File

@@ -14,22 +14,23 @@
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests.fixture_data import client
from cinderclient.tests.fixture_data import snapshots
cs = fakes.FakeClient()
class SnapshotActionsTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = snapshots.Fixture
class SnapshotActionsTest(utils.TestCase):
def test_update_snapshot_status(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available'})
cs.assert_called('POST', '/snapshots/1234/action')
s = self.cs.volume_snapshots.get('1234')
stat = {'status': 'available'}
self.cs.volume_snapshots.update_snapshot_status(s, stat)
self.assert_called('POST', '/snapshots/1234/action')
def test_update_snapshot_status_with_progress(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available',
'progress': '73%'})
cs.assert_called('POST', '/snapshots/1234/action')
s = self.cs.volume_snapshots.get('1234')
stat = {'status': 'available', 'progress': '73%'}
self.cs.volume_snapshots.update_snapshot_status(s, stat)
self.assert_called('POST', '/snapshots/1234/action')

View File

@@ -23,12 +23,12 @@ class TypesTest(utils.TestCase):
tl = cs.volume_types.list()
cs.assert_called('GET', '/types')
for t in tl:
self.assertTrue(isinstance(t, volume_types.VolumeType))
self.assertIsInstance(t, volume_types.VolumeType)
def test_create(self):
t = cs.volume_types.create('test-type-3')
cs.assert_called('POST', '/types')
self.assertTrue(isinstance(t, volume_types.VolumeType))
self.assertIsInstance(t, volume_types.VolumeType)
def test_set_key(self):
t = cs.volume_types.get(1)

View File

@@ -73,12 +73,11 @@ class VolumeEncryptionTypesTest(utils.TestCase):
Verify that one POST request is made for the encryption type creation.
Verify that encryption type creation returns a VolumeEncryptionType.
"""
result = cs.volume_encryption_types.create(2, {'encryption':
{'provider': 'Test',
'key_size': None,
'cipher': None,
'control_location':
None}})
result = cs.volume_encryption_types.create(2, {'provider': 'Test',
'key_size': None,
'cipher': None,
'control_location':
None})
cs.assert_called('POST', '/types/2/encryption')
self.assertIsInstance(result, VolumeEncryptionType)

View File

@@ -106,3 +106,8 @@ class VolumesTest(utils.TestCase):
v = cs.volumes.get('1234')
cs.volumes.update_readonly_flag(v, True)
cs.assert_called('POST', '/volumes/1234/action')
def test_set_bootable(self):
v = cs.volumes.get('1234')
cs.volumes.set_bootable(v, True)
cs.assert_called('POST', '/volumes/1234/action')

View File

@@ -69,6 +69,32 @@ def _stub_snapshot(**kwargs):
return snapshot
def _stub_consistencygroup(**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",
}
consistencygroup.update(kwargs)
return consistencygroup
def _stub_cgsnapshot(**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",
}
cgsnapshot.update(kwargs)
return cgsnapshot
def _self_href(base_uri, tenant_id, backup_id):
return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id)
@@ -294,6 +320,9 @@ class FakeHTTPClient(base_client.HTTPClient):
def delete_snapshots_1234(self, **kw):
return (202, {}, {})
def delete_snapshots_5678(self, **kw):
return (202, {}, {})
#
# Volumes
#
@@ -365,6 +394,14 @@ class FakeHTTPClient(base_client.HTTPClient):
assert list(body[action]) == ['readonly']
elif action == 'os-retype':
assert 'new_type' in body[action]
elif action == 'os-set_bootable':
assert list(body[action]) == ['bootable']
elif action == 'os-unmanage':
assert body[action] is None
elif action == 'os-promote-replica':
assert body[action] is None
elif action == 'os-reenable-replica':
assert body[action] is None
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
@@ -373,7 +410,9 @@ class FakeHTTPClient(base_client.HTTPClient):
return self.post_volumes_1234_action(body, **kw)
def post_volumes(self, **kw):
return (202, {}, {'volume': {}})
size = kw['body']['volume'].get('size', 1)
volume = _stub_volume(id='1234', size=size)
return (202, {}, {'volume': volume})
def delete_volumes_1234(self, **kw):
return (202, {}, None)
@@ -381,6 +420,43 @@ class FakeHTTPClient(base_client.HTTPClient):
def delete_volumes_5678(self, **kw):
return (202, {}, None)
#
# Consistencygroups
#
def get_consistencygroups_detail(self, **kw):
return (200, {}, {"consistencygroups": [
_stub_consistencygroup(id='1234'),
_stub_consistencygroup(id='4567')]})
def get_consistencygroups_1234(self, **kw):
return (200, {}, {'consistencygroup':
_stub_consistencygroup(id='1234')})
def post_consistencygroups(self, **kw):
return (202, {}, {'consistencygroup': {}})
def post_consistencygroups_1234_delete(self, **kw):
return (202, {}, {})
#
# Cgsnapshots
#
def get_cgsnapshots_detail(self, **kw):
return (200, {}, {"cgsnapshots": [
_stub_cgsnapshot(id='1234'),
_stub_cgsnapshot(id='4567')]})
def get_cgsnapshots_1234(self, **kw):
return (200, {}, {'cgsnapshot': _stub_cgsnapshot(id='1234')})
def post_cgsnapshots(self, **kw):
return (202, {}, {'cgsnapshot': {}})
def delete_cgsnapshots_1234(self, **kw):
return (202, {}, {})
#
# Quotas
#
@@ -412,6 +488,12 @@ class FakeHTTPClient(base_client.HTTPClient):
'snapshots': 2,
'gigabytes': 1}})
def delete_os_quota_sets_1234(self, **kw):
return (200, {}, {})
def delete_os_quota_sets_test(self, **kw):
return (200, {}, {})
#
# Quota Classes
#
@@ -478,13 +560,13 @@ class FakeHTTPClient(base_client.HTTPClient):
def get_types_1_encryption(self, **kw):
return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test',
'cipher': 'test', 'key_size': 1,
'control_location': 'front'})
'control_location': 'front-end'})
def get_types_2_encryption(self, **kw):
return (200, {}, {})
def post_types_2_encryption(self, body, **kw):
return (200, {}, {'encryption': {}})
return (200, {}, {'encryption': body})
def put_types_1_encryption_1(self, body, **kw):
return (200, {}, {})
@@ -572,6 +654,27 @@ class FakeHTTPClient(base_client.HTTPClient):
return (200, {},
{'restore': _stub_restore()})
def get_backups_76a17945_3c6f_435c_975b_b5685db10b62_export_record(self,
**kw):
return (200,
{},
{'backup-record': {'backup_service': 'fake-backup-service',
'backup_url': 'fake-backup-url'}})
def get_backups_1234_export_record(self, **kw):
return (200,
{},
{'backup-record': {'backup_service': 'fake-backup-service',
'backup_url': 'fake-backup-url'}})
def post_backups_import_record(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
return (200,
{},
{'backup': _stub_backup(backup1, base_uri, tenant_id)})
#
# QoSSpecs
#
@@ -720,6 +823,11 @@ class FakeHTTPClient(base_client.HTTPClient):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled'})
def put_os_services_disable_log_reason(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled',
'disabled_reason': body['disabled_reason']})
def get_os_availability_zone(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
@@ -789,3 +897,14 @@ class FakeHTTPClient(base_client.HTTPClient):
def put_snapshots_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
def post_os_volume_manage(self, **kw):
volume = _stub_volume(id='1234')
volume.update(kw['body']['volume'])
return (202, {}, {'volume': volume})
def post_os_promote_replica_1234(self, **kw):
return (202, {}, {})
def post_os_reenable_replica_1234(self, **kw):
return (202, {}, {})

View File

@@ -31,7 +31,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp = {
"access": {
"token": {
"expires": "12345",
"expires": "2014-11-01T03:32:15-05:00",
"id": "FAKE_ID",
},
"serviceCatalog": [
@@ -85,9 +85,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/')
self.assertEqual(cs.client.management_url, public_url)
self.assertEqual(public_url, cs.client.management_url)
token_id = resp["access"]["token"]["id"]
self.assertEqual(cs.client.auth_token, token_id)
self.assertEqual(token_id, cs.client.auth_token)
test_auth_call()
@@ -98,7 +98,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp = {
"access": {
"token": {
"expires": "12345",
"expires": "2014-11-01T03:32:15-05:00",
"id": "FAKE_ID",
"tenant": {
"description": None,
@@ -158,11 +158,11 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/')
self.assertEqual(cs.client.management_url, public_url)
self.assertEqual(public_url, cs.client.management_url)
token_id = resp["access"]["token"]["id"]
self.assertEqual(cs.client.auth_token, token_id)
self.assertEqual(token_id, cs.client.auth_token)
tenant_id = resp["access"]["token"]["tenant"]["id"]
self.assertEqual(cs.client.tenant_id, tenant_id)
self.assertEqual(tenant_id, cs.client.tenant_id)
test_auth_call()
@@ -189,7 +189,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
dict_correct_response = {
"access": {
"token": {
"expires": "12345",
"expires": "2014-11-01T03:32:15-05:00",
"id": "FAKE_ID",
},
"serviceCatalog": [
@@ -261,59 +261,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp = dict_correct_response
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/')
self.assertEqual(cs.client.management_url, public_url)
self.assertEqual(public_url, cs.client.management_url)
token_id = resp["access"]["token"]["id"]
self.assertEqual(cs.client.auth_token, token_id)
test_auth_call()
def test_ambiguous_endpoints(self):
cs = client.Client("username", "password", "project_id",
"http://localhost:8776/v2", service_type='volumev2')
resp = {
"access": {
"token": {
"expires": "12345",
"id": "FAKE_ID",
},
"serviceCatalog": [
{
"adminURL": "http://localhost:8776/v1",
"type": "volumev2",
"name": "Cinder Volume Service",
"endpoints": [
{
"region": "RegionOne",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
{
"adminURL": "http://localhost:8776/v2",
"type": "volumev2",
"name": "Cinder Volume V2",
"endpoints": [
{
"internalURL": "http://localhost:8776/v2",
"publicURL": "http://localhost:8776/v2",
},
],
},
],
},
}
auth_response = utils.TestResponse({
"status_code": 200,
"text": json.dumps(resp),
})
mock_request = mock.Mock(return_value=(auth_response))
@mock.patch.object(requests, "request", mock_request)
def test_auth_call():
self.assertRaises(exceptions.AmbiguousEndpoints,
cs.client.authenticate)
self.assertEqual(token_id, cs.client.auth_token)
test_auth_call()
@@ -347,10 +297,10 @@ class AuthenticationTests(utils.TestCase):
headers=headers,
**self.TEST_REQUEST_BASE)
self.assertEqual(cs.client.management_url,
auth_response.headers['x-server-management-url'])
self.assertEqual(cs.client.auth_token,
auth_response.headers['x-auth-token'])
self.assertEqual(auth_response.headers['x-server-management-url'],
cs.client.management_url)
self.assertEqual(auth_response.headers['x-auth-token'],
cs.client.auth_token)
test_auth_call()

View File

@@ -16,28 +16,29 @@
import six
from cinderclient.v1 import availability_zones
from cinderclient.v1 import shell
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.v1 import fakes
cs = fakes.FakeClient()
class AvailabilityZoneTest(utils.FixturedTestCase):
class AvailabilityZoneTest(utils.TestCase):
client_fixture_class = client.V2
data_fixture_class = azfixture.Fixture
def _assertZone(self, zone, name, status):
self.assertEqual(zone.zoneName, name)
self.assertEqual(zone.zoneState, status)
self.assertEqual(name, zone.zoneName)
self.assertEqual(status, zone.zoneState)
def test_list_availability_zone(self):
zones = cs.availability_zones.list(detailed=False)
cs.assert_called('GET', '/os-availability-zone')
zones = self.cs.availability_zones.list(detailed=False)
self.assert_called('GET', '/os-availability-zone')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertIsInstance(zone,
availability_zones.AvailabilityZone)
self.assertEqual(2, len(zones))
@@ -47,18 +48,18 @@ class AvailabilityZoneTest(utils.TestCase):
z0 = shell._treeizeAvailabilityZone(zones[0])
z1 = shell._treeizeAvailabilityZone(zones[1])
self.assertEqual((len(z0), len(z1)), (1, 1))
self.assertEqual((1, 1), (len(z0), len(z1)))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z1[0], l1[0], l1[1])
def test_detail_availability_zone(self):
zones = cs.availability_zones.list(detailed=True)
cs.assert_called('GET', '/os-availability-zone/detail')
zones = self.cs.availability_zones.list(detailed=True)
self.assert_called('GET', '/os-availability-zone/detail')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertIsInstance(zone,
availability_zones.AvailabilityZone)
self.assertEqual(3, len(zones))
@@ -76,7 +77,7 @@ class AvailabilityZoneTest(utils.TestCase):
z1 = shell._treeizeAvailabilityZone(zones[1])
z2 = shell._treeizeAvailabilityZone(zones[2])
self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1))
self.assertEqual((3, 3, 1), (len(z0), len(z1), len(z2)))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z0[1], l1[0], l1[1])

View File

@@ -0,0 +1,56 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class cgsnapshotsTest(utils.TestCase):
def test_delete_cgsnapshot(self):
v = cs.cgsnapshots.list()[0]
v.delete()
cs.assert_called('DELETE', '/cgsnapshots/1234')
cs.cgsnapshots.delete('1234')
cs.assert_called('DELETE', '/cgsnapshots/1234')
cs.cgsnapshots.delete(v)
cs.assert_called('DELETE', '/cgsnapshots/1234')
def test_create_cgsnapshot(self):
cs.cgsnapshots.create('cgsnap')
cs.assert_called('POST', '/cgsnapshots')
def test_create_cgsnapshot_with_cg_id(self):
cs.cgsnapshots.create('1234')
expected = {'cgsnapshot': {'status': 'creating',
'description': None,
'user_id': None,
'name': None,
'consistencygroup_id': '1234',
'project_id': None}}
cs.assert_called('POST', '/cgsnapshots', body=expected)
def test_list_cgsnapshot(self):
cs.cgsnapshots.list()
cs.assert_called('GET', '/cgsnapshots/detail')
def test_get_cgsnapshot(self):
cgsnapshot_id = '1234'
cs.cgsnapshots.get(cgsnapshot_id)
cs.assert_called('GET', '/cgsnapshots/%s' % cgsnapshot_id)

View File

@@ -0,0 +1,52 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class ConsistencygroupsTest(utils.TestCase):
def test_delete_consistencygroup(self):
v = cs.consistencygroups.list()[0]
v.delete(force='True')
cs.assert_called('POST', '/consistencygroups/1234/delete')
cs.consistencygroups.delete('1234', force=True)
cs.assert_called('POST', '/consistencygroups/1234/delete')
cs.consistencygroups.delete(v, force=True)
cs.assert_called('POST', '/consistencygroups/1234/delete')
def test_create_consistencygroup(self):
cs.consistencygroups.create('type1,type2', 'cg')
cs.assert_called('POST', '/consistencygroups')
def test_create_consistencygroup_with_volume_types(self):
cs.consistencygroups.create('type1,type2', 'cg')
expected = {'consistencygroup': {'status': 'creating',
'description': None,
'availability_zone': None,
'user_id': None,
'name': 'cg',
'volume_types': 'type1,type2',
'project_id': None}}
cs.assert_called('POST', '/consistencygroups', body=expected)
def test_list_consistencygroup(self):
cs.consistencygroups.list()
cs.assert_called('GET', '/consistencygroups/detail')

View File

@@ -77,49 +77,49 @@ class TestLimits(utils.TestCase):
l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2",
"unit2", "next2")
for item in l.rate:
self.assertTrue(item in [l1, l2])
self.assertIn(item, [l1, l2])
class TestRateLimit(utils.TestCase):
def test_equal(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit()
self.assertTrue(l1 == l2)
self.assertEqual(l1, l2)
def test_not_equal_verbs(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(verb="verb2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_uris(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(uri="uri2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_regexps(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(regex="regex2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_values(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(value="value2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_remains(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(remain="remain2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_units(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(unit="unit2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_next_available(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(next_available="next2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_repr(self):
l1 = _get_default_RateLimit()
@@ -130,17 +130,17 @@ class TestAbsoluteLimit(utils.TestCase):
def test_equal(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value1")
self.assertTrue(l1 == l2)
self.assertEqual(l1, l2)
def test_not_equal_values(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value2")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_not_equal_names(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name2", "value1")
self.assertFalse(l1 == l2)
self.assertNotEqual(l1, l2)
def test_repr(self):
l1 = limits.AbsoluteLimit("name1", "value1")
@@ -161,4 +161,4 @@ class TestLimitsManager(utils.TestCase):
self.assertIsInstance(lim, limits.Limits)
for l in lim.absolute:
self.assertEqual(l, l1)
self.assertEqual(l1, l)

View File

@@ -50,3 +50,8 @@ class QuotaSetsTest(utils.TestCase):
q2.get()
self.assertEqual(q.volumes, q2.volumes)
self.assertEqual(q.snapshots, q2.snapshots)
def test_delete_quota(self):
tenant_id = 'test'
cs.quotas.delete(tenant_id)
cs.assert_called('DELETE', '/os-quota-sets/test')

View File

@@ -26,41 +26,50 @@ class ServicesTest(utils.TestCase):
def test_list_services(self):
svs = cs.services.list()
cs.assert_called('GET', '/os-services')
self.assertEqual(len(svs), 3)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
self.assertEqual(3, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
def test_list_services_with_hostname(self):
svs = cs.services.list(host='host2')
cs.assert_called('GET', '/os-services?host=host2')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
self.assertEqual(2, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
[self.assertEqual('host2', s.host) for s in svs]
def test_list_services_with_binary(self):
svs = cs.services.list(binary='cinder-volume')
cs.assert_called('GET', '/os-services?binary=cinder-volume')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
self.assertEqual(2, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
[self.assertEqual('cinder-volume', s.binary) for s in svs]
def test_list_services_with_host_binary(self):
svs = cs.services.list('host2', 'cinder-volume')
cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume')
self.assertEqual(len(svs), 1)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
self.assertEqual(1, len(svs))
[self.assertIsInstance(s, services.Service) for s in svs]
[self.assertEqual('host2', s.host) for s in svs]
[self.assertEqual('cinder-volume', s.binary) for s in svs]
def test_services_enable(self):
s = cs.services.enable('host1', 'cinder-volume')
values = {"host": "host1", 'binary': 'cinder-volume'}
cs.assert_called('PUT', '/os-services/enable', values)
self.assertTrue(isinstance(s, services.Service))
self.assertEqual(s.status, 'enabled')
self.assertIsInstance(s, services.Service)
self.assertEqual('enabled', s.status)
def test_services_disable(self):
s = cs.services.disable('host1', 'cinder-volume')
values = {"host": "host1", 'binary': 'cinder-volume'}
cs.assert_called('PUT', '/os-services/disable', values)
self.assertTrue(isinstance(s, services.Service))
self.assertEqual(s.status, 'disabled')
self.assertIsInstance(s, services.Service)
self.assertEqual('disabled', s.status)
def test_services_disable_log_reason(self):
s = cs.services.disable_log_reason(
'host1', 'cinder-volume', 'disable bad host')
values = {"host": "host1", 'binary': 'cinder-volume',
"disabled_reason": "disable bad host"}
cs.assert_called('PUT', '/os-services/disable-log-reason', values)
self.assertIsInstance(s, services.Service)
self.assertEqual('disabled', s.status)

View File

@@ -14,11 +14,14 @@
# under the License.
import fixtures
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 import utils
from cinderclient.tests.v2 import fakes
from cinderclient.tests.fixture_data import keystone_client
class ShellTest(utils.TestCase):
@@ -28,7 +31,7 @@ class ShellTest(utils.TestCase):
'CINDER_PASSWORD': 'password',
'CINDER_PROJECT_ID': 'project_id',
'OS_VOLUME_API_VERSION': '2',
'CINDER_URL': 'http://no.where',
'CINDER_URL': keystone_client.BASE_URL,
}
# Patch os.environ to avoid required auth info.
@@ -41,10 +44,15 @@ class ShellTest(utils.TestCase):
self.shell = shell.OpenStackCinderShell()
#HACK(bcwaldon): replace this when we start using stubs
# HACK(bcwaldon): replace this when we start using stubs
self.old_get_client_class = client.get_client_class
client.get_client_class = lambda *_: fakes.FakeClient
self.requests = self.useFixture(requests_mock_fixture.Fixture())
self.requests.register_uri(
'GET', keystone_client.BASE_URL,
text=keystone_client.keystone_request_callback)
def tearDown(self):
# For some method like test_image_meta_bad_action we are
# testing a SystemExit to be thrown and object self.shell has
@@ -53,18 +61,22 @@ class ShellTest(utils.TestCase):
if hasattr(self.shell, 'cs'):
self.shell.cs.clear_callstack()
#HACK(bcwaldon): replace this when we start using stubs
# HACK(bcwaldon): replace this when we start using stubs
client.get_client_class = self.old_get_client_class
super(ShellTest, self).tearDown()
def run_command(self, cmd):
self.shell.main(cmd.split())
def assert_called(self, method, url, body=None, **kwargs):
return self.shell.cs.assert_called(method, url, body, **kwargs)
def assert_called(self, method, url, body=None,
partial_body=None, **kwargs):
return self.shell.cs.assert_called(method, url, body,
partial_body, **kwargs)
def assert_called_anytime(self, method, url, body=None):
return self.shell.cs.assert_called_anytime(method, url, body)
def assert_called_anytime(self, method, url, body=None,
partial_body=None):
return self.shell.cs.assert_called_anytime(method, url, body,
partial_body)
def test_list(self):
self.run_command('list')
@@ -83,10 +95,59 @@ class ShellTest(utils.TestCase):
self.run_command('list --all-tenants=1')
self.assert_called('GET', '/volumes/detail?all_tenants=1')
def test_list_marker(self):
self.run_command('list --marker=1234')
self.assert_called('GET', '/volumes/detail?marker=1234')
def test_list_limit(self):
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_availability_zone(self):
self.run_command('availability-zone-list')
self.assert_called('GET', '/os-availability-zone')
def test_create_volume_from_snapshot(self):
expected = {'volume': {'size': None}}
expected['volume']['snapshot_id'] = '1234'
self.run_command('create --snapshot-id=1234')
self.assert_called_anytime('POST', '/volumes', partial_body=expected)
self.assert_called('GET', '/volumes/1234')
expected['volume']['size'] = 2
self.run_command('create --snapshot-id=1234 2')
self.assert_called_anytime('POST', '/volumes', partial_body=expected)
self.assert_called('GET', '/volumes/1234')
def test_create_volume_from_volume(self):
expected = {'volume': {'size': None}}
expected['volume']['source_volid'] = '1234'
self.run_command('create --source-volid=1234')
self.assert_called_anytime('POST', '/volumes', partial_body=expected)
self.assert_called('GET', '/volumes/1234')
expected['volume']['size'] = 2
self.run_command('create --source-volid=1234 2')
self.assert_called_anytime('POST', '/volumes', partial_body=expected)
self.assert_called('GET', '/volumes/1234')
def test_create_volume_from_replica(self):
expected = {'volume': {'size': None}}
expected['volume']['source_replica'] = '1234'
self.run_command('create --source-replica=1234')
self.assert_called_anytime('POST', '/volumes', partial_body=expected)
self.assert_called('GET', '/volumes/1234')
def test_create_size_required_if_not_snapshot_or_clone(self):
self.assertRaises(SystemExit, self.run_command, 'create')
def test_show(self):
self.run_command('show 1234')
self.assert_called('GET', '/volumes/1234')
@@ -113,6 +174,16 @@ class ShellTest(utils.TestCase):
self.run_command('backup-restore 1234')
self.assert_called('POST', '/backups/1234/restore')
def test_record_export(self):
self.run_command('backup-export 1234')
self.assert_called('GET', '/backups/1234/export_record')
def test_record_import(self):
self.run_command('backup-import fake.driver URL_STRING')
expected = {'backup-record': {'backup_service': 'fake.driver',
'backup_url': 'URL_STRING'}}
self.assert_called('POST', '/backups/import_record', expected)
def test_snapshot_list_filter_volume_id(self):
self.run_command('snapshot-list --volume-id=1234')
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
@@ -198,6 +269,17 @@ class ShellTest(utils.TestCase):
self.assert_called_anytime('POST', '/volumes/5678/action',
body=expected)
def test_reset_state_two_with_one_nonexistent(self):
cmd = 'reset-state 1234 123456789'
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called_anytime('POST', '/volumes/1234/action',
body=expected)
def test_reset_state_one_with_one_nonexistent(self):
cmd = 'reset-state 123456789'
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
def test_snapshot_reset_state(self):
self.run_command('snapshot-reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
@@ -249,9 +331,10 @@ class ShellTest(utils.TestCase):
- one GET request to retrieve the relevant volume type information
- one POST request to create the new encryption type
"""
expected = {'encryption': {'cipher': None, 'key_size': None,
'provider': 'TestProvider',
'control_location': None}}
'control_location': 'front-end'}}
self.run_command('encryption-type-create 2 TestProvider')
self.assert_called('POST', '/types/2/encryption', body=expected)
self.assert_called_anytime('GET', '/types/2')
@@ -325,6 +408,13 @@ class ShellTest(utils.TestCase):
self.assert_called('PUT', '/os-services/disable',
{"binary": "cinder-volume", "host": "host"})
def test_services_disable_with_reason(self):
cmd = 'service-disable host cinder-volume --reason no_reason'
self.run_command(cmd)
body = {'host': 'host', 'binary': 'cinder-volume',
'disabled_reason': 'no_reason'}
self.assert_called('PUT', '/os-services/disable-log-reason', body)
def test_service_enable(self):
self.run_command('service-enable host cinder-volume')
self.assert_called('PUT', '/os-services/enable',
@@ -345,3 +435,108 @@ class ShellTest(utils.TestCase):
def test_snapshot_delete(self):
self.run_command('snapshot-delete 1234')
self.assert_called('DELETE', '/snapshots/1234')
def test_quota_delete(self):
self.run_command('quota-delete 1234')
self.assert_called('DELETE', '/os-quota-sets/1234')
def test_snapshot_delete_multiple(self):
self.run_command('snapshot-delete 5678')
self.assert_called('DELETE', '/snapshots/5678')
def test_volume_manage(self):
self.run_command('manage host1 key1=val1 key2=val2 '
'--name foo --description bar '
'--volume-type baz --availability-zone az '
'--metadata k1=v1 k2=v2')
expected = {'volume': {'host': 'host1',
'ref': {'key1': 'val1', 'key2': 'val2'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
'availability_zone': 'az',
'metadata': {'k1': 'v1', 'k2': 'v2'},
'bootable': False}}
self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
def test_volume_manage_bootable(self):
"""
Tests the --bootable option
If this flag is specified, then the resulting POST should contain
bootable: True.
"""
self.run_command('manage host1 key1=val1 key2=val2 '
'--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'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
'availability_zone': 'az',
'metadata': {'k1': 'v1', 'k2': 'v2'},
'bootable': True}}
self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
def test_volume_manage_source_name(self):
"""
Tests the --source-name option.
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 '
'--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'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
'availability_zone': 'az',
'metadata': {'k1': 'v1', 'k2': 'v2'},
'bootable': False}}
self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
def test_volume_manage_source_id(self):
"""
Tests the --source-id option.
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 '
'--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'},
'name': 'foo',
'description': 'bar',
'volume_type': 'baz',
'availability_zone': 'az',
'metadata': {'k1': 'v1', 'k2': 'v2'},
'bootable': False}}
self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
def test_volume_unmanage(self):
self.run_command('unmanage 1234')
self.assert_called('POST', '/volumes/1234/action',
body={'os-unmanage': None})
def test_replication_promote(self):
self.run_command('replication-promote 1234')
self.assert_called('POST', '/volumes/1234/action',
body={'os-promote-replica': None})
def test_replication_reenable(self):
self.run_command('replication-reenable 1234')
self.assert_called('POST', '/volumes/1234/action',
body={'os-reenable-replica': None})

View File

@@ -14,22 +14,23 @@
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
from cinderclient.tests.fixture_data import client
from cinderclient.tests.fixture_data import snapshots
cs = fakes.FakeClient()
class SnapshotActionsTest(utils.FixturedTestCase):
client_fixture_class = client.V2
data_fixture_class = snapshots.Fixture
class SnapshotActionsTest(utils.TestCase):
def test_update_snapshot_status(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available'})
cs.assert_called('POST', '/snapshots/1234/action')
s = self.cs.volume_snapshots.get('1234')
stat = {'status': 'available'}
self.cs.volume_snapshots.update_snapshot_status(s, stat)
self.assert_called('POST', '/snapshots/1234/action')
def test_update_snapshot_status_with_progress(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available',
'progress': '73%'})
cs.assert_called('POST', '/snapshots/1234/action')
s = self.cs.volume_snapshots.get('1234')
stat = {'status': 'available', 'progress': '73%'}
self.cs.volume_snapshots.update_snapshot_status(s, stat)
self.assert_called('POST', '/snapshots/1234/action')

View File

@@ -26,12 +26,12 @@ class TypesTest(utils.TestCase):
tl = cs.volume_types.list()
cs.assert_called('GET', '/types')
for t in tl:
self.assertTrue(isinstance(t, volume_types.VolumeType))
self.assertIsInstance(t, volume_types.VolumeType)
def test_create(self):
t = cs.volume_types.create('test-type-3')
cs.assert_called('POST', '/types')
self.assertTrue(isinstance(t, volume_types.VolumeType))
self.assertIsInstance(t, volume_types.VolumeType)
def test_set_key(self):
t = cs.volume_types.get(1)

View File

@@ -51,3 +51,17 @@ class VolumeBackupsTest(utils.TestCase):
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
cs.restores.restore(backup_id)
cs.assert_called('POST', '/backups/%s/restore' % backup_id)
def test_record_export(self):
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
cs.backups.export_record(backup_id)
cs.assert_called('GET',
'/backups/%s/export_record' % backup_id)
def test_record_import(self):
backup_service = 'fake-backup-service'
backup_url = 'fake-backup-url'
expected_body = {'backup-record': {'backup_service': backup_service,
'backup_url': backup_url}}
cs.backups.import_record(backup_service, backup_url)
cs.assert_called('POST', '/backups/import_record', expected_body)

View File

@@ -73,12 +73,11 @@ class VolumeEncryptionTypesTest(utils.TestCase):
Verify that one POST request is made for the encryption type creation.
Verify that encryption type creation returns a VolumeEncryptionType.
"""
result = cs.volume_encryption_types.create(2, {'encryption':
{'provider': 'Test',
'key_size': None,
'cipher': None,
'control_location':
None}})
result = cs.volume_encryption_types.create(2, {'provider': 'Test',
'key_size': None,
'cipher': None,
'control_location':
None})
cs.assert_called('POST', '/types/2/encryption')
self.assertIsInstance(result, VolumeEncryptionType)

View File

@@ -23,6 +23,22 @@ cs = fakes.FakeClient()
class VolumesTest(utils.TestCase):
def test_list_volumes_with_marker_limit(self):
cs.volumes.list(marker=1234, limit=2)
cs.assert_called('GET', '/volumes/detail?limit=2&marker=1234')
def test_list_volumes_with_sort_key_dir(self):
cs.volumes.list(sort_key='id', sort_dir='asc')
cs.assert_called('GET', '/volumes/detail?sort_dir=asc&sort_key=id')
def test_list_volumes_with_invalid_sort_key(self):
self.assertRaises(ValueError,
cs.volumes.list, sort_key='invalid', sort_dir='asc')
def test_list_volumes_with_invalid_sort_dir(self):
self.assertRaises(ValueError,
cs.volumes.list, sort_key='id', sort_dir='invalid')
def test_delete_volume(self):
v = cs.volumes.list()[0]
v.delete()
@@ -36,6 +52,26 @@ class VolumesTest(utils.TestCase):
cs.volumes.create(1)
cs.assert_called('POST', '/volumes')
def test_create_volume_with_hint(self):
cs.volumes.create(1, scheduler_hints='uuid')
expected = {'volume': {'status': 'creating',
'description': None,
'availability_zone': None,
'source_volid': None,
'snapshot_id': None,
'size': 1,
'user_id': None,
'name': None,
'imageRef': None,
'attach_status': 'detached',
'volume_type': None,
'project_id': None,
'metadata': {},
'source_replica': None,
'consistencygroup_id': None},
'OS-SCH-HNT:scheduler_hints': 'uuid'}
cs.assert_called('POST', '/volumes', body=expected)
def test_attach(self):
v = cs.volumes.get('1234')
cs.volumes.attach(v, 1, '/dev/vdc', mode='ro')
@@ -116,3 +152,27 @@ class VolumesTest(utils.TestCase):
cs.assert_called('POST', '/volumes/1234/action',
{'os-retype': {'new_type': 'foo',
'migration_policy': 'on-demand'}})
def test_set_bootable(self):
v = cs.volumes.get('1234')
cs.volumes.set_bootable(v, True)
cs.assert_called('POST', '/volumes/1234/action')
def test_volume_manage(self):
cs.volumes.manage('host1', {'k': 'v'})
expected = {'host': 'host1', 'name': None, 'availability_zone': None,
'description': None, 'metadata': None, 'ref': {'k': 'v'},
'volume_type': None, 'bootable': False}
cs.assert_called('POST', '/os-volume-manage', {'volume': expected})
def test_volume_manage_bootable(self):
cs.volumes.manage('host1', {'k': 'v'}, bootable=True)
expected = {'host': 'host1', 'name': None, 'availability_zone': None,
'description': None, 'metadata': None, 'ref': {'k': 'v'},
'volume_type': None, 'bootable': True}
cs.assert_called('POST', '/os-volume-manage', {'volume': expected})
def test_volume_unmanage(self):
v = cs.volumes.get('1234')
cs.volumes.unmanage(v)
cs.assert_called('POST', '/volumes/1234/action', {'os-unmanage': None})

View File

@@ -16,6 +16,7 @@
from __future__ import print_function
import os
import pkg_resources
import re
import sys
import uuid
@@ -286,6 +287,15 @@ def import_class(import_str):
__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):
try:
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]+')

View File

@@ -31,7 +31,7 @@ class AvailabilityZoneManager(base.ManagerWithFind):
resource_class = AvailabilityZone
def list(self, detailed=False):
"""Get a list of all availability zones
"""Lists all availability zones.
:rtype: list of :class:`AvailabilityZone`
"""

View File

@@ -44,14 +44,14 @@ class Client(object):
"""
def __init__(self, username, api_key, project_id=None, auth_url='',
insecure=False, timeout=None, tenant_id=None,
def __init__(self, username=None, api_key=None, project_id=None,
auth_url='', insecure=False, timeout=None, tenant_id=None,
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='volume', service_name=None,
volume_service_name=None, retries=None,
http_log_debug=False,
cacert=None):
volume_service_name=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
@@ -80,16 +80,16 @@ class Client(object):
setattr(self, extension.name,
extension.manager_class(self))
self.client = client.HTTPClient(
username,
password,
project_id,
auth_url,
self.client = client._construct_http_client(
username=username,
password=password,
project_id=project_id,
auth_url=auth_url,
insecure=insecure,
timeout=timeout,
tenant_id=tenant_id,
proxy_tenant_id=tenant_id,
proxy_token=proxy_token,
proxy_tenant_id=proxy_tenant_id,
region_name=region_name,
endpoint_type=endpoint_type,
service_type=service_type,
@@ -97,7 +97,11 @@ class Client(object):
volume_service_name=volume_service_name,
retries=retries,
http_log_debug=http_log_debug,
cacert=cacert)
cacert=cacert,
auth_system=auth_system,
auth_plugin=auth_plugin,
session=session,
**kwargs)
def authenticate(self):
"""

View File

@@ -40,7 +40,7 @@ class ListExtManager(base.Manager):
@utils.service_type('volume')
def do_list_extensions(client, _args):
"""
List all the os-api extensions that are available.
Lists all available os-api extensions.
"""
extensions = client.list_extensions.show_all()
fields = ["Name", "Summary", "Alias", "Updated"]

View File

@@ -26,7 +26,7 @@ class QuotaSet(base.Resource):
return self.tenant_id
def update(self, *args, **kwargs):
self.manager.update(self.tenant_id, *args, **kwargs)
return self.manager.update(self.tenant_id, *args, **kwargs)
class QuotaSetManager(base.Manager):
@@ -44,8 +44,14 @@ class QuotaSetManager(base.Manager):
for update in updates:
body['quota_set'][update] = updates[update]
self._update('/os-quota-sets/%s' % (tenant_id), body)
result = self._update('/os-quota-sets/%s' % (tenant_id), body)
return self.resource_class(self, result['quota_set'], loaded=True)
def defaults(self, tenant_id):
return self._get('/os-quota-sets/%s/defaults' % tenant_id,
'quota_set')
def delete(self, tenant_id):
if hasattr(tenant_id, 'tenant_id'):
tenant_id = tenant_id.tenant_id
return self._delete("/os-quota-sets/%s" % tenant_id)

View File

@@ -52,7 +52,13 @@ class ServiceManager(base.ManagerWithFind):
return self.resource_class(self, result)
def disable(self, host, binary):
"""Enable the service specified by hostname and binary."""
"""Disable the service specified by hostname and binary."""
body = {"host": host, "binary": binary}
result = self._update("/os-services/disable", body)
return self.resource_class(self, result)
def disable_log_reason(self, host, binary, reason):
"""Disable the service with reason."""
body = {"host": host, "binary": binary, "disabled_reason": reason}
result = self._update("/os-services/disable-log-reason", body)
return self.resource_class(self, result)

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ class VolumeBackupManager(base.ManagerWithFind):
def create(self, volume_id, container=None,
name=None, description=None):
"""Create a volume backup.
"""Creates a volume backup.
:param volume_id: The ID of the volume to backup.
:param container: The name of the backup service container.

View File

@@ -65,7 +65,7 @@ class VolumeEncryptionTypeManager(base.ManagerWithFind):
def create(self, volume_type, specs):
"""
Create a new encryption type for the specified volume type.
Creates encryption type for a volume type. Default: admin only.
:param volume_type: the volume type on which to add an encryption type
:param specs: the encryption type specifications to add

View File

@@ -147,7 +147,7 @@ class SnapshotManager(base.ManagerWithFind):
"""
Update the display_name or display_description for a snapshot.
:param snapshot: The :class:`Snapshot` to delete.
:param snapshot: The :class:`Snapshot` to update.
"""
if not kwargs:
return

View File

@@ -35,7 +35,7 @@ class VolumeTransferManager(base.ManagerWithFind):
resource_class = VolumeTransfer
def create(self, volume_id, name=None):
"""Create a volume transfer.
"""Creates a volume transfer.
:param volume_id: The ID of the volume to transfer.
:param name: The name of the transfer.
@@ -48,7 +48,7 @@ class VolumeTransferManager(base.ManagerWithFind):
def accept(self, transfer_id, auth_key):
"""Accept a volume transfer.
:param transfer_id: The ID of the trasnfer to accept.
:param transfer_id: The ID of the transfer to accept.
:param auth_key: The auth_key of the transfer.
:rtype: :class:`VolumeTransfer`
"""

View File

@@ -101,13 +101,13 @@ class VolumeTypeManager(base.ManagerWithFind):
"""
Delete a specific volume_type.
:param volume_type: The ID of the :class:`VolumeType` to get.
:param volume_type: The name or ID of the :class:`VolumeType` to get.
"""
self._delete("/types/%s" % base.getid(volume_type))
def create(self, name):
"""
Create a volume type.
Creates a volume type.
:param name: Descriptive name of the volume type
:rtype: :class:`VolumeType`

View File

@@ -112,7 +112,7 @@ class Volume(base.Resource):
:param volume: The UUID of the volume to extend.
:param new_size: The desired size to extend volume to.
"""
self.manager.extend(self, volume, new_size)
self.manager.extend(self, new_size)
def migrate_volume(self, host, force_host_copy):
"""Migrate the volume to a new host."""
@@ -134,7 +134,7 @@ class Volume(base.Resource):
:param read_only: The value to indicate whether to update volume to
read-only access mode.
"""
self.manager.update_readonly_flag(self, volume, read_only)
self.manager.update_readonly_flag(self, read_only)
class VolumeManager(base.ManagerWithFind):
@@ -149,7 +149,7 @@ class VolumeManager(base.ManagerWithFind):
project_id=None, availability_zone=None,
metadata=None, imageRef=None):
"""
Create a volume.
Creates a volume.
:param size: Size of volume in GB
:param snapshot_id: ID of the snapshot
@@ -190,7 +190,7 @@ class VolumeManager(base.ManagerWithFind):
"""
Get a volume.
:param volume_id: The ID of the volume to delete.
:param volume_id: The ID of the volume to get.
:rtype: :class:`Volume`
"""
return self._get("/volumes/%s" % volume_id, "volume")
@@ -425,3 +425,8 @@ class VolumeManager(base.ManagerWithFind):
return self._action('os-update_readonly_flag',
base.getid(volume),
{'readonly': flag})
def set_bootable(self, volume, flag):
return self._action('os-set_bootable',
base.getid(volume),
{'bootable': flag})

View File

@@ -31,7 +31,7 @@ class AvailabilityZoneManager(base.ManagerWithFind):
resource_class = AvailabilityZone
def list(self, detailed=False):
"""Get a list of all availability zones
"""Lists all availability zones.
:rtype: list of :class:`AvailabilityZone`
"""

View File

@@ -0,0 +1,124 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
# 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.
"""cgsnapshot interface (v2 extension)."""
import six
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
class Cgsnapshot(base.Resource):
"""A cgsnapshot is snapshot of a consistency group."""
def __repr__(self):
return "<cgsnapshot: %s>" % self.id
def delete(self):
"""Delete this cgsnapshot."""
self.manager.delete(self)
def update(self, **kwargs):
"""Update the name or description for this cgsnapshot."""
self.manager.update(self, **kwargs)
class CgsnapshotManager(base.ManagerWithFind):
"""Manage :class:`Cgsnapshot` resources."""
resource_class = Cgsnapshot
def create(self, consistencygroup_id, name=None, description=None,
user_id=None,
project_id=None):
"""Creates a cgsnapshot.
:param consistencygroup: Name or uuid of a consistencygroup
:param name: Name of the cgsnapshot
:param description: Description of the cgsnapshot
:param user_id: User id derived from context
:param project_id: Project id derived from context
:rtype: :class:`Cgsnapshot`
"""
body = {'cgsnapshot': {'consistencygroup_id': consistencygroup_id,
'name': name,
'description': description,
'user_id': user_id,
'project_id': project_id,
'status': "creating",
}}
return self._create('/cgsnapshots', body, 'cgsnapshot')
def get(self, cgsnapshot_id):
"""Get a cgsnapshot.
:param cgsnapshot_id: The ID of the cgsnapshot to get.
:rtype: :class:`Cgsnapshot`
"""
return self._get("/cgsnapshots/%s" % cgsnapshot_id, "cgsnapshot")
def list(self, detailed=True, search_opts=None):
"""Lists all cgsnapshots.
:rtype: list of :class:`Cgsnapshot`
"""
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("/cgsnapshots%s%s" % (detail, query_string),
"cgsnapshots")
def delete(self, cgsnapshot):
"""Delete a cgsnapshot.
:param cgsnapshot: The :class:`Cgsnapshot` to delete.
"""
self._delete("/cgsnapshots/%s" % base.getid(cgsnapshot))
def update(self, cgsnapshot, **kwargs):
"""Update the name or description for a cgsnapshot.
:param cgsnapshot: The :class:`Cgsnapshot` to update.
"""
if not kwargs:
return
body = {"cgsnapshot": kwargs}
self._update("/cgsnapshots/%s" % base.getid(cgsnapshot), body)
def _action(self, action, cgsnapshot, info=None, **kwargs):
"""Perform a cgsnapshot "action."
"""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/cgsnapshots/%s/action' % base.getid(cgsnapshot)
return self.api.client.post(url, body=body)

View File

@@ -14,7 +14,9 @@
# under the License.
from cinderclient import client
from cinderclient.v1 import availability_zones
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 qos_specs
from cinderclient.v2 import quota_classes
@@ -42,14 +44,14 @@ class Client(object):
...
"""
def __init__(self, username, api_key, project_id=None, auth_url='',
insecure=False, timeout=None, tenant_id=None,
def __init__(self, username=None, api_key=None, project_id=None,
auth_url='', insecure=False, timeout=None, tenant_id=None,
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):
volume_service_name=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
@@ -68,6 +70,9 @@ class Client(object):
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
self.consistencygroups = consistencygroups.\
ConsistencygroupManager(self)
self.cgsnapshots = cgsnapshots.CgsnapshotManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
@@ -78,16 +83,16 @@ class Client(object):
setattr(self, extension.name,
extension.manager_class(self))
self.client = client.HTTPClient(
username,
password,
project_id,
auth_url,
self.client = client._construct_http_client(
username=username,
password=password,
project_id=project_id,
auth_url=auth_url,
insecure=insecure,
timeout=timeout,
tenant_id=tenant_id,
proxy_tenant_id=tenant_id,
proxy_token=proxy_token,
proxy_tenant_id=proxy_tenant_id,
region_name=region_name,
endpoint_type=endpoint_type,
service_type=service_type,
@@ -95,7 +100,11 @@ class Client(object):
volume_service_name=volume_service_name,
retries=retries,
http_log_debug=http_log_debug,
cacert=cacert)
cacert=cacert,
auth_system=auth_system,
auth_plugin=auth_plugin,
session=session,
**kwargs)
def authenticate(self):
"""Authenticate against the server.

View File

@@ -0,0 +1,131 @@
# Copyright (C) 2012 - 2014 EMC Corporation.
# 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.
"""Consistencygroup interface (v2 extension)."""
import six
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
class Consistencygroup(base.Resource):
"""A Consistencygroup of volumes."""
def __repr__(self):
return "<Consistencygroup: %s>" % self.id
def delete(self, force='False'):
"""Delete this consistencygroup."""
self.manager.delete(self, force)
def update(self, **kwargs):
"""Update the name or description for this consistencygroup."""
self.manager.update(self, **kwargs)
class ConsistencygroupManager(base.ManagerWithFind):
"""Manage :class:`Consistencygroup` resources."""
resource_class = Consistencygroup
def create(self, volume_types, name=None,
description=None, user_id=None,
project_id=None, availability_zone=None):
"""Creates a consistencygroup.
:param name: Name of the ConsistencyGroup
:param description: Description of the ConsistencyGroup
:param volume_types: Types of volume
:param user_id: User id derived from context
:param project_id: Project id derived from context
:param availability_zone: Availability Zone to use
:rtype: :class:`Consistencygroup`
"""
body = {'consistencygroup': {'name': name,
'description': description,
'volume_types': volume_types,
'user_id': user_id,
'project_id': project_id,
'availability_zone': availability_zone,
'status': "creating",
}}
return self._create('/consistencygroups', body, 'consistencygroup')
def get(self, group_id):
"""Get a consistencygroup.
:param group_id: The ID of the consistencygroup to get.
:rtype: :class:`Consistencygroup`
"""
return self._get("/consistencygroups/%s" % group_id,
"consistencygroup")
def list(self, detailed=True, search_opts=None):
"""Lists all consistencygroups.
:rtype: list of :class:`Consistencygroup`
"""
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("/consistencygroups%s%s" % (detail, query_string),
"consistencygroups")
def delete(self, consistencygroup, force=False):
"""Delete a consistencygroup.
:param Consistencygroup: The :class:`Consistencygroup` to delete.
"""
body = {'consistencygroup': {'force': force}}
self.run_hooks('modify_body_for_action', body, 'consistencygroup')
url = '/consistencygroups/%s/delete' % base.getid(consistencygroup)
return self.api.client.post(url, body=body)
def update(self, consistencygroup, **kwargs):
"""Update the name or description for a consistencygroup.
:param Consistencygroup: The :class:`Consistencygroup` to update.
"""
if not kwargs:
return
body = {"consistencygroup": kwargs}
self._update("/consistencygroups/%s" % base.getid(consistencygroup),
body)
def _action(self, action, consistencygroup, info=None, **kwargs):
"""Perform a consistencygroup "action."
"""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/consistencygroups/%s/action' % base.getid(consistencygroup)
return self.api.client.post(url, body=body)

View File

@@ -37,10 +37,10 @@ class ListExtManager(base.Manager):
return self._list("/extensions", 'extensions')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_list_extensions(client, _args):
"""
List all the os-api extensions that are available.
Lists all available os-api extensions.
"""
extensions = client.list_extensions.show_all()
fields = ["Name", "Summary", "Alias", "Updated"]

View File

@@ -24,7 +24,7 @@ class QuotaSet(base.Resource):
return self.tenant_id
def update(self, *args, **kwargs):
self.manager.update(self.tenant_id, *args, **kwargs)
return self.manager.update(self.tenant_id, *args, **kwargs)
class QuotaSetManager(base.Manager):
@@ -42,8 +42,14 @@ class QuotaSetManager(base.Manager):
for update in updates:
body['quota_set'][update] = updates[update]
self._update('/os-quota-sets/%s' % (tenant_id), body)
result = self._update('/os-quota-sets/%s' % (tenant_id), body)
return self.resource_class(self, result['quota_set'], loaded=True)
def defaults(self, tenant_id):
return self._get('/os-quota-sets/%s/defaults' % tenant_id,
'quota_set')
def delete(self, tenant_id):
if hasattr(tenant_id, 'tenant_id'):
tenant_id = tenant_id.tenant_id
return self._delete("/os-quota-sets/%s" % tenant_id)

View File

@@ -52,7 +52,13 @@ class ServiceManager(base.ManagerWithFind):
return self.resource_class(self, result)
def disable(self, host, binary):
"""Enable the service specified by hostname and binary."""
"""Disable the service specified by hostname and binary."""
body = {"host": host, "binary": binary}
result = self._update("/os-services/disable", body)
return self.resource_class(self, result)
def disable_log_reason(self, host, binary, reason):
"""Disable the service with reason."""
body = {"host": host, "binary": binary, "disabled_reason": reason}
result = self._update("/os-services/disable-log-reason", body)
return self.resource_class(self, result)

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ class VolumeBackupManager(base.ManagerWithFind):
def create(self, volume_id, container=None,
name=None, description=None):
"""Create a volume backup.
"""Creates a volume backup.
:param volume_id: The ID of the volume to backup.
:param container: The name of the backup service container.
@@ -51,7 +51,7 @@ class VolumeBackupManager(base.ManagerWithFind):
return self._create('/backups', body, 'backup')
def get(self, backup_id):
"""Show details of a volume backup.
"""Show volume backup details.
:param backup_id: The ID of the backup to display.
:rtype: :class:`VolumeBackup`
@@ -74,3 +74,26 @@ class VolumeBackupManager(base.ManagerWithFind):
:param backup: The :class:`VolumeBackup` to delete.
"""
self._delete("/backups/%s" % base.getid(backup))
def export_record(self, backup_id):
"""Export volume backup metadata record.
:param backup_id: The ID of the backup to export.
:rtype: :class:`VolumeBackup`
"""
resp, body = \
self.api.client.get("/backups/%s/export_record" % backup_id)
return body['backup-record']
def import_record(self, backup_service, backup_url):
"""Export volume backup metadata record.
:param backup_service: Backup service to use for importing the backup
:param backup_urlBackup URL for importing the backup metadata
:rtype: :class:`VolumeBackup`
"""
body = {'backup-record': {'backup_service': backup_service,
'backup_url': backup_url}}
self.run_hooks('modify_body_for_update', body, 'backup-record')
resp, body = self.api.client.post("/backups/import_record", body=body)
return body['backup']

View File

@@ -65,7 +65,7 @@ class VolumeEncryptionTypeManager(base.ManagerWithFind):
def create(self, volume_type, specs):
"""
Create a new encryption type for the specified volume type.
Creates encryption type for a volume type. Default: admin only.
:param volume_type: the volume type on which to add an encryption type
:param specs: the encryption type specifications to add

View File

@@ -69,7 +69,7 @@ class SnapshotManager(base.ManagerWithFind):
def create(self, volume_id, force=False,
name=None, description=None):
"""Create a snapshot of the given volume.
"""Creates a snapshot of the given volume.
:param volume_id: The ID of the volume to snapshot.
:param force: If force is True, create a snapshot even if the volume is
@@ -85,7 +85,7 @@ class SnapshotManager(base.ManagerWithFind):
return self._create('/snapshots', body, 'snapshot')
def get(self, snapshot_id):
"""Get a snapshot.
"""Shows snapshot details.
:param snapshot_id: The ID of the snapshot to get.
:rtype: :class:`Snapshot`
@@ -132,7 +132,7 @@ class SnapshotManager(base.ManagerWithFind):
def update(self, snapshot, **kwargs):
"""Update the name or description for a snapshot.
:param snapshot: The :class:`Snapshot` to delete.
:param snapshot: The :class:`Snapshot` to update.
"""
if not kwargs:
return

View File

@@ -35,7 +35,7 @@ class VolumeTransferManager(base.ManagerWithFind):
resource_class = VolumeTransfer
def create(self, volume_id, name=None):
"""Create a volume transfer.
"""Creates a volume transfer.
:param volume_id: The ID of the volume to transfer.
:param name: The name of the transfer.
@@ -48,7 +48,7 @@ class VolumeTransferManager(base.ManagerWithFind):
def accept(self, transfer_id, auth_key):
"""Accept a volume transfer.
:param transfer_id: The ID of the trasnfer to accept.
:param transfer_id: The ID of the transfer to accept.
:param auth_key: The auth_key of the transfer.
:rtype: :class:`VolumeTransfer`
"""

View File

@@ -71,7 +71,7 @@ class VolumeTypeManager(base.ManagerWithFind):
resource_class = VolumeType
def list(self, search_opts=None):
"""Get a list of all volume types.
"""Lists all volume types.
:rtype: list of :class:`VolumeType`.
"""
@@ -86,14 +86,14 @@ class VolumeTypeManager(base.ManagerWithFind):
return self._get("/types/%s" % base.getid(volume_type), "volume_type")
def delete(self, volume_type):
"""Delete a specific volume_type.
"""Deletes a specific volume_type.
:param volume_type: The ID of the :class:`VolumeType` to get.
:param volume_type: The name or ID of the :class:`VolumeType` to get.
"""
self._delete("/types/%s" % base.getid(volume_type))
def create(self, name):
"""Create a volume type.
"""Creates a volume type.
:param name: Descriptive name of the volume type
:rtype: :class:`VolumeType`

View File

@@ -24,6 +24,11 @@ except ImportError:
from cinderclient import base
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'size', 'availability_zone', 'name',
'bootable', 'created_at')
class Volume(base.Resource):
"""A volume is an extra block level storage to the OpenStack instances."""
def __repr__(self):
@@ -111,7 +116,7 @@ class Volume(base.Resource):
:param new_size: The desired size to extend volume to.
"""
self.manager.extend(self, volume, new_size)
self.manager.extend(self, new_size)
def migrate_volume(self, host, force_host_copy):
"""Migrate the volume to a new host."""
@@ -132,21 +137,44 @@ class Volume(base.Resource):
:param read_only: The value to indicate whether to update volume to
read-only access mode.
"""
self.manager.update_readonly_flag(self, volume, read_only)
self.manager.update_readonly_flag(self, read_only)
def manage(self, host, ref, name=None, description=None,
volume_type=None, availability_zone=None, metadata=None,
bootable=False):
"""Manage an existing volume."""
self.manager.manage(host=host, ref=ref, name=name,
description=description, volume_type=volume_type,
availability_zone=availability_zone,
metadata=metadata, bootable=bootable)
def unmanage(self, volume):
"""Unmanage a volume."""
self.manager.unmanage(volume)
def promote(self, volume):
"""Promote secondary to be primary in relationship."""
self.manager.promote(volume)
def reenable(self, volume):
"""Sync the secondary volume with primary for a relationship."""
self.manager.reenable(volume)
class VolumeManager(base.ManagerWithFind):
"""Manage :class:`Volume` resources."""
resource_class = Volume
def create(self, size, snapshot_id=None, source_volid=None,
name=None, description=None,
def create(self, size, consistencygroup_id=None, snapshot_id=None,
source_volid=None, name=None, description=None,
volume_type=None, user_id=None,
project_id=None, availability_zone=None,
metadata=None, imageRef=None, scheduler_hints=None):
"""Create a volume.
metadata=None, imageRef=None, scheduler_hints=None,
source_replica=None):
"""Creates a volume.
:param size: Size of volume in GB
:param consistencygroup_id: ID of the consistencygroup
:param snapshot_id: ID of the snapshot
:param name: Name of the volume
:param description: Description of the volume
@@ -157,6 +185,7 @@ class VolumeManager(base.ManagerWithFind):
:param metadata: Optional metadata to set on volume creation
:param imageRef: reference to an image stored in glance
:param source_volid: ID of source volume to clone from
:param source_replica: ID of source volume to clone replica
:param scheduler_hints: (optional extension) arbitrary key-value pairs
specified by the client to help boot an instance
:rtype: :class:`Volume`
@@ -168,6 +197,7 @@ class VolumeManager(base.ManagerWithFind):
volume_metadata = metadata
body = {'volume': {'size': size,
'consistencygroup_id': consistencygroup_id,
'snapshot_id': snapshot_id,
'name': name,
'description': description,
@@ -180,21 +210,33 @@ class VolumeManager(base.ManagerWithFind):
'metadata': volume_metadata,
'imageRef': imageRef,
'source_volid': source_volid,
'scheduler_hints': scheduler_hints,
'source_replica': source_replica,
}}
if scheduler_hints:
body['OS-SCH-HNT:scheduler_hints'] = scheduler_hints
return self._create('/volumes', body, 'volume')
def get(self, volume_id):
"""Get a volume.
:param volume_id: The ID of the volume to delete.
:param volume_id: The ID of the volume to get.
:rtype: :class:`Volume`
"""
return self._get("/volumes/%s" % volume_id, "volume")
def list(self, detailed=True, search_opts=None):
"""Get a list of all volumes.
def list(self, detailed=True, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None):
"""Lists all volumes.
:param detailed: Whether to return detailed volume info.
:param search_opts: Search options to filter out volumes.
: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'.
:rtype: list of :class:`Volume`
"""
if search_opts is None:
@@ -206,7 +248,33 @@ class VolumeManager(base.ManagerWithFind):
if val:
qparams[opt] = val
query_string = "?%s" % urlencode(qparams) if qparams else ""
if marker:
qparams['marker'] = marker
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))
# 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:
@@ -225,7 +293,7 @@ class VolumeManager(base.ManagerWithFind):
def update(self, volume, **kwargs):
"""Update the name or description for a volume.
:param volume: The :class:`Volume` to delete.
:param volume: The :class:`Volume` to update.
"""
if not kwargs:
return
@@ -419,3 +487,35 @@ class VolumeManager(base.ManagerWithFind):
volume,
{'new_type': volume_type,
'migration_policy': policy})
def set_bootable(self, volume, flag):
return self._action('os-set_bootable',
base.getid(volume),
{'bootable': flag})
def manage(self, host, ref, name=None, description=None,
volume_type=None, availability_zone=None, metadata=None,
bootable=False):
"""Manage an existing volume."""
body = {'volume': {'host': host,
'ref': ref,
'name': name,
'description': description,
'volume_type': volume_type,
'availability_zone': availability_zone,
'metadata': metadata,
'bootable': bootable
}}
return self._create('/os-volume-manage', body, 'volume')
def unmanage(self, volume):
"""Unmanage a volume."""
return self._action('os-unmanage', volume, None)
def promote(self, volume):
"""Promote secondary to be primary in relationship."""
return self._action('os-promote-replica', volume, None)
def reenable(self, volume):
"""Sync the secondary volume with primary for a relationship."""
return self._action('os-reenable-replica', volume, None)

View File

@@ -28,7 +28,7 @@ sys.path.insert(0, ROOT)
# Add any Sphinx extension module names here, as strings. They can be
# extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'oslosphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -102,7 +102,7 @@ man_pages = [
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'nature'
#html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View File

@@ -30,6 +30,42 @@ See also :doc:`/man/cinder`.
Release Notes
=============
MASTER
-----
1.1.1
------
.. _1370152 http://bugs.launchpad.net/python-cinderclient/+bug/1370152
1.1.0
------
* Add support for ConsistencyGroups
* Use Adapter from keystoneclient
* Add support for Replication feature
* Add pagination for Volume List
.. _1325773 http://bugs.launchpad.net/python-cinderclient/+bug/1325773
.. _1333257 http://bugs.launchpad.net/python-cinderclient/+bug/1333257
.. _1268480 http://bugs.launchpad.net/python-cinderclient/+bug/1268480
.. _1275025 http://bugs.launchpad.net/python-cinderclient/+bug/1275025
.. _1258489 http://bugs.launchpad.net/python-cinderclient/+bug/1258489
.. _1241682 http://bugs.launchpad.net/python-cinderclient/+bug/1241682
.. _1203471 http://bugs.launchpad.net/python-cinderclient/+bug/1203471
.. _1210874 http://bugs.launchpad.net/python-cinderclient/+bug/1210874
.. _1200214 http://bugs.launchpad.net/python-cinderclient/+bug/1200214
.. _1130572 http://bugs.launchpad.net/python-cinderclient/+bug/1130572
.. _1156994 http://bugs.launchpad.net/python-cinderclient/+bug/1156994
** Note Connection refused --> Connection error commit: c9e7818f3f90ce761ad8ccd09181c705880a4266
** Note Mask Passwords in log output commit: 80582f2b860b2dadef7ae07bdbd8395bf03848b1
1.0.9
------
.. _1255905: http://bugs.launchpad.net/python-cinderclient/+bug/1255905
.. _1267168: http://bugs.launchpad.net/python-cinderclient/+bug/1267168
.. _1284540: http://bugs.launchpad.net/python-cinderclient/+bug/1284540
1.0.8
-----
* Add support for reset-state on multiple volumes or snapshots at once

View File

@@ -2,6 +2,7 @@
# The list of modules to copy from openstack-common
module=apiclient
module=py3kcompat
module=strutils
module=install_venv_common

View File

@@ -1,7 +1,8 @@
pbr>=0.5.21,<1.0
pbr>=0.6,!=0.7,<1.0
argparse
PrettyTable>=0.7,<0.8
requests>=1.1
simplejson>=2.0.9
python-keystoneclient>=0.10.0
requests>=1.2.1
simplejson>=2.2.0
Babel>=1.3
six>=1.4.1
six>=1.7.0

View File

@@ -16,8 +16,10 @@ classifier =
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
[global]
setup-hooks =
@@ -38,3 +40,6 @@ build-dir = doc/build
[upload_sphinx]
upload-dir = doc/build/html
[wheel]
universal = 1

View File

@@ -17,6 +17,14 @@
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

View File

@@ -4,7 +4,9 @@ coverage>=3.6
discover
fixtures>=0.3.14
mock>=1.0
python-subunit
sphinx>=1.1.2,<1.2
testtools>=0.9.32
testrepository>=0.0.17
oslosphinx>=2.2.0.0a2
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
testrepository>=0.0.18

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013, Nebula, Inc.
# Copyright 2010 United States Government as represented by the

View File

@@ -1,5 +1,3 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
@@ -70,7 +68,6 @@ def main(argv):
install.check_dependencies()
install.create_virtualenv(no_site_packages=options.no_site_packages)
install.install_dependencies()
install.post_process()
print_help(project, venv, root)
if __name__ == '__main__':

View File

@@ -128,6 +128,9 @@ class InstallVenv(object):
"install")
return parser.parse_args(argv[1:])[0]
def post_process(self, **kwargs):
pass
class Distro(InstallVenv):

View File

@@ -8,9 +8,6 @@ skipsdist = True
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
@@ -25,6 +22,10 @@ commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:docs]
commands=
python setup.py build_sphinx
[tox:jenkins]
downloadcache = ~/cache/pip