Merge tag '1.1.1' into debian/experimental
Tag release version 1.1.1 of python-cinderclient
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ cover
|
||||
*.pyc
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
doc/build
|
||||
build
|
||||
dist
|
||||
cinderclient/versioninfo
|
||||
|
||||
1
.mailmap
1
.mailmap
@@ -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>
|
||||
|
||||
@@ -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
143
cinderclient/auth_plugin.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('.', '_')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
80
cinderclient/tests/fixture_data/availability_zones.py
Normal file
80
cinderclient/tests/fixture_data/availability_zones.py
Normal 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)
|
||||
38
cinderclient/tests/fixture_data/base.py
Normal file
38
cinderclient/tests/fixture_data/base.py
Normal 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)
|
||||
64
cinderclient/tests/fixture_data/client.py
Normal file
64
cinderclient/tests/fixture_data/client.py
Normal 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)
|
||||
230
cinderclient/tests/fixture_data/keystone_client.py
Normal file
230
cinderclient/tests/fixture_data/keystone_client.py
Normal 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)
|
||||
56
cinderclient/tests/fixture_data/snapshots.py
Normal file
56
cinderclient/tests/fixture_data/snapshots.py
Normal 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)
|
||||
346
cinderclient/tests/test_auth_plugins.py
Normal file
346
cinderclient/tests/test_auth_plugins.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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, {}, {})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
56
cinderclient/tests/v2/test_cgsnapshots.py
Normal file
56
cinderclient/tests/v2/test_cgsnapshots.py
Normal 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)
|
||||
52
cinderclient/tests/v2/test_consistencygroups.py
Normal file
52
cinderclient/tests/v2/test_consistencygroups.py
Normal 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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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]+')
|
||||
|
||||
|
||||
@@ -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`
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
"""
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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`
|
||||
"""
|
||||
|
||||
124
cinderclient/v2/cgsnapshots.py
Normal file
124
cinderclient/v2/cgsnapshots.py
Normal 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)
|
||||
@@ -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.
|
||||
|
||||
131
cinderclient/v2/consistencygroups.py
Normal file
131
cinderclient/v2/consistencygroups.py
Normal 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)
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
"""
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
module=apiclient
|
||||
module=py3kcompat
|
||||
module=strutils
|
||||
module=install_venv_common
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
8
setup.py
8
setup.py
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -128,6 +128,9 @@ class InstallVenv(object):
|
||||
"install")
|
||||
return parser.parse_args(argv[1:])[0]
|
||||
|
||||
def post_process(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class Distro(InstallVenv):
|
||||
|
||||
|
||||
7
tox.ini
7
tox.ini
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user