Merge branch 'debian/unstable' into debian/experimental

This commit is contained in:
Thomas Goirand
2014-09-30 14:48:50 +08:00
135 changed files with 11277 additions and 2656 deletions

7
.coveragerc Normal file
View File

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

7
.gitignore vendored
View File

@@ -6,12 +6,13 @@ subunit.log
*,cover
cover
*.pyc
.idea
*.swp
*~
AUTHORS
ChangeLog
build
dist
cinderclient/versioninfo
python_cinderclient.egg-info
# Development environment files
.project
.pydevproject

View File

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

115
HACKING
View File

@@ -1,115 +0,0 @@
Nova Style Commandments
=======================
Step 1: Read http://www.python.org/dev/peps/pep-0008/
Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
Step 3: Read on
Imports
-------
- thou shalt not import objects, only modules
- thou shalt not import more than one module per line
- thou shalt not make relative imports
- thou shalt organize your imports according to the following template
::
# vim: tabstop=4 shiftwidth=4 softtabstop=4
{{stdlib imports in human alphabetical order}}
\n
{{cinder imports in human alphabetical order}}
\n
\n
{{begin your code}}
General
-------
- thou shalt put two newlines twixt toplevel code (funcs, classes, etc)
- thou shalt put one newline twixt methods in classes and anywhere else
- thou shalt not write "except:", use "except Exception:" at the very least
- thou shalt include your name with TODOs as in "TODO(termie)"
- thou shalt not name anything the same name as a builtin or reserved word
- thou shalt not violate causality in our time cone, or else
Human Alphabetical Order Examples
---------------------------------
::
import httplib
import logging
import random
import StringIO
import time
import unittest
from cinder import flags
from cinder import test
from cinder.auth import users
from cinder.endpoint import api
from cinder.endpoint import cloud
Docstrings
----------
"""A one line docstring looks like this and ends in a period."""
"""A multiline docstring has a one-line summary, less than 80 characters.
Then a new paragraph after a newline that explains in more detail any
general information about the function, class or method. Example usages
are also great to have here if it is a complex class for function. After
you have finished your descriptions add an extra newline and close the
quotations.
When writing the docstring for a class, an extra line should be placed
after the closing quotations. For more in-depth explanations for these
decisions see http://www.python.org/dev/peps/pep-0257/
If you are going to describe parameters and return values, use Sphinx, the
appropriate syntax is as follows.
:param foo: the foo parameter
:param bar: the bar parameter
:returns: description of the return value
"""
Text encoding
----------
- All text within python code should be of type 'unicode'.
WRONG:
>>> s = 'foo'
>>> s
'foo'
>>> type(s)
<type 'str'>
RIGHT:
>>> u = u'foo'
>>> u
u'foo'
>>> type(u)
<type 'unicode'>
- Transitions between internal unicode and external strings should always
be immediately and explicitly encoded or decoded.
- All external text that is not explicitly encoded (database storage,
commandline arguments, etc.) should be presumed to be encoded as utf-8.
WRONG:
mystring = infile.readline()
myreturnstring = do_some_magic_with(mystring)
outfile.write(myreturnstring)
RIGHT:
mystring = infile.readline()
mytext = s.decode('utf-8')
returntext = do_some_magic_with(mytext)
returnstring = returntext.encode('utf-8')
outfile.write(returnstring)

77
HACKING.rst Normal file
View File

@@ -0,0 +1,77 @@
Cinder Client Style Commandments
================================
- Step 1: Read the OpenStack Style Commandments
http://docs.openstack.org/developer/hacking/
- Step 2: Read on
Cinder Client Specific Commandments
-----------------------------------
General
-------
- Use 'raise' instead of 'raise e' to preserve original traceback or exception being reraised::
except Exception as e:
...
raise e # BAD
except Exception:
...
raise # OKAY
Text encoding
-------------
- All text within python code should be of type 'unicode'.
WRONG:
>>> s = 'foo'
>>> s
'foo'
>>> type(s)
<type 'str'>
RIGHT:
>>> u = u'foo'
>>> u
u'foo'
>>> type(u)
<type 'unicode'>
- Transitions between internal unicode and external strings should always
be immediately and explicitly encoded or decoded.
- All external text that is not explicitly encoded (database storage,
commandline arguments, etc.) should be presumed to be encoded as utf-8.
WRONG:
mystring = infile.readline()
myreturnstring = do_some_magic_with(mystring)
outfile.write(myreturnstring)
RIGHT:
mystring = infile.readline()
mytext = s.decode('utf-8')
returntext = do_some_magic_with(mytext)
returnstring = returntext.encode('utf-8')
outfile.write(returnstring)
Release Notes
-------------
- Each patch should add an entry in the doc/source/index.rst file under
"MASTER".
- On each new release, the entries under "MASTER" will become the release notes
for that release, and "MASTER" will be cleared.
- The format should match existing release notes. For example, a feature::
* Add support for function foo
Or a bug fix::
.. _1241941: http://bugs.launchpad.net/python-cinderclient/+bug/1241941

View File

@@ -44,13 +44,13 @@ params, but it's easier to just set them as environment variables::
export OS_TENANT_NAME=myproject
You will also need to define the authentication url with ``--os-auth-url``
and the version of the API with ``--version``. Or set them as an environment
variables as well::
and the version of the API with ``--os-volume-api-version``. Or set them as
environment variables as well::
export OS_AUTH_URL=http://example.com:8774/v1.1/
export OS_VOLUME_API_VERSION=1
If you are using Keystone, you need to set the CINDER_URL to the keystone
If you are using Keystone, you need to set the OS_AUTH_URL to the keystone
endpoint::
export OS_AUTH_URL=http://example.com:5000/v2.0/
@@ -148,3 +148,5 @@ Quick-start using keystone::
>>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="volume")
>>> nt.volumes.list()
[...]
See release notes and more at `<http://docs.openstack.org/developer/python-cinderclient/>`_.

View File

@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC
# Copyright (c) 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -14,9 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.openstack.common import version
__all__ = ['__version__']
version_info = version.VersionInfo('python-cinderclient')
import pbr.version
version_info = pbr.version.VersionInfo('python-cinderclient')
# We have a circular import problem when we first run python setup.py sdist
# It's harmless, so deflect it.
try:

View File

@@ -1,6 +1,6 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -18,10 +18,13 @@
"""
Base utilities to build API operation managers and objects on top of.
"""
import abc
import contextlib
import hashlib
import os
import six
from cinderclient import exceptions
from cinderclient import utils
@@ -99,14 +102,15 @@ class Manager(utils.HookableMixin):
# pair
username = utils.env('OS_USERNAME', 'CINDER_USERNAME')
url = utils.env('OS_URL', 'CINDER_URL')
uniqifier = hashlib.md5(username + url).hexdigest()
uniqifier = hashlib.md5(username.encode('utf-8') +
url.encode('utf-8')).hexdigest()
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
try:
os.makedirs(cache_dir, 0755)
os.makedirs(cache_dir, 0o755)
except OSError:
# NOTE(kiall): This is typicaly either permission denied while
# NOTE(kiall): This is typically either permission denied while
# attempting to create the directory, or the directory
# already exists. Either way, don't fail.
pass
@@ -120,7 +124,7 @@ class Manager(utils.HookableMixin):
try:
setattr(self, cache_attr, open(path, mode))
except IOError:
# NOTE(kiall): This is typicaly a permission denied while
# NOTE(kiall): This is typically a permission denied while
# attempting to write the cache file.
pass
@@ -163,10 +167,15 @@ class Manager(utils.HookableMixin):
return body
class ManagerWithFind(Manager):
class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)):
"""
Like a `Manager`, but with additional `find()`/`findall()` methods.
"""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""
Find a single item with attributes matching ``**kwargs``.
@@ -192,9 +201,12 @@ class ManagerWithFind(Manager):
the Python side.
"""
found = []
searches = kwargs.items()
searches = list(kwargs.items())
for obj in self.list():
# Want to search for all tenants here so that when attempting to delete
# that a user like admin doesn't get a failure when trying to delete
# another tenant's volume by name.
for obj in self.list(search_opts={'all_tenants': 1}):
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
@@ -204,9 +216,6 @@ class ManagerWithFind(Manager):
return found
def list(self):
raise NotImplementedError
class Resource(object):
"""
@@ -245,7 +254,7 @@ class Resource(object):
return None
def _add_details(self, info):
for (k, v) in info.iteritems():
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
except AttributeError:
@@ -264,8 +273,8 @@ class Resource(object):
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
reprkeys = sorted(k for k in 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)

View File

@@ -1,16 +1,33 @@
# Copyright (c) 2011 OpenStack Foundation
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
from __future__ import print_function
import logging
import os
import sys
import urlparse
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
try:
from eventlet import sleep
except ImportError:
@@ -61,6 +78,7 @@ class HTTPClient(object):
self.auth_token = None
self.proxy_token = proxy_token
self.proxy_tenant_id = proxy_tenant_id
self.timeout = timeout
if insecure:
self.verify_cert = False
@@ -71,7 +89,7 @@ class HTTPClient(object):
self.verify_cert = True
self._logger = logging.getLogger(__name__)
if self.http_log_debug:
if self.http_log_debug and not self._logger.handlers:
ch = logging.StreamHandler()
self._logger.setLevel(logging.DEBUG)
self._logger.addHandler(ch)
@@ -115,6 +133,8 @@ class HTTPClient(object):
kwargs['data'] = json.dumps(kwargs['body'])
del kwargs['body']
if self.timeout:
kwargs.setdefault('timeout', self.timeout)
self.http_log_req((url, method,), kwargs)
resp = requests.request(
method,
@@ -174,7 +194,8 @@ class HTTPClient(object):
except requests.exceptions.ConnectionError as e:
# Catch a connection refused from requests.request
self._logger.debug("Connection refused: %s" % e)
raise
msg = 'Unable to establish connection: %s' % e
raise exceptions.ConnectionError(msg)
self._logger.debug(
"Failed attempt(%s of %s), retrying in %s seconds" %
(attempts, self.retries, backoff))
@@ -196,7 +217,8 @@ class HTTPClient(object):
def _extract_service_catalog(self, url, resp, body, extract_token=True):
"""See what the auth service told us and process the response.
We may get redirected to another site, fail or actually get
back a service catalog with a token and our endpoints."""
back a service catalog with a token and our endpoints.
"""
if resp.status_code == 200: # content must always present
try:
@@ -217,13 +239,13 @@ class HTTPClient(object):
self.management_url = management_url.rstrip('/')
return None
except exceptions.AmbiguousEndpoints:
print "Found more than one valid endpoint. Use a more " \
"restrictive filter"
print("Found more than one valid endpoint. Use a more "
"restrictive filter")
raise
except KeyError:
raise exceptions.AuthorizationFailure()
except exceptions.EndpointNotFound:
print "Could not find any suitable endpoint. Correct region?"
print("Could not find any suitable endpoint. Correct region?")
raise
elif resp.status_code == 305:
@@ -248,7 +270,7 @@ class HTTPClient(object):
% (self.proxy_token, self.proxy_tenant_id)])
self._logger.debug("Using Endpoint URL: %s" % url)
resp, body = self.request(url, "GET",
headers={'X-Auth_Token': self.auth_token})
headers={'X-Auth-Token': self.auth_token})
return self._extract_service_catalog(url, resp, body,
extract_token=False)
@@ -273,10 +295,7 @@ class HTTPClient(object):
auth_url = self.auth_url
if self.version == "v2.0":
while auth_url:
if "CINDER_RAX_AUTH" in os.environ:
auth_url = self._rax_auth(auth_url)
else:
auth_url = self._v2_auth(auth_url)
auth_url = self._v2_auth(auth_url)
# Are we acting on behalf of another user via an
# existing token? If so, our actual endpoints may
@@ -335,16 +354,6 @@ class HTTPClient(object):
self._authenticate(url, body)
def _rax_auth(self, url):
"""Authenticate against the Rackspace auth service."""
body = {"auth": {
"RAX-KSKEY:apiKeyCredentials": {
"username": self.user,
"apiKey": self.password,
"tenantName": self.projectid}}}
self._authenticate(url, body)
def _authenticate(self, url, body):
"""Authenticate and extract the service catalog."""
token_url = url + "/tokens"
@@ -358,6 +367,18 @@ 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 get_client_class(version):
version_map = {
@@ -368,7 +389,7 @@ def get_client_class(version):
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = "Invalid client version '%s'. must be one of: %s" % (
(version, ', '.join(version_map.keys())))
(version, ', '.join(version_map)))
raise exceptions.UnsupportedVersion(msg)
return utils.import_class(client_path)

View File

@@ -1,4 +1,18 @@
# Copyright 2010 Jacob Kaplan-Moss
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Exception definitions.
"""
@@ -6,7 +20,12 @@ Exception definitions.
class UnsupportedVersion(Exception):
"""Indicates that the user is trying to use an unsupported
version of the API"""
version of the API.
"""
pass
class InvalidAPIVersion(Exception):
pass
@@ -24,7 +43,8 @@ class NoUniqueMatch(Exception):
class NoTokenLookupException(Exception):
"""This form of authentication does not support looking up
endpoints from an existing token."""
endpoints from an existing token.
"""
pass
@@ -33,6 +53,11 @@ class EndpointNotFound(Exception):
pass
class ConnectionError(Exception):
"""Could not open a connection to the API service."""
pass
class AmbiguousEndpoints(Exception):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
@@ -141,7 +166,7 @@ def from_response(response, body):
message = "n/a"
details = "n/a"
if hasattr(body, 'keys'):
error = body[body.keys()[0]]
error = body[list(body)[0]]
message = error.get('message', None)
details = error.get('details', None)
return cls(code=response.status_code, message=message, details=details,

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -29,7 +29,7 @@ class Extension(utils.HookableMixin):
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
for attr_name, attr_value in list(self.module.__dict__.items()):
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
elif utils.safe_issubclass(attr_value, base.Manager):

View File

@@ -0,0 +1,221 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import abc
import argparse
import os
import six
from stevedore import extension
from cinderclient.openstack.common.apiclient import exceptions
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
global _discovered_plugins
_discovered_plugins = {}
def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin
ep_namespace = "cinderclient.openstack.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in six.iteritems(_discovered_plugins):
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)
def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)
def load_plugin_from_args(args):
"""Load required plugin and populate it with options.
Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.
:type args: argparse.Namespace
:raises: AuthorizationFailure
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]
def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)
@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.
:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin.
"""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins.
"""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.
:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))
def authenticate(self, http_client):
"""Authenticate using plugin defined method.
The method usually analyses `self.opts` and performs
a request to authentication server.
:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication.
"""
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
missing = [opt
for opt in self.opt_names
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)
@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.
:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

View File

@@ -0,0 +1,491 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import six
from cinderclient.openstack.common.apiclient import exceptions
from cinderclient.openstack.common.py3kcompat import urlutils
from cinderclient.openstack.common import strutils
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param **args: args to be passed to every hook function
:param **kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in six.iteritems(kwargs.copy()):
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
return strutils.to_slug(getattr(self, self.NAME_ATTR))
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val

View File

@@ -0,0 +1,358 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import logging
import time
try:
import simplejson as json
except ImportError:
import json
import requests
from cinderclient.openstack.common.apiclient import exceptions
from cinderclient.openstack.common import importutils
_logger = logging.getLogger(__name__)
class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "cinderclient.openstack.common.apiclient"
def __init__(self,
auth_plugin,
region_name=None,
endpoint_type="publicURL",
original_ip=None,
verify=True,
cert=None,
timeout=None,
timings=False,
keyring_saver=None,
debug=False,
user_agent=None,
http=None):
self.auth_plugin = auth_plugin
self.endpoint_type = endpoint_type
self.region_name = region_name
self.original_ip = original_ip
self.timeout = timeout
self.verify = verify
self.cert = cert
self.keyring_saver = keyring_saver
self.debug = debug
self.user_agent = user_agent or self.user_agent
self.times = [] # [("item", starttime, endtime), ...]
self.timings = timings
# requests within the same session can reuse TCP connections from pool
self.http = http or requests.Session()
self.cached_token = None
def _http_log_req(self, method, url, kwargs):
if not self.debug:
return
string_parts = [
"curl -i",
"-X '%s'" % method,
"'%s'" % url,
]
for element in kwargs['headers']:
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
string_parts.append(header)
_logger.debug("REQ: %s" % " ".join(string_parts))
if 'data' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
def _http_log_resp(self, resp):
if not self.debug:
return
_logger.debug(
"RESP: [%s] %s\n",
resp.status_code,
resp.headers)
if resp._content_consumed:
_logger.debug(
"RESP BODY: %s\n",
resp.text)
def serialize(self, kwargs):
if kwargs.get('json') is not None:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['json'])
try:
del kwargs['json']
except KeyError:
pass
def get_timings(self):
return self.times
def reset_timings(self):
self.times = []
def request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
' requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", kwargs.get("headers", {}))
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
self.original_ip, self.user_agent)
if self.timeout is not None:
kwargs.setdefault("timeout", self.timeout)
kwargs.setdefault("verify", self.verify)
if self.cert is not None:
kwargs.setdefault("cert", self.cert)
self.serialize(kwargs)
self._http_log_req(method, url, kwargs)
if self.timings:
start_time = time.time()
resp = self.http.request(method, url, **kwargs)
if self.timings:
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
self._http_log_resp(resp)
if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
resp.status_code)
raise exceptions.from_response(resp, method, url)
return resp
@staticmethod
def concat_url(endpoint, url):
"""Concatenate endpoint and final URL.
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
"http://keystone/v2.0/tokens".
:param endpoint: the base URL
:param url: the final URL
"""
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
def client_request(self, client, method, url, **kwargs):
"""Send an http request using `client`'s endpoint and specified `url`.
If request was rejected as unauthorized (possibly because the token is
expired), issue one authorization attempt and send the request once
again.
:param client: instance of BaseClient descendant
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
' `HTTPClient.request`
"""
filter_args = {
"endpoint_type": client.endpoint_type or self.endpoint_type,
"service_type": client.service_type,
}
token, endpoint = (self.cached_token, client.cached_endpoint)
just_authenticated = False
if not (token and endpoint):
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
pass
if not (token and endpoint):
self.authenticate()
just_authenticated = True
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
if not (token and endpoint):
raise exceptions.AuthorizationFailure(
"Cannot find endpoint or token for request")
old_token_endpoint = (token, endpoint)
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
self.cached_token = token
client.cached_endpoint = endpoint
# Perform the request once. If we get Unauthorized, then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
except exceptions.Unauthorized as unauth_ex:
if just_authenticated:
raise
self.cached_token = None
client.cached_endpoint = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
raise unauth_ex
if (not (token and endpoint) or
old_token_endpoint == (token, endpoint)):
raise unauth_ex
self.cached_token = token
client.cached_endpoint = endpoint
kwargs["headers"]["X-Auth-Token"] = token
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
def add_client(self, base_client_instance):
"""Add a new instance of :class:`BaseClient` descendant.
`self` will store a reference to `base_client_instance`.
Example:
>>> def test_clients():
... from keystoneclient.auth import keystone
... from openstack.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
... openstack_client = client.HTTPClient(auth)
... # create nova client
... from novaclient.v1_1 import client
... client.Client(openstack_client)
... # create keystone client
... from keystoneclient.v2_0 import client
... client.Client(openstack_client)
... # use them
... openstack_client.identity.tenants.list()
... openstack_client.compute.servers.list()
"""
service_type = base_client_instance.service_type
if service_type and not hasattr(self, service_type):
setattr(self, service_type, base_client_instance)
def authenticate(self):
self.auth_plugin.authenticate(self)
# Store the authentication results in the keyring for later requests
if self.keyring_saver:
self.keyring_saver.save(self)
class BaseClient(object):
"""Top-level object to access the OpenStack API.
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
will handle a bunch of issues such as authentication.
"""
service_type = None
endpoint_type = None # "publicURL" will be used
cached_endpoint = None
def __init__(self, http_client, extensions=None):
self.http_client = http_client
http_client.add_client(self)
# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))
def client_request(self, method, url, **kwargs):
return self.http_client.client_request(
self, method, url, **kwargs)
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.client_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
@staticmethod
def get_class(api_name, version, version_map):
"""Returns the client class for the requested API version
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = "Invalid %s client version '%s'. must be one of: %s" % (
(api_name, version, ', '.join(version_map.keys())))
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)

View File

@@ -0,0 +1,444 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Exception definitions.
"""
import inspect
import sys
import six
class ClientException(Exception):
"""The base exception class for all exceptions this library raises.
"""
pass
class MissingArgs(ClientException):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = "Missing argument(s): %s" % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
class UnsupportedVersion(ClientException):
"""User is trying to use an unsupported version of the API."""
pass
class CommandError(ClientException):
"""Error in CLI tool."""
pass
class AuthorizationFailure(ClientException):
"""Cannot authorize API client."""
pass
class ConnectionRefused(ClientException):
"""Cannot connect to API service."""
pass
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
super(AuthPluginOptionsMissing, self).__init__(
"Authentication failed. Missing options: %s" %
", ".join(opt_names))
self.opt_names = opt_names
class AuthSystemNotFound(AuthorizationFailure):
"""User has specified a AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
"AuthSystemNotFound: %s" % repr(auth_system))
self.auth_system = auth_system
class NoUniqueMatch(ClientException):
"""Multiple entities found instead of one."""
pass
class EndpointException(ClientException):
"""Something is rotten in Service Catalog."""
pass
class EndpointNotFound(EndpointException):
"""Could not find requested endpoint in Service Catalog."""
pass
class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
"AmbiguousEndpoints: %s" % repr(endpoints))
self.endpoints = endpoints
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions.
"""
http_status = 0
message = "HTTP Error"
def __init__(self, message=None, details=None,
response=None, request_id=None,
url=None, method=None, http_status=None):
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
self.request_id = request_id
self.response = response
self.url = url
self.method = method
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
if request_id:
formatted_string += " (Request-ID: %s)" % request_id
super(HttpError, self).__init__(formatted_string)
class HTTPClientError(HttpError):
"""Client-side HTTP error.
Exception for cases in which the client seems to have erred.
"""
message = "HTTP Client Error"
class HttpServerError(HttpError):
"""Server-side HTTP error.
Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
message = "HTTP Server Error"
class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.
The request cannot be fulfilled due to bad syntax.
"""
http_status = 400
message = "Bad Request"
class Unauthorized(HTTPClientError):
"""HTTP 401 - Unauthorized.
Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
http_status = 401
message = "Unauthorized"
class PaymentRequired(HTTPClientError):
"""HTTP 402 - Payment Required.
Reserved for future use.
"""
http_status = 402
message = "Payment Required"
class Forbidden(HTTPClientError):
"""HTTP 403 - Forbidden.
The request was a valid request, but the server is refusing to respond
to it.
"""
http_status = 403
message = "Forbidden"
class NotFound(HTTPClientError):
"""HTTP 404 - Not Found.
The requested resource could not be found but may be available again
in the future.
"""
http_status = 404
message = "Not Found"
class MethodNotAllowed(HTTPClientError):
"""HTTP 405 - Method Not Allowed.
A request was made of a resource using a request method not supported
by that resource.
"""
http_status = 405
message = "Method Not Allowed"
class NotAcceptable(HTTPClientError):
"""HTTP 406 - Not Acceptable.
The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
http_status = 406
message = "Not Acceptable"
class ProxyAuthenticationRequired(HTTPClientError):
"""HTTP 407 - Proxy Authentication Required.
The client must first authenticate itself with the proxy.
"""
http_status = 407
message = "Proxy Authentication Required"
class RequestTimeout(HTTPClientError):
"""HTTP 408 - Request Timeout.
The server timed out waiting for the request.
"""
http_status = 408
message = "Request Timeout"
class Conflict(HTTPClientError):
"""HTTP 409 - Conflict.
Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
http_status = 409
message = "Conflict"
class Gone(HTTPClientError):
"""HTTP 410 - Gone.
Indicates that the resource requested is no longer available and will
not be available again.
"""
http_status = 410
message = "Gone"
class LengthRequired(HTTPClientError):
"""HTTP 411 - Length Required.
The request did not specify the length of its content, which is
required by the requested resource.
"""
http_status = 411
message = "Length Required"
class PreconditionFailed(HTTPClientError):
"""HTTP 412 - Precondition Failed.
The server does not meet one of the preconditions that the requester
put on the request.
"""
http_status = 412
message = "Precondition Failed"
class RequestEntityTooLarge(HTTPClientError):
"""HTTP 413 - Request Entity Too Large.
The request is larger than the server is willing or able to process.
"""
http_status = 413
message = "Request Entity Too Large"
def __init__(self, *args, **kwargs):
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
self.retry_after = 0
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
class RequestUriTooLong(HTTPClientError):
"""HTTP 414 - Request-URI Too Long.
The URI provided was too long for the server to process.
"""
http_status = 414
message = "Request-URI Too Long"
class UnsupportedMediaType(HTTPClientError):
"""HTTP 415 - Unsupported Media Type.
The request entity has a media type which the server or resource does
not support.
"""
http_status = 415
message = "Unsupported Media Type"
class RequestedRangeNotSatisfiable(HTTPClientError):
"""HTTP 416 - Requested Range Not Satisfiable.
The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
http_status = 416
message = "Requested Range Not Satisfiable"
class ExpectationFailed(HTTPClientError):
"""HTTP 417 - Expectation Failed.
The server cannot meet the requirements of the Expect request-header field.
"""
http_status = 417
message = "Expectation Failed"
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = "Unprocessable Entity"
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = "Internal Server Error"
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = "Not Implemented"
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = "Bad Gateway"
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = "Service Unavailable"
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = "Gateway Timeout"
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = "HTTP Version Not Supported"
# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in six.iteritems(vars(sys.modules[__name__]))
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": response.headers.get("x-compute-request-id"),
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if hasattr(body, "keys"):
error = body[body.keys()[0]]
kwargs["message"] = error.get("message", None)
kwargs["details"] = error.get("details", None)
elif content_type.startswith("text/"):
kwargs["details"] = response.text
try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

View File

@@ -0,0 +1,173 @@
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import requests
import six
from cinderclient.openstack.common.apiclient import client
from cinderclient.openstack.common.py3kcompat import urlutils
def assert_has_keys(dct, required=[], optional=[]):
for k in required:
try:
assert k in dct
except AssertionError:
extra_keys = set(dct.keys()).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization.
"""
def __init__(self, data):
super(TestResponse, self).__init__()
self._content_consumed = True
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
# Fake the text attribute to streamline Response creation
text = data.get('text', "")
if isinstance(text, (dict, list)):
self._content = json.dumps(text)
default_headers = {
"Content-Type": "application/json",
}
else:
self._content = text
default_headers = {}
if six.PY3 and isinstance(self._content, six.string_types):
self._content = self._content.encode('utf-8', 'strict')
self.headers = data.get('headers') or default_headers
else:
self.status_code = data
def __eq__(self, other):
return (self.status_code == other.status_code and
self.headers == other.headers and
self._content == other._content)
class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and not "auth_plugin" in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called.
"""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = urlutils.parse_qsl(urlutils.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@@ -0,0 +1,440 @@
# Copyright 2012 Red Hat, Inc.
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
gettext for openstack-common modules.
Usual usage in an openstack.common module:
from cinderclient.openstack.common.gettextutils import _
"""
import copy
import gettext
import 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 = {}
USE_LAZY = False
def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
Call this at the start of execution to enable the gettextutils._
function to use lazy gettext functionality. This is useful if
your project is importing _ directly instead of using the
gettextutils.install() way of importing the _ function.
"""
global USE_LAZY
USE_LAZY = True
def _(msg):
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):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
install() function.
The main difference from gettext.install() is that we allow
overriding the default localedir (e.g. /usr/share/locale) using
a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR).
:param domain: the translation domain
:param lazy: indicates whether or not to install the lazy _() function.
The lazy _() introduces a way to do deferred translation
of messages by installing a _ that builds Message objects,
instead of strings, which can then be lazily translated into
any available locale.
"""
if lazy:
# NOTE(mrodden): Lazy gettext functionality.
#
# The following introduces a deferred way to do translations on
# messages in OpenStack. We override the standard _() function
# and % (format string) operation to build Message objects that can
# later be translated when we have more information.
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)
class Message(six.text_type):
"""A Message object is a unicode object that can be translated.
Translation of Message is done explicitly using the translate() method.
For all non-translation intents and purposes, a Message is simply unicode,
and can be treated as such.
"""
def __new__(cls, msgid, msgtext=None, params=None,
domain='cinderclient', *args):
"""Create a new Message object.
In order for translation to work gettext requires a message ID, this
msgid will be used as the base unicode text. It is also possible
for the msgid and the base unicode text to be different by passing
the msgtext parameter.
"""
# If the base msgtext is not given, we use the default translation
# of the msgid (which is in English) just in case the system locale is
# not English, so that the base text will be in that locale by default.
if not msgtext:
msgtext = Message._translate_msgid(msgid, domain)
# We want to initialize the parent unicode with the actual object that
# would have been plain unicode if 'Message' was not enabled.
msg = super(Message, cls).__new__(cls, msgtext)
msg.msgid = msgid
msg.domain = domain
msg.params = params
return msg
def translate(self, desired_locale=None):
"""Translate this message to the desired locale.
:param desired_locale: The desired locale to translate the message to,
if no locale is provided the message will be
translated to the system's default locale.
:returns: the translated message in unicode
"""
translated_message = Message._translate_msgid(self.msgid,
self.domain,
desired_locale)
if self.params is None:
# No need for more translation
return translated_message
# This Message object may have been formatted with one or more
# Message objects as substitution arguments, given either as a single
# argument, part of a tuple, or as one or more values in a dictionary.
# When translating this Message we need to translate those Messages too
translated_params = _translate_args(self.params, desired_locale)
translated_message = translated_message % translated_params
return translated_message
@staticmethod
def _translate_msgid(msgid, domain, desired_locale=None):
if not desired_locale:
system_locale = locale.getdefaultlocale()
# If the system locale is not available to the runtime use English
if not system_locale[0]:
desired_locale = 'en_US'
else:
desired_locale = system_locale[0]
locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
lang = gettext.translation(domain,
localedir=locale_dir,
languages=[desired_locale],
fallback=True)
if six.PY3:
translator = lang.gettext
else:
translator = lang.ugettext
translated_message = translator(msgid)
return translated_message
def __mod__(self, other):
# When we mod a Message we want the actual operation to be performed
# by the parent class (i.e. unicode()), the only thing we do here is
# save the original msgid and the parameters in case of a translation
params = self._sanitize_mod_params(other)
unicode_mod = super(Message, self).__mod__(params)
modded = Message(self.msgid,
msgtext=unicode_mod,
params=params,
domain=self.domain)
return modded
def _sanitize_mod_params(self, other):
"""Sanitize the object being modded with this Message.
- Add support for modding 'None' so translation supports it
- Trim the modded object, which can be a large dictionary, to only
those keys that would actually be used in a translation
- Snapshot the object being modded, in case the message is
translated, it will be used as it was when the Message was created
"""
if other is None:
params = (other,)
elif isinstance(other, dict):
params = self._trim_dictionary_parameters(other)
else:
params = self._copy_param(other)
return params
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:
# Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied
return six.text_type(param)
def __add__(self, other):
msg = _('Message objects do not support addition.')
raise TypeError(msg)
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)
def get_available_languages(domain):
"""Lists the available languages for the given translation domain.
:param domain: the domain to get languages for
"""
if domain in _AVAILABLE_LANGUAGES:
return copy.copy(_AVAILABLE_LANGUAGES[domain])
localedir = '%s_LOCALEDIR' % domain.upper()
find = lambda x: gettext.find(domain,
localedir=os.environ.get(localedir),
languages=[x])
# NOTE(mrodden): en_US should always be available (and first in case
# order matters) since our in-line message strings are en_US
language_list = ['en_US']
# NOTE(luisg): Babel <1.0 used a function called list(), which was
# renamed to locale_identifiers() in >=1.0, the requirements master list
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
# this check when the master list updates to >=1.0, and update all projects
list_identifiers = (getattr(localedata, 'list', None) or
getattr(localedata, 'locale_identifiers'))
locale_identifiers = list_identifiers()
for i in locale_identifiers:
if find(i) is not None:
language_list.append(i)
# NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
# locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
# are perfectly legitimate locales:
# https://github.com/mitsuhiko/babel/issues/37
# In Babel 1.3 they fixed the bug and they support these locales, but
# they are still not explicitly "listed" by locale_identifiers().
# That is why we add the locales here explicitly if necessary so that
# they are listed as supported.
aliases = {'zh': 'zh_CN',
'zh_Hant_HK': 'zh_HK',
'zh_Hant': 'zh_TW',
'fil': 'tl_PH'}
for (locale, alias) in six.iteritems(aliases):
if locale in language_list and alias not in language_list:
language_list.append(alias)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
def translate(obj, desired_locale=None):
"""Gets the translated unicode representation of the given object.
If the object is not translatable it is returned as-is.
If the locale is None the object is translated to the system locale.
:param obj: the object to translate
:param desired_locale: the locale to translate the message to, if None the
default system locale will be used
:returns: the translated object in unicode, or the original object if
it could not be translated
"""
message = obj
if not isinstance(message, Message):
# If the object to translate is not already translatable,
# let's first get its unicode representation
message = six.text_type(obj)
if isinstance(message, Message):
# Even after unicoding() we still need to check if we are
# running with translatable unicode before translating
return message.translate(desired_locale)
return obj
def _translate_args(args, desired_locale=None):
"""Translates all the translatable elements of the given arguments object.
This method is used for translating the translatable values in method
arguments which include values of tuples or dictionaries.
If the object is not a tuple or a dictionary the object itself is
translated if it is translatable.
If the locale is None the object is translated to the system locale.
:param args: the args to translate
:param desired_locale: the locale to translate the args to, if None the
default system locale will be used
:returns: a new args object with the translated contents of the original
"""
if isinstance(args, tuple):
return tuple(translate(v, desired_locale) for v in args)
if isinstance(args, dict):
translated_dict = {}
for (k, v) in six.iteritems(args):
translated_v = translate(v, desired_locale)
translated_dict[k] = translated_v
return translated_dict
return translate(args, desired_locale)
class TranslationHandler(handlers.MemoryHandler):
"""Handler that translates records before logging them.
The TranslationHandler takes a locale and a target logging.Handler object
to forward LogRecord objects to after translating them. This handler
depends on Message objects being logged, instead of regular strings.
The handler can be configured declaratively in the logging.conf as follows:
[handlers]
keys = translatedlog, translator
[handler_translatedlog]
class = handlers.WatchedFileHandler
args = ('/var/log/api-localized.log',)
formatter = context
[handler_translator]
class = openstack.common.log.TranslationHandler
target = translatedlog
args = ('zh_CN',)
If the specified locale is not available in the system, the handler will
log in the default locale.
"""
def __init__(self, locale=None, target=None):
"""Initialize a TranslationHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
# NOTE(luisg): In order to allow this handler to be a wrapper for
# other handlers, such as a FileHandler, and still be able to
# configure it using logging.conf, this handler has to extend
# MemoryHandler because only the MemoryHandlers' logging.conf
# parsing is implemented such that it accepts a target handler.
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
self.locale = locale
def setFormatter(self, fmt):
self.target.setFormatter(fmt)
def emit(self, record):
# We save the message from the original record to restore it
# after translation, so other handlers are not affected by this
original_msg = record.msg
original_args = record.args
try:
self._translate_and_log_record(record)
finally:
record.msg = original_msg
record.args = original_args
def _translate_and_log_record(self, record):
record.msg = translate(record.msg, self.locale)
# In addition to translating the message, we also need to translate
# arguments that were passed to the log method that were not part
# of the main message e.g., log.info(_('Some message %s'), this_one))
record.args = _translate_args(record.args, self.locale)
self.target.emit(record)

View File

@@ -0,0 +1,66 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Import related utilities and helper functions.
"""
import sys
import traceback
def import_class(import_str):
"""Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.')
try:
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
except (ValueError, AttributeError):
raise ImportError('Class %s cannot be found (%s)' %
(class_str,
traceback.format_exception(*sys.exc_info())))
def import_object(import_str, *args, **kwargs):
"""Import a class and return an instance of it."""
return import_class(import_str)(*args, **kwargs)
def import_object_ns(name_space, import_str, *args, **kwargs):
"""Tries to import object from default namespace.
Imports a class and return an instance of it, first by trying
to find the class in a default namespace, then failing back to
a full path if not found in the default namespace.
"""
import_value = "%s.%s" % (name_space, import_str)
try:
return import_class(import_value)(*args, **kwargs)
except ImportError:
return import_class(import_str)(*args, **kwargs)
def import_module(import_str):
"""Import a module."""
__import__(import_str)
return sys.modules[import_str]
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try:
return import_module(import_str)
except ImportError:
return default

View File

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

View File

@@ -1,335 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Utilities with minimum-depends for use in setup.py
"""
import email
import os
import re
import subprocess
import sys
from setuptools.command import sdist
def parse_mailmap(mailmap='.mailmap'):
mapping = {}
if os.path.exists(mailmap):
with open(mailmap, 'r') as fp:
for l in fp:
try:
canonical_email, alias = re.match(
r'[^#]*?(<.+>).*(<.+>).*', l).groups()
except AttributeError:
continue
mapping[alias] = canonical_email
return mapping
def canonicalize_emails(changelog, mapping):
"""Takes in a string and an email alias mapping and replaces all
instances of the aliases in the string with their real email.
"""
for alias, email_address in mapping.iteritems():
changelog = changelog.replace(alias, email_address)
return changelog
# Get requirements from the first file that exists
def get_reqs_from_files(requirements_files):
for requirements_file in requirements_files:
if os.path.exists(requirements_file):
with open(requirements_file, 'r') as fil:
return fil.read().split('\n')
return []
def parse_requirements(requirements_files=['requirements.txt',
'tools/pip-requires']):
requirements = []
for line in get_reqs_from_files(requirements_files):
# For the requirements list, we need to inject only the portion
# after egg= so that distutils knows the package it's looking for
# such as:
# -e git://github.com/openstack/nova/master#egg=nova
if re.match(r'\s*-e\s+', line):
requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
line))
# such as:
# http://github.com/openstack/nova/zipball/master#egg=nova
elif re.match(r'\s*https?:', line):
requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
line))
# -f lines are for index locations, and don't get used here
elif re.match(r'\s*-f\s+', line):
pass
# argparse is part of the standard library starting with 2.7
# adding it to the requirements list screws distro installs
elif line == 'argparse' and sys.version_info >= (2, 7):
pass
else:
requirements.append(line)
return requirements
def parse_dependency_links(requirements_files=['requirements.txt',
'tools/pip-requires']):
dependency_links = []
# dependency_links inject alternate locations to find packages listed
# in requirements
for line in get_reqs_from_files(requirements_files):
# skip comments and blank lines
if re.match(r'(\s*#)|(\s*$)', line):
continue
# lines with -e or -f need the whole line, minus the flag
if re.match(r'\s*-[ef]\s+', line):
dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
# lines that are only urls can go in unmolested
elif re.match(r'\s*https?:', line):
dependency_links.append(line)
return dependency_links
def _run_shell_command(cmd, throw_on_error=False):
if os.name == 'nt':
output = subprocess.Popen(["cmd.exe", "/C", cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
else:
output = subprocess.Popen(["/bin/sh", "-c", cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if output.returncode and throw_on_error:
raise Exception("%s returned %d" % cmd, output.returncode)
out = output.communicate()
if len(out) == 0:
return None
if len(out[0].strip()) == 0:
return None
return out[0].strip()
def write_git_changelog():
"""Write a changelog based on the git changelog."""
new_changelog = 'ChangeLog'
if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
if os.path.isdir('.git'):
git_log_cmd = 'git log --stat'
changelog = _run_shell_command(git_log_cmd)
mailmap = parse_mailmap()
with open(new_changelog, "w") as changelog_file:
changelog_file.write(canonicalize_emails(changelog, mailmap))
else:
open(new_changelog, 'w').close()
def generate_authors():
"""Create AUTHORS file using git commits."""
jenkins_email = 'jenkins@review.(openstack|stackforge).org'
old_authors = 'AUTHORS.in'
new_authors = 'AUTHORS'
if not os.getenv('SKIP_GENERATE_AUTHORS'):
if os.path.isdir('.git'):
# don't include jenkins email address in AUTHORS file
git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
"egrep -v '" + jenkins_email + "'")
changelog = _run_shell_command(git_log_cmd)
mailmap = parse_mailmap()
with open(new_authors, 'w') as new_authors_fh:
new_authors_fh.write(canonicalize_emails(changelog, mailmap))
if os.path.exists(old_authors):
with open(old_authors, "r") as old_authors_fh:
new_authors_fh.write('\n' + old_authors_fh.read())
else:
open(new_authors, 'w').close()
_rst_template = """%(heading)s
%(underline)s
.. automodule:: %(module)s
:members:
:undoc-members:
:show-inheritance:
"""
def get_cmdclass():
"""Return dict of commands to run from setup.py."""
cmdclass = dict()
def _find_modules(arg, dirname, files):
for filename in files:
if filename.endswith('.py') and filename != '__init__.py':
arg["%s.%s" % (dirname.replace('/', '.'),
filename[:-3])] = True
class LocalSDist(sdist.sdist):
"""Builds the ChangeLog and Authors files from VC first."""
def run(self):
write_git_changelog()
generate_authors()
# sdist.sdist is an old style class, can't use super()
sdist.sdist.run(self)
cmdclass['sdist'] = LocalSDist
# If Sphinx is installed on the box running setup.py,
# enable setup.py to build the documentation, otherwise,
# just ignore it
try:
from sphinx.setup_command import BuildDoc
class LocalBuildDoc(BuildDoc):
builders = ['html', 'man']
def generate_autoindex(self):
print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
modules = {}
option_dict = self.distribution.get_option_dict('build_sphinx')
source_dir = os.path.join(option_dict['source_dir'][1], 'api')
if not os.path.exists(source_dir):
os.makedirs(source_dir)
for pkg in self.distribution.packages:
if '.' not in pkg:
os.path.walk(pkg, _find_modules, modules)
module_list = modules.keys()
module_list.sort()
autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
with open(autoindex_filename, 'w') as autoindex:
autoindex.write(""".. toctree::
:maxdepth: 1
""")
for module in module_list:
output_filename = os.path.join(source_dir,
"%s.rst" % module)
heading = "The :mod:`%s` Module" % module
underline = "=" * len(heading)
values = dict(module=module, heading=heading,
underline=underline)
print "Generating %s" % output_filename
with open(output_filename, 'w') as output_file:
output_file.write(_rst_template % values)
autoindex.write(" %s.rst\n" % module)
def run(self):
if not os.getenv('SPHINX_DEBUG'):
self.generate_autoindex()
for builder in self.builders:
self.builder = builder
self.finalize_options()
self.project = self.distribution.get_name()
self.version = self.distribution.get_version()
self.release = self.distribution.get_version()
BuildDoc.run(self)
class LocalBuildLatex(LocalBuildDoc):
builders = ['latex']
cmdclass['build_sphinx'] = LocalBuildDoc
cmdclass['build_sphinx_latex'] = LocalBuildLatex
except ImportError:
pass
return cmdclass
def _get_revno():
"""Return the number of commits since the most recent tag.
We use git-describe to find this out, but if there are no
tags then we fall back to counting commits since the beginning
of time.
"""
describe = _run_shell_command("git describe --always")
if "-" in describe:
return describe.rsplit("-", 2)[-2]
# no tags found
revlist = _run_shell_command("git rev-list --abbrev-commit HEAD")
return len(revlist.splitlines())
def get_version_from_git(pre_version):
"""Return a version which is equal to the tag that's on the current
revision if there is one, or tag plus number of additional revisions
if the current revision has no tag."""
if os.path.isdir('.git'):
if pre_version:
try:
return _run_shell_command(
"git describe --exact-match",
throw_on_error=True).replace('-', '.')
except Exception:
sha = _run_shell_command("git log -n1 --pretty=format:%h")
return "%s.a%s.g%s" % (pre_version, _get_revno(), sha)
else:
return _run_shell_command(
"git describe --always").replace('-', '.')
return None
def get_version_from_pkg_info(package_name):
"""Get the version from PKG-INFO file if we can."""
try:
pkg_info_file = open('PKG-INFO', 'r')
except (IOError, OSError):
return None
try:
pkg_info = email.message_from_file(pkg_info_file)
except email.MessageError:
return None
# Check to make sure we're in our own dir
if pkg_info.get('Name', None) != package_name:
return None
return pkg_info.get('Version', None)
def get_version(package_name, pre_version=None):
"""Get the version of the project. First, try getting it from PKG-INFO, if
it exists. If it does, that means we're in a distribution tarball or that
install has happened. Otherwise, if there is no PKG-INFO file, pull the
version from git.
We do not support setup.py version sanity in git archive tarballs, nor do
we support packagers directly sucking our git repo into theirs. We expect
that a source tarball be made from our git repo - or that if someone wants
to make a source tarball from a fork of our repo with additional tags in it
that they understand and desire the results of doing that.
"""
version = os.environ.get("OSLO_PACKAGE_VERSION", None)
if version:
return version
version = get_version_from_pkg_info(package_name)
if version:
return version
version = get_version_from_git(pre_version)
if version:
return version
raise Exception("Versioning for this project requires either an sdist"
" tarball, or access to an upstream git repository.")

View File

@@ -1,6 +1,4 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -19,15 +17,35 @@
System-level utilities and helper functions.
"""
import logging
import re
import sys
import unicodedata
LOG = logging.getLogger(__name__)
import six
from cinderclient.openstack.common.gettextutils import _
# Used for looking up extensions of text
# to their 'multiplied' byte amount
BYTE_MULTIPLIERS = {
'': 1,
't': 1024 ** 4,
'g': 1024 ** 3,
'm': 1024 ** 2,
'k': 1024,
}
BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
def int_from_bool_as_string(subject):
"""
Interpret a string as a boolean and return either 1 or 0.
"""Interpret a string as a boolean and return either 1 or 0.
Any string value in:
@@ -40,42 +58,53 @@ def int_from_bool_as_string(subject):
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject):
def bool_from_string(subject, strict=False, default=False):
"""Interpret a string as a boolean.
A case-insensitive match is performed such that strings matching 't',
'true', 'on', 'y', 'yes', or '1' are considered True and, when
`strict=False`, anything else returns the value specified by 'default'.
Useful for JSON-decoded stuff and config file parsing.
If `strict=True`, unrecognized values, including None, will raise a
ValueError which is useful when parsing values passed in from an API call.
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
"""
Interpret a string as a boolean.
if not isinstance(subject, six.string_types):
subject = str(subject)
Any string value in:
lowered = subject.strip().lower()
('True', 'true', 'On', 'on', 'Yes', 'yes', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
if isinstance(subject, bool):
return subject
if isinstance(subject, basestring):
if subject.strip().lower() in ('true', 'on', 'yes', '1'):
return True
return False
if lowered in TRUE_STRINGS:
return True
elif lowered in FALSE_STRINGS:
return False
elif strict:
acceptable = ', '.join(
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
msg = _("Unrecognized value '%(val)s', acceptable values are:"
" %(acceptable)s") % {'val': subject,
'acceptable': acceptable}
raise ValueError(msg)
else:
return default
def safe_decode(text, incoming=None, errors='strict'):
"""
Decodes incoming str using `incoming` if they're
not already unicode.
"""Decodes incoming str using `incoming` if they're not already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
:raises TypeError: If text is not an isntance of basestring
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, basestring):
if not isinstance(text, six.string_types):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, unicode):
if isinstance(text, six.text_type):
return text
if not incoming:
@@ -102,11 +131,10 @@ def safe_decode(text, incoming=None, errors='strict'):
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""
Encodes incoming str/unicode using `encoding`. If
incoming is not specified, text is expected to
be encoded with current python's default encoding.
(`sys.getdefaultencoding`)
"""Encodes incoming str/unicode using `encoding`.
If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`)
:param incoming: Text's current encoding
:param encoding: Expected encoding for text (Default UTF-8)
@@ -114,20 +142,81 @@ def safe_encode(text, incoming=None,
values http://docs.python.org/2/library/codecs.html
:returns: text or a bytestring `encoding` encoded
representation of it.
:raises TypeError: If text is not an isntance of basestring
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, basestring):
if not isinstance(text, six.string_types):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
if isinstance(text, unicode):
return text.encode(encoding, errors)
if isinstance(text, six.text_type):
if six.PY3:
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
return text.encode(encoding, errors)
if six.PY3:
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
return text
def to_bytes(text, default=0):
"""Converts a string into an integer of bytes.
Looks at the last characters of the text to determine
what conversion is needed to turn the input text into a byte number.
Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive)
:param text: String input for bytes size conversion.
:param default: Default return value when text is blank.
"""
match = BYTE_REGEX.search(text)
if match:
magnitude = int(match.group(1))
mult_key_org = match.group(2)
if not mult_key_org:
return magnitude
elif text:
msg = _('Invalid string format: %s') % text
raise TypeError(msg)
else:
return default
mult_key = mult_key_org.lower().replace('b', '', 1)
multiplier = BYTE_MULTIPLIERS.get(mult_key)
if multiplier is None:
msg = _('Unknown byte multiplier: %s') % mult_key_org
raise TypeError(msg)
return magnitude * multiplier
def to_slug(value, incoming=None, errors="strict"):
"""Normalize string.
Convert to lowercase, remove non-word characters, and convert spaces
to hyphens.
Inspired by Django's `slugify` filter.
:param value: Text to slugify
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: slugified unicode representation of `value`
:raises TypeError: If text is not an instance of str
"""
value = safe_decode(value, incoming, errors)
# NOTE(aababilov): no need to use safe_(encode|decode) here:
# encodings are always "ascii", error handling is always "ignore"
# and types are always known (first: unicode; second: str)
value = unicodedata.normalize("NFKD", value).encode(
"ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value)

View File

@@ -1,86 +0,0 @@
# Copyright 2012 OpenStack LLC
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Utilities for consuming the version from pkg_resources.
"""
import pkg_resources
class VersionInfo(object):
def __init__(self, package):
"""Object that understands versioning for a package
:param package: name of the python package, such as glance, or
python-glanceclient
"""
self.package = package
self.release = None
self.version = None
self._cached_version = None
def _get_version_from_pkg_resources(self):
"""Get the version of the package from the pkg_resources record
associated with the package."""
try:
requirement = pkg_resources.Requirement.parse(self.package)
provider = pkg_resources.get_provider(requirement)
return provider.version
except pkg_resources.DistributionNotFound:
# The most likely cause for this is running tests in a tree with
# produced from a tarball where the package itself has not been
# installed into anything. Check for a PKG-INFO file.
from cinderclient.openstack.common import setup
return setup.get_version_from_pkg_info(self.package)
def release_string(self):
"""Return the full version of the package including suffixes indicating
VCS status.
"""
if self.release is None:
self.release = self._get_version_from_pkg_resources()
return self.release
def version_string(self):
"""Return the short version minus any alpha/beta tags."""
if self.version is None:
parts = []
for part in self.release_string().split('.'):
if part[0].isdigit():
parts.append(part)
else:
break
self.version = ".".join(parts)
return self.version
# Compatibility functions
canonical_version_string = version_string
version_string_with_vcs = release_string
def cached_version_string(self, prefix=""):
"""Generate an object which will expand in a string context to
the results of version_string(). We do this so that don't
call into pkg_resources every time we start up a program when
passing version information into the CONF constructor, but
rather only do the calculation when and if a version is requested
"""
if not self._cached_version:
self._cached_version = "%s%s" % (prefix,
self.version_string())
return self._cached_version

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# Copyright 2011, Piston Cloud Computing, Inc.
#
# All Rights Reserved.
@@ -33,7 +33,8 @@ class ServiceCatalog(object):
service_name=None, volume_service_name=None):
"""Fetch the public URL from the Compute service for
a particular endpoint attribute. If none given, return
the first. See tests for sample service catalog."""
the first. See tests for sample service catalog.
"""
matching_endpoints = []
if 'endpoints' in self.catalog:
# We have a bastardized service catalog. Treat it special. :/
@@ -44,22 +45,29 @@ class ServiceCatalog(object):
raise cinderclient.exceptions.EndpointNotFound()
# We don't always get a service catalog back ...
if not 'serviceCatalog' in self.catalog['access']:
if 'serviceCatalog' not in self.catalog['access']:
return None
# Full catalog ...
catalog = self.catalog['access']['serviceCatalog']
for service in catalog:
if service.get("type") != service_type:
# NOTE(thingee): For backwards compatibility, if they have v2
# enabled and the service_type is set to 'volume', go ahead and
# accept that.
skip_service_type_check = False
if service_type == 'volumev2' and service['type'] == 'volume':
version = service['endpoints'][0]['publicURL'].split('/')[3]
if version == 'v2':
skip_service_type_check = True
if (not skip_service_type_check
and service.get("type") != service_type):
continue
if (service_name and service_type == 'compute' and
service.get('name') != service_name):
continue
if (volume_service_name and service_type == 'volume' and
service.get('name') != volume_service_name):
if (volume_service_name and service_type in ('volume', 'volumev2')
and service.get('name') != volume_service_name):
continue
endpoints = service['endpoints']

View File

@@ -18,6 +18,8 @@
Command-line interface to the OpenStack Cinder API.
"""
from __future__ import print_function
import argparse
import glob
import imp
@@ -37,8 +39,9 @@ from cinderclient.v2 import shell as shell_v2
DEFAULT_OS_VOLUME_API_VERSION = "1"
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
DEFAULT_CINDER_SERVICE_TYPE = 'compute'
DEFAULT_CINDER_SERVICE_TYPE = 'volume'
logging.basicConfig()
logger = logging.getLogger(__name__)
@@ -115,6 +118,14 @@ class OpenStackCinderShell(object):
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',
@@ -133,7 +144,7 @@ class OpenStackCinderShell(object):
parser.add_argument('--service-type',
metavar='<service-type>',
help='Defaults to compute for most actions')
help='Defaults to volume for most actions')
parser.add_argument('--service_type',
help=argparse.SUPPRESS)
@@ -161,9 +172,9 @@ class OpenStackCinderShell(object):
help=argparse.SUPPRESS)
parser.add_argument('--os-volume-api-version',
metavar='<compute-api-ver>',
metavar='<volume-api-ver>',
default=utils.env('OS_VOLUME_API_VERSION',
default=DEFAULT_OS_VOLUME_API_VERSION),
default=None),
help='Accepts 1 or 2,defaults '
'to env[OS_VOLUME_API_VERSION].')
parser.add_argument('--os_volume_api_version',
@@ -287,7 +298,7 @@ class OpenStackCinderShell(object):
def _find_actions(self, subparsers, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
# I prefer to be hyphen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
@@ -317,7 +328,7 @@ class OpenStackCinderShell(object):
streamhandler = logging.StreamHandler()
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
streamhandler.setFormatter(logging.Formatter(streamformat))
logger.setLevel(logging.DEBUG)
logger.setLevel(logging.WARNING)
logger.addHandler(streamhandler)
def main(self, argv):
@@ -325,6 +336,14 @@ class OpenStackCinderShell(object):
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.setup_debugging(options.debug)
api_version_input = True
if not options.os_volume_api_version:
# Environment variable OS_VOLUME_API_VERSION was
# not set and '--os-volume-api-version' option doesn't
# specify a value. Fall back to default.
options.os_volume_api_version = DEFAULT_OS_VOLUME_API_VERSION
api_version_input = False
# build available subcommands based on version
self.extensions = self._discover_extensions(
@@ -351,13 +370,14 @@ class OpenStackCinderShell(object):
return 0
(os_username, os_password, os_tenant_name, os_auth_url,
os_region_name, endpoint_type, insecure,
os_region_name, os_tenant_id, endpoint_type, insecure,
service_type, service_name, volume_service_name,
username, apikey, projectid, url, region_name, cacert) = (
args.os_username, args.os_password,
args.os_tenant_name, args.os_auth_url,
args.os_region_name, args.endpoint_type,
args.insecure, args.service_type, args.service_name,
args.os_region_name, args.os_tenant_id,
args.endpoint_type, args.insecure,
args.service_type, args.service_name,
args.volume_service_name, args.username,
args.apikey, args.projectid,
args.url, args.region_name, args.os_cacert)
@@ -389,11 +409,11 @@ class OpenStackCinderShell(object):
else:
os_password = apikey
if not os_tenant_name:
if not (os_tenant_name or os_tenant_id):
if not projectid:
raise exc.CommandError("You must provide a tenant name "
"via either --os-tenant-name or "
"env[OS_TENANT_NAME]")
raise exc.CommandError("You must provide a tenant_id "
"via either --os-tenant-id or "
"env[OS_TENANT_ID]")
else:
os_tenant_name = projectid
@@ -408,10 +428,10 @@ class OpenStackCinderShell(object):
if not os_region_name and region_name:
os_region_name = region_name
if not os_tenant_name:
if not (os_tenant_name or os_tenant_id):
raise exc.CommandError(
"You must provide a tenant name "
"via either --os-tenant-name or env[OS_TENANT_NAME]")
"You must provide a tenant_id "
"via either --os-tenant-id or env[OS_TENANT_ID]")
if not os_auth_url:
raise exc.CommandError(
@@ -421,6 +441,7 @@ class OpenStackCinderShell(object):
self.cs = client.Client(options.os_volume_api_version, os_username,
os_password, os_tenant_name, os_auth_url,
insecure, region_name=os_region_name,
tenant_id=os_tenant_id,
endpoint_type=endpoint_type,
extensions=self.extensions,
service_type=service_type,
@@ -438,6 +459,34 @@ class OpenStackCinderShell(object):
except exc.AuthorizationFailure:
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
# back to trying to use what the user specified via
# --os-volume-api-version or with the OS_VOLUME_API_VERSION environment
# variable. Fail safe is to use the default API setting.
try:
endpoint_api_version = \
self.cs.get_volume_api_version_from_endpoint()
if endpoint_api_version != options.os_volume_api_version:
msg = (("Volume API version is set to %s "
"but you are accessing a %s endpoint. "
"Change its value via either --os-volume-api-version "
"or env[OS_VOLUME_API_VERSION]")
% (options.os_volume_api_version, endpoint_api_version))
raise exc.InvalidAPIVersion(msg)
except exc.UnsupportedVersion:
endpoint_api_version = options.os_volume_api_version
if api_version_input:
logger.warning("Unable to determine the API version via "
"endpoint URL. Falling back to user "
"specified version: %s" %
endpoint_api_version)
else:
logger.warning("Unable to determine the API version from "
"endpoint URL or user input. Falling back to "
"default API version: %s" %
endpoint_api_version)
args.func(self.cs, args)
def _run_extension_hooks(self, hook_type, *args, **kwargs):
@@ -446,20 +495,21 @@ class OpenStackCinderShell(object):
extension.run_hooks(hook_type, *args, **kwargs)
def do_bash_completion(self, args):
"""
"""Print arguments for bash_completion.
Prints all of the commands and options to stdout so that the
cinder.bash_completion script doesn't have to hard code them.
"""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
for sc_str, sc in list(self.subcommands.items()):
commands.add(sc_str)
for option in sc._optionals._option_string_actions.keys():
for option in sc._optionals._option_string_actions:
options.add(option)
commands.remove('bash-completion')
commands.remove('bash_completion')
print ' '.join(commands | options)
print(' '.join(commands | options))
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
@@ -487,16 +537,17 @@ class OpenStackHelpFormatter(argparse.HelpFormatter):
def main():
try:
OpenStackCinderShell().main(map(strutils.safe_decode, sys.argv[1:]))
if sys.version_info >= (3, 0):
OpenStackCinderShell().main(sys.argv[1:])
else:
OpenStackCinderShell().main(map(strutils.safe_decode,
sys.argv[1:]))
except KeyboardInterrupt:
print >> sys.stderr, "... terminating cinder client"
print("... terminating cinder client", file=sys.stderr)
sys.exit(130)
except Exception, e:
except Exception as e:
logger.debug(e, exc_info=1)
message = e.message
if not isinstance(message, basestring):
message = str(message)
print >> sys.stderr, "ERROR: %s" % strutils.safe_encode(message)
print("ERROR: %s" % strutils.six.text_type(e), file=sys.stderr)
sys.exit(1)

View File

@@ -1,3 +1,16 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
A fake server that "responds" to API methods with pre-canned responses.
@@ -6,14 +19,15 @@ wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
from __future__ import print_function
def assert_has_keys(dict, required=[], optional=[]):
keys = dict.keys()
for k in required:
try:
assert k in keys
assert k in dict
except AssertionError:
extra_keys = set(keys).difference(set(required + optional))
extra_keys = set(dict).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
@@ -52,19 +66,17 @@ class FakeClient(object):
break
assert found, 'Expected %s %s; got %s' % (
expected, self.client.callstack)
expected + (self.client.callstack, ))
if body is not None:
try:
assert entry[2] == body
except AssertionError:
print entry[2]
print "!="
print body
print(entry[2])
print("!=")
print(body)
raise
self.client.callstack = []
def clear_callstack(self):
self.client.callstack = []

View File

@@ -1,8 +1,21 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient import base
from cinderclient import exceptions
from cinderclient.v1 import volumes
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -1,8 +1,21 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import cinderclient.client
import cinderclient.v1.client
import cinderclient.v2.client
from tests import utils
from cinderclient.tests import utils
class ClientTest(utils.TestCase):

View File

@@ -1,10 +1,23 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import requests
from cinderclient import client
from cinderclient import exceptions
from tests import utils
from cinderclient.tests import utils
fake_response = utils.TestResponse({

View File

@@ -0,0 +1,275 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient import exceptions
from cinderclient import service_catalog
from cinderclient.tests import utils
# Taken directly from keystone/content/common/samples/auth.json
# Do not edit this structure. Instead, grab the latest from there.
SERVICE_CATALOG = {
"access": {
"token": {
"id": "ab48a9efdfedb23ty3494",
"expires": "2010-11-01T03:32:15-05:00",
"tenant": {
"id": "345",
"name": "My Project"
}
},
"user": {
"id": "123",
"name": "jqsmith",
"roles": [
{
"id": "234",
"name": "compute:admin",
},
{
"id": "235",
"name": "object-store:admin",
"tenantId": "1",
}
],
"roles_links": [],
},
"serviceCatalog": [
{
"name": "Cloud Servers",
"type": "compute",
"endpoints": [
{
"tenantId": "1",
"publicURL": "https://compute1.host/v1/1234",
"internalURL": "https://compute1.host/v1/1234",
"region": "North",
"versionId": "1.0",
"versionInfo": "https://compute1.host/v1/",
"versionList": "https://compute1.host/"
},
{
"tenantId": "2",
"publicURL": "https://compute1.host/v1/3456",
"internalURL": "https://compute1.host/v1/3456",
"region": "North",
"versionId": "1.1",
"versionInfo": "https://compute1.host/v1/",
"versionList": "https://compute1.host/"
},
],
"endpoints_links": [],
},
{
"name": "Cinder Volume Service",
"type": "volume",
"endpoints": [
{
"tenantId": "1",
"publicURL": "https://volume1.host/v1/1234",
"internalURL": "https://volume1.host/v1/1234",
"region": "South",
"versionId": "1.0",
"versionInfo": "uri",
"versionList": "uri"
},
{
"tenantId": "2",
"publicURL": "https://volume1.host/v1/3456",
"internalURL": "https://volume1.host/v1/3456",
"region": "South",
"versionId": "1.1",
"versionInfo": "https://volume1.host/v1/",
"versionList": "https://volume1.host/"
},
],
"endpoints_links": [
{
"rel": "next",
"href": "https://identity1.host/v2.0/endpoints"
},
],
},
{
"name": "Cinder Volume Service V2",
"type": "volumev2",
"endpoints": [
{
"tenantId": "1",
"publicURL": "https://volume1.host/v2/1234",
"internalURL": "https://volume1.host/v2/1234",
"region": "South",
"versionId": "2.0",
"versionInfo": "uri",
"versionList": "uri"
},
{
"tenantId": "2",
"publicURL": "https://volume1.host/v2/3456",
"internalURL": "https://volume1.host/v2/3456",
"region": "South",
"versionId": "1.1",
"versionInfo": "https://volume1.host/v2/",
"versionList": "https://volume1.host/"
},
],
"endpoints_links": [
{
"rel": "next",
"href": "https://identity1.host/v2.0/endpoints"
},
],
},
],
"serviceCatalog_links": [
{
"rel": "next",
"href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar",
},
],
},
}
SERVICE_COMPATIBILITY_CATALOG = {
"access": {
"token": {
"id": "ab48a9efdfedb23ty3494",
"expires": "2010-11-01T03:32:15-05:00",
"tenant": {
"id": "345",
"name": "My Project"
}
},
"user": {
"id": "123",
"name": "jqsmith",
"roles": [
{
"id": "234",
"name": "compute:admin",
},
{
"id": "235",
"name": "object-store:admin",
"tenantId": "1",
}
],
"roles_links": [],
},
"serviceCatalog": [
{
"name": "Cloud Servers",
"type": "compute",
"endpoints": [
{
"tenantId": "1",
"publicURL": "https://compute1.host/v1/1234",
"internalURL": "https://compute1.host/v1/1234",
"region": "North",
"versionId": "1.0",
"versionInfo": "https://compute1.host/v1/",
"versionList": "https://compute1.host/"
},
{
"tenantId": "2",
"publicURL": "https://compute1.host/v1/3456",
"internalURL": "https://compute1.host/v1/3456",
"region": "North",
"versionId": "1.1",
"versionInfo": "https://compute1.host/v1/",
"versionList": "https://compute1.host/"
},
],
"endpoints_links": [],
},
{
"name": "Cinder Volume Service V2",
"type": "volume",
"endpoints": [
{
"tenantId": "1",
"publicURL": "https://volume1.host/v2/1234",
"internalURL": "https://volume1.host/v2/1234",
"region": "South",
"versionId": "2.0",
"versionInfo": "uri",
"versionList": "uri"
},
{
"tenantId": "2",
"publicURL": "https://volume1.host/v2/3456",
"internalURL": "https://volume1.host/v2/3456",
"region": "South",
"versionId": "1.1",
"versionInfo": "https://volume1.host/v2/",
"versionList": "https://volume1.host/"
},
],
"endpoints_links": [
{
"rel": "next",
"href": "https://identity1.host/v2.0/endpoints"
},
],
},
],
"serviceCatalog_links": [
{
"rel": "next",
"href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar",
},
],
},
}
class ServiceCatalogTest(utils.TestCase):
def test_building_a_service_catalog(self):
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
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.assertRaises(exceptions.EndpointNotFound, sc.url_for,
"region", "South", service_type='compute')
def test_alternate_service_type(self):
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
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(sc.url_for('tenantId', '2', service_type='volumev2'),
"https://volume1.host/v2/3456")
self.assertEqual(sc.url_for('tenantId', '2', service_type='volumev2'),
"https://volume1.host/v2/3456")
self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
"region", "North", service_type='volume')
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")

View File

@@ -1,14 +1,26 @@
import cStringIO
import os
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import sys
import fixtures
from six import moves
from testtools import matchers
from cinderclient import exceptions
import cinderclient.shell
from tests import utils
from cinderclient.tests import utils
class ShellTest(utils.TestCase):
@@ -30,7 +42,7 @@ class ShellTest(utils.TestCase):
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = cStringIO.StringIO()
sys.stdout = moves.StringIO()
_shell = cinderclient.shell.OpenStackCinderShell()
_shell.main(argstr.split())
except SystemExit:

View File

@@ -1,8 +1,25 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import sys
from six import moves
from cinderclient import exceptions
from cinderclient import utils
from cinderclient import base
from tests import utils as test_utils
from cinderclient.tests import utils as test_utils
UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
@@ -38,7 +55,7 @@ class FakeManager(base.ManagerWithFind):
return resource
raise exceptions.NotFound(resource_id)
def list(self):
def list(self, search_opts):
return self.resources
@@ -73,3 +90,51 @@ class FindResourceTestCase(test_utils.TestCase):
def test_find_by_str_displayname(self):
output = utils.find_resource(self.manager, 'entity_three')
self.assertEqual(output, self.manager.get('4242'))
class CaptureStdout(object):
"""Context manager for capturing stdout from statments in its's block."""
def __enter__(self):
self.real_stdout = sys.stdout
self.stringio = moves.StringIO()
sys.stdout = self.stringio
return self
def __exit__(self, *args):
sys.stdout = self.real_stdout
self.stringio.seek(0)
self.read = self.stringio.read
class PrintListTestCase(test_utils.TestCase):
def test_print_list_with_list(self):
Row = collections.namedtuple('Row', ['a', 'b'])
to_print = [Row(a=1, b=2), Row(a=3, b=4)]
with CaptureStdout() as cso:
utils.print_list(to_print, ['a', 'b'])
self.assertEqual(cso.read(), """\
+---+---+
| a | b |
+---+---+
| 1 | 2 |
| 3 | 4 |
+---+---+
""")
def test_print_list_with_generator(self):
Row = collections.namedtuple('Row', ['a', 'b'])
def gen_rows():
for row in [Row(a=1, b=2), Row(a=3, b=4)]:
yield row
with CaptureStdout() as cso:
utils.print_list(gen_rows(), ['a', 'b'])
self.assertEqual(cso.read(), """\
+---+---+
| a | b |
+---+---+
| 1 | 2 |
| 3 | 4 |
+---+---+
""")

View File

@@ -1,3 +1,16 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import fixtures
@@ -23,8 +36,9 @@ class TestCase(testtools.TestCase):
class TestResponse(requests.Response):
""" Class used to wrap requests.Response and provide some
convenience to initialize with a dict """
"""Class used to wrap requests.Response and provide some
convenience to initialize with a dict.
"""
def __init__(self, data):
self._text = None

View File

@@ -0,0 +1,34 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient import extension
from cinderclient.v1.contrib import list_extensions
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
extensions = [
extension.Extension(list_extensions.__name__.split(".")[-1],
list_extensions),
]
cs = fakes.FakeClient(extensions=extensions)
class ListExtensionsTests(utils.TestCase):
def test_list_extensions(self):
all_exts = cs.list_extensions.show_all()
cs.assert_called('GET', '/extensions')
self.assertTrue(len(all_exts) > 0)
for r in all_exts:
self.assertTrue(len(r.summary) > 0)

View File

@@ -0,0 +1,779 @@
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
# Copyright (c) 2011 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# 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
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from cinderclient import client as base_client
from cinderclient.tests import fakes
import cinderclient.tests.utils as utils
from cinderclient.v1 import client
def _stub_volume(**kwargs):
volume = {
'id': '1234',
'display_name': None,
'display_description': None,
"attachments": [],
"bootable": "false",
"availability_zone": "cinder",
"created_at": "2012-08-27T00:00:00.000000",
"id": '00000000-0000-0000-0000-000000000000',
"metadata": {},
"size": 1,
"snapshot_id": None,
"status": "available",
"volume_type": "None",
}
volume.update(kwargs)
return volume
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
def _self_href(base_uri, tenant_id, backup_id):
return '%s/v1/%s/backups/%s' % (base_uri, tenant_id, backup_id)
def _bookmark_href(base_uri, tenant_id, backup_id):
return '%s/%s/backups/%s' % (base_uri, tenant_id, backup_id)
def _stub_backup_full(id, base_uri, tenant_id):
return {
'id': id,
'name': 'backup',
'description': 'nightly backup',
'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b',
'container': 'volumebackups',
'object_count': 220,
'size': 10,
'availability_zone': 'az1',
'created_at': '2013-04-12T08:16:37.000000',
'status': 'available',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_backup(id, base_uri, tenant_id):
return {
'id': id,
'name': 'backup',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_restore():
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None):
if not name:
name = 'fake-name'
if not specs:
specs = {}
return {
'qos_specs': {
'id': id,
'name': name,
'consumer': 'back-end',
'specs': specs,
},
'links': {
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
}
def _stub_qos_associates(id, name):
return {
'assoications_type': 'volume_type',
'name': name,
'id': id,
}
def _stub_transfer_full(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'created_at': '2013-04-12T08:16:37.000000',
'auth_key': '123456',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_transfer(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_extend(id, new_size):
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
client.Client.__init__(self, 'username', 'password',
'project_id', 'auth_url',
extensions=kwargs.get('extensions'))
self.client = FakeHTTPClient(**kwargs)
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()
class FakeHTTPClient(base_client.HTTPClient):
def __init__(self, **kwargs):
self.username = 'username'
self.password = 'password'
self.auth_url = 'auth_url'
self.callstack = []
self.management_url = 'http://10.0.2.15:8776/v1/fake'
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert 'body' not in kwargs
elif method == 'PUT':
assert 'body' in kwargs
# Call the method
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
status, headers, body = getattr(self, callback)(**kwargs)
r = utils.TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})
return r, body
if hasattr(status, 'items'):
return utils.TestResponse(status), body
else:
return utils.TestResponse({"status": status}), body
def get_volume_api_version_from_endpoint(self):
magic_tuple = urlparse.urlsplit(self.management_url)
scheme, netloc, path, query, frag = magic_tuple
return path.lstrip('/').split('/')[0][1:]
#
# Snapshots
#
def get_snapshots_detail(self, **kw):
return (200, {}, {'snapshots': [
_stub_snapshot(),
]})
def get_snapshots_1234(self, **kw):
return (200, {}, {'snapshot': _stub_snapshot(id='1234')})
def get_snapshots_5678(self, **kw):
return (200, {}, {'snapshot': _stub_snapshot(id='5678')})
def put_snapshots_1234(self, **kw):
snapshot = _stub_snapshot(id='1234')
snapshot.update(kw['body']['snapshot'])
return (200, {}, {'snapshot': snapshot})
def post_snapshots_1234_action(self, body, **kw):
_body = None
resp = 202
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 (resp, {}, _body)
def post_snapshots_5678_action(self, body, **kw):
return self.post_snapshots_1234_action(body, **kw)
def delete_snapshots_1234(self, **kw):
return (202, {}, {})
#
# Volumes
#
def put_volumes_1234(self, **kw):
volume = _stub_volume(id='1234')
volume.update(kw['body']['volume'])
return (200, {}, {'volume': volume})
def get_volumes(self, **kw):
return (200, {}, {"volumes": [
{'id': 1234, 'name': 'sample-volume'},
{'id': 5678, 'name': 'sample-volume2'}
]})
# TODO(jdg): This will need to change
# at the very least it's not complete
def get_volumes_detail(self, **kw):
return (200, {}, {"volumes": [
{'id': kw.get('id', 1234),
'name': 'sample-volume',
'attachments': [{'server_id': 1234}]},
]})
def get_volumes_1234(self, **kw):
r = {'volume': self.get_volumes_detail(id=1234)[2]['volumes'][0]}
return (200, {}, r)
def get_volumes_5678(self, **kw):
r = {'volume': self.get_volumes_detail(id=5678)[2]['volumes'][0]}
return (200, {}, r)
def get_volumes_1234_encryption(self, **kw):
r = {'encryption_key_id': 'id'}
return (200, {}, r)
def post_volumes_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(list(body)) == 1
action = list(body)[0]
if action == 'os-attach':
assert sorted(list(body[action])) == ['instance_uuid',
'mode',
'mountpoint']
elif action == 'os-detach':
assert body[action] is None
elif action == 'os-reserve':
assert body[action] is None
elif action == 'os-unreserve':
assert body[action] is None
elif action == 'os-initialize_connection':
assert list(body[action]) == ['connector']
return (202, {}, {'connection_info': 'foos'})
elif action == 'os-terminate_connection':
assert list(body[action]) == ['connector']
elif action == 'os-begin_detaching':
assert body[action] is None
elif action == 'os-roll_detaching':
assert body[action] is None
elif action == 'os-reset_status':
assert 'status' in body[action]
elif action == 'os-extend':
assert list(body[action]) == ['new_size']
elif action == 'os-migrate_volume':
assert 'host' in body[action]
assert 'force_host_copy' in body[action]
elif action == 'os-update_readonly_flag':
assert list(body[action]) == ['readonly']
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
def post_volumes_5678_action(self, body, **kw):
return self.post_volumes_1234_action(body, **kw)
def post_volumes(self, **kw):
return (202, {}, {'volume': {}})
def delete_volumes_1234(self, **kw):
return (202, {}, None)
def delete_volumes_5678(self, **kw):
return (202, {}, None)
#
# Quotas
#
def get_os_quota_sets_test(self, **kw):
return (200, {}, {'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
def get_os_quota_sets_test_defaults(self):
return (200, {}, {'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
def put_os_quota_sets_test(self, body, **kw):
assert list(body) == ['quota_set']
fakes.assert_has_keys(body['quota_set'],
required=['tenant_id'])
return (200, {}, {'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
#
# Quota Classes
#
def get_os_quota_class_sets_test(self, **kw):
return (200, {}, {'quota_class_set': {
'class_name': 'test',
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set']
fakes.assert_has_keys(body['quota_class_set'],
required=['class_name'])
return (200, {}, {'quota_class_set': {
'class_name': 'test',
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
#
# VolumeTypes
#
def get_types(self, **kw):
return (200, {}, {
'volume_types': [{'id': 1,
'name': 'test-type-1',
'extra_specs': {}},
{'id': 2,
'name': 'test-type-2',
'extra_specs': {}}]})
def get_types_1(self, **kw):
return (200, {}, {'volume_type': {'id': 1,
'name': 'test-type-1',
'extra_specs': {}}})
def get_types_2(self, **kw):
return (200, {}, {'volume_type': {'id': 2,
'name': 'test-type-2',
'extra_specs': {}}})
def post_types(self, body, **kw):
return (202, {}, {'volume_type': {'id': 3,
'name': 'test-type-3',
'extra_specs': {}}})
def post_types_1_extra_specs(self, body, **kw):
assert list(body) == ['extra_specs']
return (200, {}, {'extra_specs': {'k': 'v'}})
def delete_types_1_extra_specs_k(self, **kw):
return(204, {}, None)
def delete_types_1(self, **kw):
return (202, {}, None)
#
# VolumeEncryptionTypes
#
def get_types_1_encryption(self, **kw):
return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test',
'cipher': 'test', 'key_size': 1,
'control_location': 'front'})
def get_types_2_encryption(self, **kw):
return (200, {}, {})
def post_types_2_encryption(self, body, **kw):
return (200, {}, {'encryption': {}})
def put_types_1_encryption_1(self, body, **kw):
return (200, {}, {})
def delete_types_1_encryption_provider(self, **kw):
return (202, {}, None)
#
# Set/Unset metadata
#
def delete_volumes_1234_metadata_test_key(self, **kw):
return (204, {}, None)
def delete_volumes_1234_metadata_key1(self, **kw):
return (204, {}, None)
def delete_volumes_1234_metadata_key2(self, **kw):
return (204, {}, None)
def post_volumes_1234_metadata(self, **kw):
return (204, {}, {'metadata': {'test_key': 'test_value'}})
#
# List all extensions
#
def get_extensions(self, **kw):
exts = [
{
"alias": "FAKE-1",
"description": "Fake extension number 1",
"links": [],
"name": "Fake1",
"namespace": ("http://docs.openstack.org/"
"/ext/fake1/api/v1.1"),
"updated": "2011-06-09T00:00:00+00:00"
},
{
"alias": "FAKE-2",
"description": "Fake extension number 2",
"links": [],
"name": "Fake2",
"namespace": ("http://docs.openstack.org/"
"/ext/fake1/api/v1.1"),
"updated": "2011-06-09T00:00:00+00:00"
},
]
return (200, {}, {"extensions": exts, })
#
# VolumeBackups
#
def get_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
return (200, {},
{'backup': _stub_backup_full(backup1, base_uri, tenant_id)})
def get_backups_detail(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
backup2 = 'd09534c6-08b8-4441-9e87-8976f3a8f699'
return (200, {},
{'backups': [
_stub_backup_full(backup1, base_uri, tenant_id),
_stub_backup_full(backup2, base_uri, tenant_id)]})
def delete_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw):
return (202, {}, None)
def post_backups(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
return (202, {},
{'backup': _stub_backup(backup1, base_uri, tenant_id)})
def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw):
return (200, {},
{'restore': _stub_restore()})
def post_backups_1234_restore(self, **kw):
return (200, {},
{'restore': _stub_restore()})
#
# QoSSpecs
#
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
return (200, {},
_stub_qos_full(qos_id1, base_uri, tenant_id))
def get_qos_specs(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B'
return (200, {},
{'qos_specs': [
_stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'),
_stub_qos_full(qos_id2, base_uri, tenant_id)]})
def post_qos_specs(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
qos_name = 'qos-name'
return (202, {},
_stub_qos_full(qos_id, base_uri, tenant_id, qos_name))
def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw):
return (202, {}, None)
def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys(
self, **kw):
return (202, {}, None)
def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw):
return (202, {}, None)
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations(
self, **kw):
type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF'
type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF'
type_name1 = 'type1'
type_name2 = 'type2'
return (202, {},
{'qos_associations': [
_stub_qos_associates(type_id1, type_name1),
_stub_qos_associates(type_id2, type_name2)]})
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate(
self, **kw):
return (202, {}, None)
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate(
self, **kw):
return (202, {}, None)
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all(
self, **kw):
return (202, {}, None)
#
# VolumeTransfers
#
def get_os_volume_transfer_5678(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer':
_stub_transfer_full(transfer1, base_uri, tenant_id)})
def get_os_volume_transfer_detail(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41'
return (200, {},
{'transfers': [
_stub_transfer_full(transfer1, base_uri, tenant_id),
_stub_transfer_full(transfer2, base_uri, tenant_id)]})
def delete_os_volume_transfer_5678(self, **kw):
return (202, {}, None)
def post_os_volume_transfer(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (202, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
def post_os_volume_transfer_5678_accept(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
#
# Services
#
def get_os_services(self, **kw):
host = kw.get('host', None)
binary = kw.get('binary', None)
services = [
{
'binary': 'cinder-volume',
'host': 'host1',
'zone': 'cinder',
'status': 'enabled',
'state': 'up',
'updated_at': datetime(2012, 10, 29, 13, 42, 2)
},
{
'binary': 'cinder-volume',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
{
'binary': 'cinder-scheduler',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
]
if host:
services = filter(lambda i: i['host'] == host, services)
if binary:
services = filter(lambda i: i['binary'] == binary, services)
return (200, {}, {'services': services})
def put_os_services_enable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'enabled'})
def put_os_services_disable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled'})
def get_os_availability_zone(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": None,
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})
def get_os_availability_zone_detail(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-volume": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 25, 0)
}
}
}
},
{
"zoneName": "internal",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-sched": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 24, 0)
}
}
}
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})
def post_snapshots_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
def delete_snapshots_1234_metadata_key1(self, **kw):
return (200, {}, None)
def delete_snapshots_1234_metadata_key2(self, **kw):
return (200, {}, None)
def put_volumes_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
def put_snapshots_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})

View File

@@ -1,3 +1,16 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import mock
@@ -5,13 +18,13 @@ import requests
from cinderclient.v1 import client
from cinderclient import exceptions
from tests import utils
from cinderclient.tests import utils
class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_authenticate_success(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", service_type='compute')
"http://localhost:8776/v1", service_type='volume')
resp = {
"access": {
"token": {
@@ -20,13 +33,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"type": "compute",
"type": "volume",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://localhost:8774/v1",
"internalURL": "http://localhost:8774/v1",
"publicURL": "http://localhost:8774/v1/",
"adminURL": "http://localhost:8776/v1",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
@@ -76,8 +89,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
test_auth_call()
def test_authenticate_tenant_id(self):
cs = client.Client("username", "password", auth_url="auth_url/v2.0",
tenant_id='tenant_id', service_type='compute')
cs = client.Client("username", "password",
auth_url="http://localhost:8776/v1",
tenant_id='tenant_id', service_type='volume')
resp = {
"access": {
"token": {
@@ -92,13 +106,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"type": "compute",
"type": "volume",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://localhost:8774/v1",
"internalURL": "http://localhost:8774/v1",
"publicURL": "http://localhost:8774/v1/",
"adminURL": "http://localhost:8776/v1",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
@@ -151,7 +165,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_authenticate_failure(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0")
"http://localhost:8776/v1")
resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
auth_response = utils.TestResponse({
"status_code": 401,
@@ -168,7 +182,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_auth_redirect(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v1", service_type='compute')
"http://localhost:8776/v1", service_type='volume')
dict_correct_response = {
"access": {
"token": {
@@ -177,13 +191,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"type": "compute",
"type": "volume",
"endpoints": [
{
"adminURL": "http://localhost:8774/v1",
"adminURL": "http://localhost:8776/v1",
"region": "RegionOne",
"internalURL": "http://localhost:8774/v1",
"publicURL": "http://localhost:8774/v1/",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1/",
},
],
},
@@ -192,7 +206,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
}
correct_response = json.dumps(dict_correct_response)
dict_responses = [
{"headers": {'location':'http://127.0.0.1:5001'},
{"headers": {'location': 'http://127.0.0.1:5001'},
"status_code": 305,
"text": "Use proxy"},
# Configured on admin port, cinder redirects to v2.0 port.
@@ -252,7 +266,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_ambiguous_endpoints(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", service_type='compute')
"http://localhost:8776/v1", service_type='volume')
resp = {
"access": {
"token": {
@@ -261,25 +275,25 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"adminURL": "http://localhost:8774/v1",
"type": "compute",
"name": "Compute CLoud",
"adminURL": "http://localhost:8776/v1",
"type": "volume",
"name": "Cinder Volume Service",
"endpoints": [
{
"region": "RegionOne",
"internalURL": "http://localhost:8774/v1",
"publicURL": "http://localhost:8774/v1/",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
{
"adminURL": "http://localhost:8774/v1",
"type": "compute",
"name": "Hyper-compute Cloud",
"adminURL": "http://localhost:8776/v1",
"type": "volume",
"name": "Cinder Volume Cloud Service",
"endpoints": [
{
"internalURL": "http://localhost:8774/v1",
"publicURL": "http://localhost:8774/v1/",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},

View File

@@ -0,0 +1,87 @@
# Copyright 2011-2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
from cinderclient.v1 import availability_zones
from cinderclient.v1 import shell
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class AvailabilityZoneTest(utils.TestCase):
def _assertZone(self, zone, name, status):
self.assertEqual(zone.zoneName, name)
self.assertEqual(zone.zoneState, status)
def test_list_availability_zone(self):
zones = cs.availability_zones.list(detailed=False)
cs.assert_called('GET', '/os-availability-zone')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertEqual(2, len(zones))
l0 = [six.u('zone-1'), six.u('available')]
l1 = [six.u('zone-2'), six.u('not available')]
z0 = shell._treeizeAvailabilityZone(zones[0])
z1 = shell._treeizeAvailabilityZone(zones[1])
self.assertEqual((len(z0), len(z1)), (1, 1))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z1[0], l1[0], l1[1])
def test_detail_availability_zone(self):
zones = cs.availability_zones.list(detailed=True)
cs.assert_called('GET', '/os-availability-zone/detail')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertEqual(3, len(zones))
l0 = [six.u('zone-1'), six.u('available')]
l1 = [six.u('|- fake_host-1'), six.u('')]
l2 = [six.u('| |- cinder-volume'),
six.u('enabled :-) 2012-12-26 14:45:25')]
l3 = [six.u('internal'), six.u('available')]
l4 = [six.u('|- fake_host-1'), six.u('')]
l5 = [six.u('| |- cinder-sched'),
six.u('enabled :-) 2012-12-26 14:45:24')]
l6 = [six.u('zone-2'), six.u('not available')]
z0 = shell._treeizeAvailabilityZone(zones[0])
z1 = shell._treeizeAvailabilityZone(zones[1])
z2 = shell._treeizeAvailabilityZone(zones[2])
self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z0[1], l1[0], l1[1])
self._assertZone(z0[2], l2[0], l2[1])
self._assertZone(z1[0], l3[0], l3[1])
self._assertZone(z1[1], l4[0], l4[1])
self._assertZone(z1[2], l5[0], l5[1])
self._assertZone(z2[0], l6[0], l6[1])

View File

@@ -0,0 +1,164 @@
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
from cinderclient.tests import utils
from cinderclient.v1 import limits
def _get_default_RateLimit(verb="verb1", uri="uri1", regex="regex1",
value="value1",
remain="remain1", unit="unit1",
next_available="next1"):
return limits.RateLimit(verb, uri, regex, value, remain, unit,
next_available)
class TestLimits(utils.TestCase):
def test_repr(self):
l = limits.Limits(None, {"foo": "bar"})
self.assertEqual("<Limits>", repr(l))
def test_absolute(self):
l = limits.Limits(None,
{"absolute": {"name1": "value1", "name2": "value2"}})
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name2", "value2")
for item in l.absolute:
self.assertIn(item, [l1, l2])
def test_rate(self):
l = limits.Limits(None,
{
"rate": [
{
"uri": "uri1",
"regex": "regex1",
"limit": [
{
"verb": "verb1",
"value": "value1",
"remaining": "remain1",
"unit": "unit1",
"next-available": "next1",
},
],
},
{
"uri": "uri2",
"regex": "regex2",
"limit": [
{
"verb": "verb2",
"value": "value2",
"remaining": "remain2",
"unit": "unit2",
"next-available": "next2",
},
],
},
],
})
l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1",
"unit1", "next1")
l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2",
"unit2", "next2")
for item in l.rate:
self.assertTrue(item in [l1, l2])
class TestRateLimit(utils.TestCase):
def test_equal(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit()
self.assertTrue(l1 == l2)
def test_not_equal_verbs(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(verb="verb2")
self.assertFalse(l1 == l2)
def test_not_equal_uris(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(uri="uri2")
self.assertFalse(l1 == l2)
def test_not_equal_regexps(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(regex="regex2")
self.assertFalse(l1 == l2)
def test_not_equal_values(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(value="value2")
self.assertFalse(l1 == l2)
def test_not_equal_remains(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(remain="remain2")
self.assertFalse(l1 == l2)
def test_not_equal_units(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(unit="unit2")
self.assertFalse(l1 == l2)
def test_not_equal_next_available(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(next_available="next2")
self.assertFalse(l1 == l2)
def test_repr(self):
l1 = _get_default_RateLimit()
self.assertEqual("<RateLimit: method=verb1 uri=uri1>", repr(l1))
class TestAbsoluteLimit(utils.TestCase):
def test_equal(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value1")
self.assertTrue(l1 == l2)
def test_not_equal_values(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value2")
self.assertFalse(l1 == l2)
def test_not_equal_names(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name2", "value1")
self.assertFalse(l1 == l2)
def test_repr(self):
l1 = limits.AbsoluteLimit("name1", "value1")
self.assertEqual("<AbsoluteLimit: name=name1>", repr(l1))
class TestLimitsManager(utils.TestCase):
def test_get(self):
api = mock.Mock()
api.client.get.return_value = (
None,
{"limits": {"absolute": {"name1": "value1", }},
"no-limits": {"absolute": {"name2": "value2", }}})
l1 = limits.AbsoluteLimit("name1", "value1")
limitsManager = limits.LimitsManager(api)
lim = limitsManager.get()
self.assertIsInstance(lim, limits.Limits)
for l in lim.absolute:
self.assertEqual(l, l1)

View File

@@ -0,0 +1,79 @@
# Copyright (C) 2013 eBay Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class QoSSpecsTest(utils.TestCase):
def test_create(self):
specs = dict(k1='v1', k2='v2')
cs.qos_specs.create('qos-name', specs)
cs.assert_called('POST', '/qos-specs')
def test_get(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.get(qos_id)
cs.assert_called('GET', '/qos-specs/%s' % qos_id)
def test_list(self):
cs.qos_specs.list()
cs.assert_called('GET', '/qos-specs')
def test_delete(self):
cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C')
cs.assert_called('DELETE',
'/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?'
'force=False')
def test_set_keys(self):
body = {'qos_specs': dict(k1='v1')}
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.set_keys(qos_id, body)
cs.assert_called('PUT', '/qos-specs/%s' % qos_id)
def test_unset_keys(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
body = {'keys': ['k1']}
cs.qos_specs.unset_keys(qos_id, body)
cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id)
def test_get_associations(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.get_associations(qos_id)
cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id)
def test_associate(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF'
cs.qos_specs.associate(qos_id, type_id)
cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s'
% (qos_id, type_id))
def test_disassociate(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF'
cs.qos_specs.disassociate(qos_id, type_id)
cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s'
% (qos_id, type_id))
def test_disassociate_all(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.disassociate_all(qos_id)
cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id)

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
@@ -29,7 +29,7 @@ class QuotaClassSetsTest(utils.TestCase):
def test_update_quota(self):
q = cs.quota_classes.get('test')
q.update(volumes=2)
q.update(volumes=2, snapshots=2)
cs.assert_called('PUT', '/os-quota-class-sets/test')
def test_refresh_quota(self):

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
@@ -25,7 +25,7 @@ class QuotaSetsTest(utils.TestCase):
def test_tenant_quotas_get(self):
tenant_id = 'test'
cs.quotas.get(tenant_id)
cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id)
cs.assert_called('GET', '/os-quota-sets/%s?usage=False' % tenant_id)
def test_tenant_quotas_defaults(self):
tenant_id = 'test'

View File

@@ -0,0 +1,66 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.v1 import services
cs = fakes.FakeClient()
class ServicesTest(utils.TestCase):
def test_list_services(self):
svs = cs.services.list()
cs.assert_called('GET', '/os-services')
self.assertEqual(len(svs), 3)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
def test_list_services_with_hostname(self):
svs = cs.services.list(host='host2')
cs.assert_called('GET', '/os-services?host=host2')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
def test_list_services_with_binary(self):
svs = cs.services.list(binary='cinder-volume')
cs.assert_called('GET', '/os-services?binary=cinder-volume')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
def test_list_services_with_host_binary(self):
svs = cs.services.list('host2', 'cinder-volume')
cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume')
self.assertEqual(len(svs), 1)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
def test_services_enable(self):
s = cs.services.enable('host1', 'cinder-volume')
values = {"host": "host1", 'binary': 'cinder-volume'}
cs.assert_called('PUT', '/os-services/enable', values)
self.assertTrue(isinstance(s, services.Service))
self.assertEqual(s.status, 'enabled')
def test_services_disable(self):
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')

View File

@@ -0,0 +1,357 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import fixtures
from cinderclient import client
from cinderclient import shell
from cinderclient.v1 import shell as shell_v1
from cinderclient.tests.v1 import fakes
from cinderclient.tests import utils
class ShellTest(utils.TestCase):
FAKE_ENV = {
'CINDER_USERNAME': 'username',
'CINDER_PASSWORD': 'password',
'CINDER_PROJECT_ID': 'project_id',
'OS_VOLUME_API_VERSION': '1',
'CINDER_URL': 'http://no.where',
}
# Patch os.environ to avoid required auth info.
def setUp(self):
"""Run before each test."""
super(ShellTest, self).setUp()
for var in self.FAKE_ENV:
self.useFixture(fixtures.EnvironmentVariable(var,
self.FAKE_ENV[var]))
self.shell = shell.OpenStackCinderShell()
#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
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
# no time to get instantatiated which is OK in this case, so
# we make sure the method is there before launching it.
if hasattr(self.shell, 'cs'):
self.shell.cs.clear_callstack()
#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_anytime(self, method, url, body=None):
return self.shell.cs.assert_called_anytime(method, url, body)
def test_extract_metadata(self):
# mimic the result of argparse's parse_args() method
class Arguments:
def __init__(self, metadata=[]):
self.metadata = metadata
inputs = [
([], {}),
(["key=value"], {"key": "value"}),
(["key"], {"key": None}),
(["k1=v1", "k2=v2"], {"k1": "v1", "k2": "v2"}),
(["k1=v1", "k2"], {"k1": "v1", "k2": None}),
(["k1", "k2=v2"], {"k1": None, "k2": "v2"})
]
for input in inputs:
args = Arguments(metadata=input[0])
self.assertEqual(shell_v1._extract_metadata(args), input[1])
def test_list(self):
self.run_command('list')
# NOTE(jdg): we default to detail currently
self.assert_called('GET', '/volumes/detail')
def test_list_filter_status(self):
self.run_command('list --status=available')
self.assert_called('GET', '/volumes/detail?status=available')
def test_list_filter_display_name(self):
self.run_command('list --display-name=1234')
self.assert_called('GET', '/volumes/detail?display_name=1234')
def test_list_all_tenants(self):
self.run_command('list --all-tenants=1')
self.assert_called('GET', '/volumes/detail?all_tenants=1')
def test_list_availability_zone(self):
self.run_command('availability-zone-list')
self.assert_called('GET', '/os-availability-zone')
def test_show(self):
self.run_command('show 1234')
self.assert_called('GET', '/volumes/1234')
def test_delete(self):
self.run_command('delete 1234')
self.assert_called('DELETE', '/volumes/1234')
def test_delete_by_name(self):
self.run_command('delete sample-volume')
self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1')
self.assert_called('DELETE', '/volumes/1234')
def test_delete_multiple(self):
self.run_command('delete 1234 5678')
self.assert_called_anytime('DELETE', '/volumes/1234')
self.assert_called('DELETE', '/volumes/5678')
def test_backup(self):
self.run_command('backup-create 1234')
self.assert_called('POST', '/backups')
def test_restore(self):
self.run_command('backup-restore 1234')
self.assert_called('POST', '/backups/1234/restore')
def test_snapshot_list_filter_volume_id(self):
self.run_command('snapshot-list --volume-id=1234')
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
def test_snapshot_list_filter_status_and_volume_id(self):
self.run_command('snapshot-list --status=available --volume-id=1234')
self.assert_called('GET', '/snapshots/detail?'
'status=available&volume_id=1234')
def test_rename(self):
# basic rename with positional arguments
self.run_command('rename 1234 new-name')
expected = {'volume': {'display_name': 'new-name'}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# change description only
self.run_command('rename 1234 --display-description=new-description')
expected = {'volume': {'display_description': 'new-description'}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# rename and change description
self.run_command('rename 1234 new-name '
'--display-description=new-description')
expected = {'volume': {
'display_name': 'new-name',
'display_description': 'new-description',
}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# Call rename with no arguments
self.assertRaises(SystemExit, self.run_command, 'rename')
def test_rename_snapshot(self):
# basic rename with positional arguments
self.run_command('snapshot-rename 1234 new-name')
expected = {'snapshot': {'display_name': 'new-name'}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# change description only
self.run_command('snapshot-rename 1234 '
'--display-description=new-description')
expected = {'snapshot': {'display_description': 'new-description'}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# snapshot-rename and change description
self.run_command('snapshot-rename 1234 new-name '
'--display-description=new-description')
expected = {'snapshot': {
'display_name': 'new-name',
'display_description': 'new-description',
}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# Call snapshot-rename with no arguments
self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
def test_set_metadata_set(self):
self.run_command('metadata 1234 set key1=val1 key2=val2')
self.assert_called('POST', '/volumes/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_set_metadata_delete_dict(self):
self.run_command('metadata 1234 unset key1=val1 key2=val2')
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
def test_set_metadata_delete_keys(self):
self.run_command('metadata 1234 unset key1 key2')
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
def test_reset_state(self):
self.run_command('reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_with_flag(self):
self.run_command('reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_multiple(self):
self.run_command('reset-state 1234 5678 --state error')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called_anytime('POST', '/volumes/1234/action',
body=expected)
self.assert_called_anytime('POST', '/volumes/5678/action',
body=expected)
def test_snapshot_reset_state(self):
self.run_command('snapshot-reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)
def test_snapshot_reset_state_with_flag(self):
self.run_command('snapshot-reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)
def test_snapshot_reset_state_multiple(self):
self.run_command('snapshot-reset-state 1234 5678')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called_anytime('POST', '/snapshots/1234/action',
body=expected)
self.assert_called_anytime('POST', '/snapshots/5678/action',
body=expected)
def test_encryption_type_list(self):
"""
Test encryption-type-list shell command.
Verify a series of GET requests are made:
- one to get the volume type list information
- one per volume type to retrieve the encryption type information
"""
self.run_command('encryption-type-list')
self.assert_called_anytime('GET', '/types')
self.assert_called_anytime('GET', '/types/1/encryption')
self.assert_called_anytime('GET', '/types/2/encryption')
def test_encryption_type_show(self):
"""
Test encryption-type-show shell command.
Verify two GET requests are made per command invocation:
- one to get the volume type information
- one to get the encryption type information
"""
self.run_command('encryption-type-show 1')
self.assert_called('GET', '/types/1/encryption')
self.assert_called_anytime('GET', '/types/1')
def test_encryption_type_create(self):
"""
Test encryption-type-create shell command.
Verify GET and POST requests are made per command invocation:
- one GET request to retrieve the relevant volume type information
- one POST request to create the new encryption type
"""
expected = {'encryption': {'cipher': None, 'key_size': None,
'provider': 'TestProvider',
'control_location': None}}
self.run_command('encryption-type-create 2 TestProvider')
self.assert_called('POST', '/types/2/encryption', body=expected)
self.assert_called_anytime('GET', '/types/2')
def test_encryption_type_update(self):
"""
Test encryption-type-update shell command.
Verify two GETs/one PUT requests are made per command invocation:
- one GET request to retrieve the relevant volume type information
- one GET request to retrieve the relevant encryption type information
- one PUT request to update the encryption type information
"""
self.skipTest("Not implemented")
def test_encryption_type_delete(self):
"""
Test encryption-type-delete shell command.
Verify one GET/one DELETE requests are made per command invocation:
- one GET request to retrieve the relevant volume type information
- one DELETE request to delete the encryption type information
"""
self.run_command('encryption-type-delete 1')
self.assert_called('DELETE', '/types/1/encryption/provider')
self.assert_called_anytime('GET', '/types/1')
def test_migrate_volume(self):
self.run_command('migrate 1234 fakehost --force-host-copy=True')
expected = {'os-migrate_volume': {'force_host_copy': 'True',
'host': 'fakehost'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_snapshot_metadata_set(self):
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
self.assert_called('POST', '/snapshots/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_snapshot_metadata_unset_dict(self):
self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
def test_snapshot_metadata_unset_keys(self):
self.run_command('snapshot-metadata 1234 unset key1 key2')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
def test_volume_metadata_update_all(self):
self.run_command('metadata-update-all 1234 key1=val1 key2=val2')
self.assert_called('PUT', '/volumes/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_snapshot_metadata_update_all(self):
self.run_command('snapshot-metadata-update-all\
1234 key1=val1 key2=val2')
self.assert_called('PUT', '/snapshots/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_readonly_mode_update(self):
self.run_command('readonly-mode-update 1234 True')
expected = {'os-update_readonly_flag': {'readonly': True}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
self.run_command('readonly-mode-update 1234 False')
expected = {'os-update_readonly_flag': {'readonly': False}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_service_disable(self):
self.run_command('service-disable host cinder-volume')
self.assert_called('PUT', '/os-services/disable',
{"binary": "cinder-volume", "host": "host"})
def test_service_disable(self):
self.run_command('service-enable host cinder-volume')
self.assert_called('PUT', '/os-services/enable',
{"binary": "cinder-volume", "host": "host"})
def test_snapshot_delete(self):
self.run_command('snapshot-delete 1234')
self.assert_called('DELETE', '/snapshots/1234')

View File

@@ -0,0 +1,35 @@
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class SnapshotActionsTest(utils.TestCase):
def test_update_snapshot_status(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available'})
cs.assert_called('POST', '/snapshots/1234/action')
def test_update_snapshot_status_with_progress(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available',
'progress': '73%'})
cs.assert_called('POST', '/snapshots/1234/action')

View File

@@ -1,7 +1,19 @@
from cinderclient import exceptions
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient.v1 import volume_types
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()

View File

@@ -0,0 +1,53 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class VolumeBackupsTest(utils.TestCase):
def test_create(self):
cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4')
cs.assert_called('POST', '/backups')
def test_get(self):
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
cs.backups.get(backup_id)
cs.assert_called('GET', '/backups/%s' % backup_id)
def test_list(self):
cs.backups.list()
cs.assert_called('GET', '/backups/detail')
def test_delete(self):
b = cs.backups.list()[0]
b.delete()
cs.assert_called('DELETE',
'/backups/76a17945-3c6f-435c-975b-b5685db10b62')
cs.backups.delete('76a17945-3c6f-435c-975b-b5685db10b62')
cs.assert_called('DELETE',
'/backups/76a17945-3c6f-435c-975b-b5685db10b62')
cs.backups.delete(b)
cs.assert_called('DELETE',
'/backups/76a17945-3c6f-435c-975b-b5685db10b62')
def test_restore(self):
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
cs.restores.restore(backup_id)
cs.assert_called('POST', '/backups/%s/restore' % backup_id)

View File

@@ -0,0 +1,100 @@
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.v1.volume_encryption_types import VolumeEncryptionType
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class VolumeEncryptionTypesTest(utils.TestCase):
"""
Test suite for the Volume Encryption Types Resource and Manager.
"""
def test_list(self):
"""
Unit test for VolumeEncryptionTypesManager.list
Verify that a series of GET requests are made:
- one GET request for the list of volume types
- one GET request per volume type for encryption type information
Verify that all returned information is :class: VolumeEncryptionType
"""
encryption_types = cs.volume_encryption_types.list()
cs.assert_called_anytime('GET', '/types')
cs.assert_called_anytime('GET', '/types/2/encryption')
cs.assert_called_anytime('GET', '/types/1/encryption')
for encryption_type in encryption_types:
self.assertIsInstance(encryption_type, VolumeEncryptionType)
def test_get(self):
"""
Unit test for VolumeEncryptionTypesManager.get
Verify that one GET request is made for the volume type encryption
type information. Verify that returned information is :class:
VolumeEncryptionType
"""
encryption_type = cs.volume_encryption_types.get(1)
cs.assert_called('GET', '/types/1/encryption')
self.assertIsInstance(encryption_type, VolumeEncryptionType)
def test_get_no_encryption(self):
"""
Unit test for VolumeEncryptionTypesManager.get
Verify that a request on a volume type with no associated encryption
type information returns a VolumeEncryptionType with no attributes.
"""
encryption_type = cs.volume_encryption_types.get(2)
self.assertIsInstance(encryption_type, VolumeEncryptionType)
self.assertFalse(hasattr(encryption_type, 'id'),
'encryption type has an id')
def test_create(self):
"""
Unit test for VolumeEncryptionTypesManager.create
Verify that one POST request is made for the encryption type creation.
Verify that encryption type creation returns a VolumeEncryptionType.
"""
result = cs.volume_encryption_types.create(2, {'encryption':
{'provider': 'Test',
'key_size': None,
'cipher': None,
'control_location':
None}})
cs.assert_called('POST', '/types/2/encryption')
self.assertIsInstance(result, VolumeEncryptionType)
def test_update(self):
"""
Unit test for VolumeEncryptionTypesManager.update
"""
self.skipTest("Not implemented")
def test_delete(self):
"""
Unit test for VolumeEncryptionTypesManager.delete
Verify that one DELETE request is made for encryption type deletion
Verify that encryption type deletion returns None
"""
result = cs.volume_encryption_types.delete(1)
cs.assert_called('DELETE', '/types/1/encryption/provider')
self.assertIsNone(result, "delete result must be None")

View File

@@ -0,0 +1,51 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class VolumeTransfersTest(utils.TestCase):
def test_create(self):
cs.transfers.create('1234')
cs.assert_called('POST', '/os-volume-transfer')
def test_get(self):
transfer_id = '5678'
cs.transfers.get(transfer_id)
cs.assert_called('GET', '/os-volume-transfer/%s' % transfer_id)
def test_list(self):
cs.transfers.list()
cs.assert_called('GET', '/os-volume-transfer/detail')
def test_delete(self):
b = cs.transfers.list()[0]
b.delete()
cs.assert_called('DELETE', '/os-volume-transfer/5678')
cs.transfers.delete('5678')
cs.assert_called('DELETE', '/os-volume-transfer/5678')
cs.transfers.delete(b)
cs.assert_called('DELETE', '/os-volume-transfer/5678')
def test_accept(self):
transfer_id = '5678'
auth_key = '12345'
cs.transfers.accept(transfer_id, auth_key)
cs.assert_called('POST', '/os-volume-transfer/%s/accept' % transfer_id)

View File

@@ -1,5 +1,18 @@
from tests import utils
from tests.v1 import fakes
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
@@ -16,13 +29,13 @@ class VolumesTest(utils.TestCase):
cs.volumes.delete(v)
cs.assert_called('DELETE', '/volumes/1234')
def test_create_keypair(self):
def test_create_volume(self):
cs.volumes.create(1)
cs.assert_called('POST', '/volumes')
def test_attach(self):
v = cs.volumes.get('1234')
cs.volumes.attach(v, 1, '/dev/vdc')
cs.volumes.attach(v, 1, '/dev/vdc', mode='rw')
cs.assert_called('POST', '/volumes/1234/action')
def test_detach(self):
@@ -69,3 +82,27 @@ class VolumesTest(utils.TestCase):
keys = ['key1']
cs.volumes.delete_metadata(1234, keys)
cs.assert_called('DELETE', '/volumes/1234/metadata/key1')
def test_extend(self):
v = cs.volumes.get('1234')
cs.volumes.extend(v, 2)
cs.assert_called('POST', '/volumes/1234/action')
def test_get_encryption_metadata(self):
cs.volumes.get_encryption_metadata('1234')
cs.assert_called('GET', '/volumes/1234/encryption')
def test_migrate(self):
v = cs.volumes.get('1234')
cs.volumes.migrate_volume(v, 'dest', False)
cs.assert_called('POST', '/volumes/1234/action')
def test_metadata_update_all(self):
cs.volumes.update_all_metadata(1234, {'k1': 'v1'})
cs.assert_called('PUT', '/volumes/1234/metadata',
{'metadata': {'k1': 'v1'}})
def test_readonly_mode_update(self):
v = cs.volumes.get('1234')
cs.volumes.update_readonly_flag(v, True)
cs.assert_called('POST', '/volumes/1234/action')

View File

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -16,8 +16,8 @@
from cinderclient import extension
from cinderclient.v2.contrib import list_extensions
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
extensions = [

View File

@@ -0,0 +1,791 @@
# Copyright (c) 2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# 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
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from cinderclient import client as base_client
from cinderclient.tests import fakes
import cinderclient.tests.utils as utils
from cinderclient.v2 import client
def _stub_volume(**kwargs):
volume = {
'id': '1234',
'name': None,
'description': None,
"attachments": [],
"bootable": "false",
"availability_zone": "cinder",
"created_at": "2012-08-27T00:00:00.000000",
"id": '00000000-0000-0000-0000-000000000000',
"metadata": {},
"size": 1,
"snapshot_id": None,
"status": "available",
"volume_type": "None",
"links": [
{
"href": "http://localhost/v2/fake/volumes/1234",
"rel": "self"
},
{
"href": "http://localhost/fake/volumes/1234",
"rel": "bookmark"
}
],
}
volume.update(kwargs)
return volume
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
def _self_href(base_uri, tenant_id, backup_id):
return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id)
def _bookmark_href(base_uri, tenant_id, backup_id):
return '%s/%s/backups/%s' % (base_uri, tenant_id, backup_id)
def _stub_backup_full(id, base_uri, tenant_id):
return {
'id': id,
'name': 'backup',
'description': 'nightly backup',
'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b',
'container': 'volumebackups',
'object_count': 220,
'size': 10,
'availability_zone': 'az1',
'created_at': '2013-04-12T08:16:37.000000',
'status': 'available',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_backup(id, base_uri, tenant_id):
return {
'id': id,
'name': 'backup',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None):
if not name:
name = 'fake-name'
if not specs:
specs = {}
return {
'qos_specs': {
'id': id,
'name': name,
'consumer': 'back-end',
'specs': specs,
},
'links': {
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
}
def _stub_qos_associates(id, name):
return {
'assoications_type': 'volume_type',
'name': name,
'id': id,
}
def _stub_restore():
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
def _stub_transfer_full(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'created_at': '2013-04-12T08:16:37.000000',
'auth_key': '123456',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_transfer(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_extend(id, new_size):
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
client.Client.__init__(self, 'username', 'password',
'project_id', 'auth_url',
extensions=kwargs.get('extensions'))
self.client = FakeHTTPClient(**kwargs)
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()
class FakeHTTPClient(base_client.HTTPClient):
def __init__(self, **kwargs):
self.username = 'username'
self.password = 'password'
self.auth_url = 'auth_url'
self.callstack = []
self.management_url = 'http://10.0.2.15:8776/v2/fake'
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert 'body' not in kwargs
elif method == 'PUT':
assert 'body' in kwargs
# Call the method
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
status, headers, body = getattr(self, callback)(**kwargs)
r = utils.TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})
return r, body
if hasattr(status, 'items'):
return utils.TestResponse(status), body
else:
return utils.TestResponse({"status": status}), body
def get_volume_api_version_from_endpoint(self):
magic_tuple = urlparse.urlsplit(self.management_url)
scheme, netloc, path, query, frag = magic_tuple
return path.lstrip('/').split('/')[0][1:]
#
# Snapshots
#
def get_snapshots_detail(self, **kw):
return (200, {}, {'snapshots': [
_stub_snapshot(),
]})
def get_snapshots_1234(self, **kw):
return (200, {}, {'snapshot': _stub_snapshot(id='1234')})
def get_snapshots_5678(self, **kw):
return (200, {}, {'snapshot': _stub_snapshot(id='5678')})
def put_snapshots_1234(self, **kw):
snapshot = _stub_snapshot(id='1234')
snapshot.update(kw['body']['snapshot'])
return (200, {}, {'snapshot': snapshot})
def post_snapshots_1234_action(self, body, **kw):
_body = None
resp = 202
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 (resp, {}, _body)
def post_snapshots_5678_action(self, body, **kw):
return self.post_snapshots_1234_action(body, **kw)
def delete_snapshots_1234(self, **kw):
return (202, {}, {})
#
# Volumes
#
def put_volumes_1234(self, **kw):
volume = _stub_volume(id='1234')
volume.update(kw['body']['volume'])
return (200, {}, {'volume': volume})
def get_volumes(self, **kw):
return (200, {}, {"volumes": [
{'id': 1234, 'name': 'sample-volume'},
{'id': 5678, 'name': 'sample-volume2'}
]})
# TODO(jdg): This will need to change
# at the very least it's not complete
def get_volumes_detail(self, **kw):
return (200, {}, {"volumes": [
{'id': kw.get('id', 1234),
'name': 'sample-volume',
'attachments': [{'server_id': 1234}]},
]})
def get_volumes_1234(self, **kw):
r = {'volume': self.get_volumes_detail(id=1234)[2]['volumes'][0]}
return (200, {}, r)
def get_volumes_5678(self, **kw):
r = {'volume': self.get_volumes_detail(id=5678)[2]['volumes'][0]}
return (200, {}, r)
def get_volumes_1234_encryption(self, **kw):
r = {'encryption_key_id': 'id'}
return (200, {}, r)
def post_volumes_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(list(body)) == 1
action = list(body)[0]
if action == 'os-attach':
assert sorted(list(body[action])) == ['instance_uuid',
'mode',
'mountpoint']
elif action == 'os-detach':
assert body[action] is None
elif action == 'os-reserve':
assert body[action] is None
elif action == 'os-unreserve':
assert body[action] is None
elif action == 'os-initialize_connection':
assert list(body[action]) == ['connector']
return (202, {}, {'connection_info': 'foos'})
elif action == 'os-terminate_connection':
assert list(body[action]) == ['connector']
elif action == 'os-begin_detaching':
assert body[action] is None
elif action == 'os-roll_detaching':
assert body[action] is None
elif action == 'os-reset_status':
assert 'status' in body[action]
elif action == 'os-extend':
assert list(body[action]) == ['new_size']
elif action == 'os-migrate_volume':
assert 'host' in body[action]
assert 'force_host_copy' in body[action]
elif action == 'os-update_readonly_flag':
assert list(body[action]) == ['readonly']
elif action == 'os-retype':
assert 'new_type' in body[action]
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
def post_volumes_5678_action(self, body, **kw):
return self.post_volumes_1234_action(body, **kw)
def post_volumes(self, **kw):
return (202, {}, {'volume': {}})
def delete_volumes_1234(self, **kw):
return (202, {}, None)
def delete_volumes_5678(self, **kw):
return (202, {}, None)
#
# Quotas
#
def get_os_quota_sets_test(self, **kw):
return (200, {}, {'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
def get_os_quota_sets_test_defaults(self):
return (200, {}, {'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
def put_os_quota_sets_test(self, body, **kw):
assert list(body) == ['quota_set']
fakes.assert_has_keys(body['quota_set'],
required=['tenant_id'])
return (200, {}, {'quota_set': {
'tenant_id': 'test',
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
#
# Quota Classes
#
def get_os_quota_class_sets_test(self, **kw):
return (200, {}, {'quota_class_set': {
'class_name': 'test',
'metadata_items': [],
'volumes': 1,
'snapshots': 1,
'gigabytes': 1}})
def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set']
fakes.assert_has_keys(body['quota_class_set'],
required=['class_name'])
return (200, {}, {'quota_class_set': {
'class_name': 'test',
'metadata_items': [],
'volumes': 2,
'snapshots': 2,
'gigabytes': 1}})
#
# VolumeTypes
#
def get_types(self, **kw):
return (200, {}, {
'volume_types': [{'id': 1,
'name': 'test-type-1',
'extra_specs': {}},
{'id': 2,
'name': 'test-type-2',
'extra_specs': {}}]})
def get_types_1(self, **kw):
return (200, {}, {'volume_type': {'id': 1,
'name': 'test-type-1',
'extra_specs': {}}})
def get_types_2(self, **kw):
return (200, {}, {'volume_type': {'id': 2,
'name': 'test-type-2',
'extra_specs': {}}})
def post_types(self, body, **kw):
return (202, {}, {'volume_type': {'id': 3,
'name': 'test-type-3',
'extra_specs': {}}})
def post_types_1_extra_specs(self, body, **kw):
assert list(body) == ['extra_specs']
return (200, {}, {'extra_specs': {'k': 'v'}})
def delete_types_1_extra_specs_k(self, **kw):
return(204, {}, None)
def delete_types_1(self, **kw):
return (202, {}, None)
#
# VolumeEncryptionTypes
#
def get_types_1_encryption(self, **kw):
return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test',
'cipher': 'test', 'key_size': 1,
'control_location': 'front'})
def get_types_2_encryption(self, **kw):
return (200, {}, {})
def post_types_2_encryption(self, body, **kw):
return (200, {}, {'encryption': {}})
def put_types_1_encryption_1(self, body, **kw):
return (200, {}, {})
def delete_types_1_encryption_provider(self, **kw):
return (202, {}, None)
#
# Set/Unset metadata
#
def delete_volumes_1234_metadata_test_key(self, **kw):
return (204, {}, None)
def delete_volumes_1234_metadata_key1(self, **kw):
return (204, {}, None)
def delete_volumes_1234_metadata_key2(self, **kw):
return (204, {}, None)
def post_volumes_1234_metadata(self, **kw):
return (204, {}, {'metadata': {'test_key': 'test_value'}})
#
# List all extensions
#
def get_extensions(self, **kw):
exts = [
{
"alias": "FAKE-1",
"description": "Fake extension number 1",
"links": [],
"name": "Fake1",
"namespace": ("http://docs.openstack.org/"
"/ext/fake1/api/v1.1"),
"updated": "2011-06-09T00:00:00+00:00"
},
{
"alias": "FAKE-2",
"description": "Fake extension number 2",
"links": [],
"name": "Fake2",
"namespace": ("http://docs.openstack.org/"
"/ext/fake1/api/v1.1"),
"updated": "2011-06-09T00:00:00+00:00"
},
]
return (200, {}, {"extensions": exts, })
#
# VolumeBackups
#
def get_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
return (200, {},
{'backup': _stub_backup_full(backup1, base_uri, tenant_id)})
def get_backups_detail(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
backup2 = 'd09534c6-08b8-4441-9e87-8976f3a8f699'
return (200, {},
{'backups': [
_stub_backup_full(backup1, base_uri, tenant_id),
_stub_backup_full(backup2, base_uri, tenant_id)]})
def delete_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw):
return (202, {}, None)
def post_backups(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
backup1 = '76a17945-3c6f-435c-975b-b5685db10b62'
return (202, {},
{'backup': _stub_backup(backup1, base_uri, tenant_id)})
def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw):
return (200, {},
{'restore': _stub_restore()})
def post_backups_1234_restore(self, **kw):
return (200, {},
{'restore': _stub_restore()})
#
# QoSSpecs
#
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
return (200, {},
_stub_qos_full(qos_id1, base_uri, tenant_id))
def get_qos_specs(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B'
return (200, {},
{'qos_specs': [
_stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'),
_stub_qos_full(qos_id2, base_uri, tenant_id)]})
def post_qos_specs(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
qos_name = 'qos-name'
return (202, {},
_stub_qos_full(qos_id, base_uri, tenant_id, qos_name))
def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw):
return (202, {}, None)
def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys(
self, **kw):
return (202, {}, None)
def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw):
return (202, {}, None)
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations(
self, **kw):
type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF'
type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF'
type_name1 = 'type1'
type_name2 = 'type2'
return (202, {},
{'qos_associations': [
_stub_qos_associates(type_id1, type_name1),
_stub_qos_associates(type_id2, type_name2)]})
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate(
self, **kw):
return (202, {}, None)
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate(
self, **kw):
return (202, {}, None)
def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all(
self, **kw):
return (202, {}, None)
#
#
# VolumeTransfers
#
def get_os_volume_transfer_5678(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer':
_stub_transfer_full(transfer1, base_uri, tenant_id)})
def get_os_volume_transfer_detail(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41'
return (200, {},
{'transfers': [
_stub_transfer_full(transfer1, base_uri, tenant_id),
_stub_transfer_full(transfer2, base_uri, tenant_id)]})
def delete_os_volume_transfer_5678(self, **kw):
return (202, {}, None)
def post_os_volume_transfer(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (202, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
def post_os_volume_transfer_5678_accept(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
#
# Services
#
def get_os_services(self, **kw):
host = kw.get('host', None)
binary = kw.get('binary', None)
services = [
{
'binary': 'cinder-volume',
'host': 'host1',
'zone': 'cinder',
'status': 'enabled',
'state': 'up',
'updated_at': datetime(2012, 10, 29, 13, 42, 2)
},
{
'binary': 'cinder-volume',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
{
'binary': 'cinder-scheduler',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
]
if host:
services = filter(lambda i: i['host'] == host, services)
if binary:
services = filter(lambda i: i['binary'] == binary, services)
return (200, {}, {'services': services})
def put_os_services_enable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'enabled'})
def put_os_services_disable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled'})
def get_os_availability_zone(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": None,
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})
def get_os_availability_zone_detail(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-volume": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 25, 0)
}
}
}
},
{
"zoneName": "internal",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-sched": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 24, 0)
}
}
}
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})
def post_snapshots_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
def delete_snapshots_1234_metadata_key1(self, **kw):
return (200, {}, None)
def delete_snapshots_1234_metadata_key2(self, **kw):
return (200, {}, None)
def put_volumes_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
def put_snapshots_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -21,13 +21,13 @@ import requests
from cinderclient import exceptions
from cinderclient.v2 import client
from tests import utils
from cinderclient.tests import utils
class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_authenticate_success(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", service_type='compute')
"http://localhost:8776/v2", service_type='volumev2')
resp = {
"access": {
"token": {
@@ -36,13 +36,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"type": "compute",
"type": "volumev2",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://localhost:8774/v2",
"internalURL": "http://localhost:8774/v2",
"publicURL": "http://localhost:8774/v2/",
"adminURL": "http://localhost:8776/v2",
"internalURL": "http://localhost:8776/v2",
"publicURL": "http://localhost:8776/v2",
},
],
},
@@ -92,8 +92,9 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
test_auth_call()
def test_authenticate_tenant_id(self):
cs = client.Client("username", "password", auth_url="auth_url/v2.0",
tenant_id='tenant_id', service_type='compute')
cs = client.Client("username", "password",
auth_url="http://localhost:8776/v2",
tenant_id='tenant_id', service_type='volumev2')
resp = {
"access": {
"token": {
@@ -108,13 +109,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"type": "compute",
"type": 'volumev2',
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://localhost:8774/v2",
"internalURL": "http://localhost:8774/v2",
"publicURL": "http://localhost:8774/v2/",
"adminURL": "http://localhost:8776/v2",
"internalURL": "http://localhost:8776/v2",
"publicURL": "http://localhost:8776/v2",
},
],
},
@@ -167,7 +168,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_authenticate_failure(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0")
"http://localhost:8776/v2")
resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
auth_response = utils.TestResponse({
"status_code": 401,
@@ -184,7 +185,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_auth_redirect(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2", service_type='compute')
"http://localhost:8776/v2", service_type='volumev2')
dict_correct_response = {
"access": {
"token": {
@@ -193,13 +194,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"type": "compute",
"type": "volumev2",
"endpoints": [
{
"adminURL": "http://localhost:8774/v2",
"adminURL": "http://localhost:8776/v2",
"region": "RegionOne",
"internalURL": "http://localhost:8774/v2",
"publicURL": "http://localhost:8774/v2/",
"internalURL": "http://localhost:8776/v2",
"publicURL": "http://localhost:8776/v2/",
},
],
},
@@ -208,7 +209,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
}
correct_response = json.dumps(dict_correct_response)
dict_responses = [
{"headers": {'location':'http://127.0.0.1:5001'},
{"headers": {'location': 'http://127.0.0.1:5001'},
"status_code": 305,
"text": "Use proxy"},
# Configured on admin port, cinder redirects to v2.0 port.
@@ -268,7 +269,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_ambiguous_endpoints(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0", service_type='compute')
"http://localhost:8776/v2", service_type='volumev2')
resp = {
"access": {
"token": {
@@ -277,25 +278,25 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"adminURL": "http://localhost:8774/v2",
"type": "compute",
"name": "Compute CLoud",
"adminURL": "http://localhost:8776/v1",
"type": "volumev2",
"name": "Cinder Volume Service",
"endpoints": [
{
"region": "RegionOne",
"internalURL": "http://localhost:8774/v2",
"publicURL": "http://localhost:8774/v2/",
"internalURL": "http://localhost:8776/v1",
"publicURL": "http://localhost:8776/v1",
},
],
},
{
"adminURL": "http://localhost:8774/v2",
"type": "compute",
"name": "Hyper-compute Cloud",
"adminURL": "http://localhost:8776/v2",
"type": "volumev2",
"name": "Cinder Volume V2",
"endpoints": [
{
"internalURL": "http://localhost:8774/v2",
"publicURL": "http://localhost:8774/v2/",
"internalURL": "http://localhost:8776/v2",
"publicURL": "http://localhost:8776/v2",
},
],
},

View File

@@ -0,0 +1,87 @@
# Copyright 2011-2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
from cinderclient.v1 import availability_zones
from cinderclient.v1 import shell
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class AvailabilityZoneTest(utils.TestCase):
def _assertZone(self, zone, name, status):
self.assertEqual(zone.zoneName, name)
self.assertEqual(zone.zoneState, status)
def test_list_availability_zone(self):
zones = cs.availability_zones.list(detailed=False)
cs.assert_called('GET', '/os-availability-zone')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertEqual(2, len(zones))
l0 = [six.u('zone-1'), six.u('available')]
l1 = [six.u('zone-2'), six.u('not available')]
z0 = shell._treeizeAvailabilityZone(zones[0])
z1 = shell._treeizeAvailabilityZone(zones[1])
self.assertEqual((len(z0), len(z1)), (1, 1))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z1[0], l1[0], l1[1])
def test_detail_availability_zone(self):
zones = cs.availability_zones.list(detailed=True)
cs.assert_called('GET', '/os-availability-zone/detail')
for zone in zones:
self.assertTrue(isinstance(zone,
availability_zones.AvailabilityZone))
self.assertEqual(3, len(zones))
l0 = [six.u('zone-1'), six.u('available')]
l1 = [six.u('|- fake_host-1'), six.u('')]
l2 = [six.u('| |- cinder-volume'),
six.u('enabled :-) 2012-12-26 14:45:25')]
l3 = [six.u('internal'), six.u('available')]
l4 = [six.u('|- fake_host-1'), six.u('')]
l5 = [six.u('| |- cinder-sched'),
six.u('enabled :-) 2012-12-26 14:45:24')]
l6 = [six.u('zone-2'), six.u('not available')]
z0 = shell._treeizeAvailabilityZone(zones[0])
z1 = shell._treeizeAvailabilityZone(zones[1])
z2 = shell._treeizeAvailabilityZone(zones[2])
self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1))
self._assertZone(z0[0], l0[0], l0[1])
self._assertZone(z0[1], l1[0], l1[1])
self._assertZone(z0[2], l2[0], l2[1])
self._assertZone(z1[0], l3[0], l3[1])
self._assertZone(z1[1], l4[0], l4[1])
self._assertZone(z1[2], l5[0], l5[1])
self._assertZone(z2[0], l6[0], l6[1])

View File

@@ -0,0 +1,164 @@
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
from cinderclient.tests import utils
from cinderclient.v2 import limits
def _get_default_RateLimit(verb="verb1", uri="uri1", regex="regex1",
value="value1",
remain="remain1", unit="unit1",
next_available="next1"):
return limits.RateLimit(verb, uri, regex, value, remain, unit,
next_available)
class TestLimits(utils.TestCase):
def test_repr(self):
l = limits.Limits(None, {"foo": "bar"})
self.assertEqual("<Limits>", repr(l))
def test_absolute(self):
l = limits.Limits(None,
{"absolute": {"name1": "value1", "name2": "value2"}})
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name2", "value2")
for item in l.absolute:
self.assertIn(item, [l1, l2])
def test_rate(self):
l = limits.Limits(None,
{
"rate": [
{
"uri": "uri1",
"regex": "regex1",
"limit": [
{
"verb": "verb1",
"value": "value1",
"remaining": "remain1",
"unit": "unit1",
"next-available": "next1",
},
],
},
{
"uri": "uri2",
"regex": "regex2",
"limit": [
{
"verb": "verb2",
"value": "value2",
"remaining": "remain2",
"unit": "unit2",
"next-available": "next2",
},
],
},
],
})
l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1",
"unit1", "next1")
l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2",
"unit2", "next2")
for item in l.rate:
self.assertTrue(item in [l1, l2])
class TestRateLimit(utils.TestCase):
def test_equal(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit()
self.assertTrue(l1 == l2)
def test_not_equal_verbs(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(verb="verb2")
self.assertFalse(l1 == l2)
def test_not_equal_uris(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(uri="uri2")
self.assertFalse(l1 == l2)
def test_not_equal_regexps(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(regex="regex2")
self.assertFalse(l1 == l2)
def test_not_equal_values(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(value="value2")
self.assertFalse(l1 == l2)
def test_not_equal_remains(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(remain="remain2")
self.assertFalse(l1 == l2)
def test_not_equal_units(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(unit="unit2")
self.assertFalse(l1 == l2)
def test_not_equal_next_available(self):
l1 = _get_default_RateLimit()
l2 = _get_default_RateLimit(next_available="next2")
self.assertFalse(l1 == l2)
def test_repr(self):
l1 = _get_default_RateLimit()
self.assertEqual("<RateLimit: method=verb1 uri=uri1>", repr(l1))
class TestAbsoluteLimit(utils.TestCase):
def test_equal(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value1")
self.assertTrue(l1 == l2)
def test_not_equal_values(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name1", "value2")
self.assertFalse(l1 == l2)
def test_not_equal_names(self):
l1 = limits.AbsoluteLimit("name1", "value1")
l2 = limits.AbsoluteLimit("name2", "value1")
self.assertFalse(l1 == l2)
def test_repr(self):
l1 = limits.AbsoluteLimit("name1", "value1")
self.assertEqual("<AbsoluteLimit: name=name1>", repr(l1))
class TestLimitsManager(utils.TestCase):
def test_get(self):
api = mock.Mock()
api.client.get.return_value = (
None,
{"limits": {"absolute": {"name1": "value1", }},
"no-limits": {"absolute": {"name2": "value2", }}})
l1 = limits.AbsoluteLimit("name1", "value1")
limitsManager = limits.LimitsManager(api)
lim = limitsManager.get()
self.assertIsInstance(lim, limits.Limits)
for l in lim.absolute:
self.assertEqual(l, l1)

View File

@@ -0,0 +1,79 @@
# Copyright (C) 2013 eBay Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class QoSSpecsTest(utils.TestCase):
def test_create(self):
specs = dict(k1='v1', k2='v2')
cs.qos_specs.create('qos-name', specs)
cs.assert_called('POST', '/qos-specs')
def test_get(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.get(qos_id)
cs.assert_called('GET', '/qos-specs/%s' % qos_id)
def test_list(self):
cs.qos_specs.list()
cs.assert_called('GET', '/qos-specs')
def test_delete(self):
cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C')
cs.assert_called('DELETE',
'/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?'
'force=False')
def test_set_keys(self):
body = {'qos_specs': dict(k1='v1')}
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.set_keys(qos_id, body)
cs.assert_called('PUT', '/qos-specs/%s' % qos_id)
def test_unset_keys(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
body = {'keys': ['k1']}
cs.qos_specs.unset_keys(qos_id, body)
cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id)
def test_get_associations(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.get_associations(qos_id)
cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id)
def test_associate(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF'
cs.qos_specs.associate(qos_id, type_id)
cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s'
% (qos_id, type_id))
def test_disassociate(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF'
cs.qos_specs.disassociate(qos_id, type_id)
cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s'
% (qos_id, type_id))
def test_disassociate_all(self):
qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C'
cs.qos_specs.disassociate_all(qos_id)
cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id)

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
@@ -29,7 +29,7 @@ class QuotaClassSetsTest(utils.TestCase):
def test_update_quota(self):
q = cs.quota_classes.get('test')
q.update(volumes=2)
q.update(volumes=2, snapshots=2)
cs.assert_called('PUT', '/os-quota-class-sets/test')
def test_refresh_quota(self):

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
@@ -25,7 +25,7 @@ class QuotaSetsTest(utils.TestCase):
def test_tenant_quotas_get(self):
tenant_id = 'test'
cs.quotas.get(tenant_id)
cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id)
cs.assert_called('GET', '/os-quota-sets/%s?usage=False' % tenant_id)
def test_tenant_quotas_defaults(self):
tenant_id = 'test'

View File

@@ -0,0 +1,66 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
from cinderclient.v2 import services
cs = fakes.FakeClient()
class ServicesTest(utils.TestCase):
def test_list_services(self):
svs = cs.services.list()
cs.assert_called('GET', '/os-services')
self.assertEqual(len(svs), 3)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
def test_list_services_with_hostname(self):
svs = cs.services.list(host='host2')
cs.assert_called('GET', '/os-services?host=host2')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
def test_list_services_with_binary(self):
svs = cs.services.list(binary='cinder-volume')
cs.assert_called('GET', '/os-services?binary=cinder-volume')
self.assertEqual(len(svs), 2)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
def test_list_services_with_host_binary(self):
svs = cs.services.list('host2', 'cinder-volume')
cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume')
self.assertEqual(len(svs), 1)
[self.assertTrue(isinstance(s, services.Service)) for s in svs]
[self.assertEqual(s.host, 'host2') for s in svs]
[self.assertEqual(s.binary, 'cinder-volume') for s in svs]
def test_services_enable(self):
s = cs.services.enable('host1', 'cinder-volume')
values = {"host": "host1", 'binary': 'cinder-volume'}
cs.assert_called('PUT', '/os-services/enable', values)
self.assertTrue(isinstance(s, services.Service))
self.assertEqual(s.status, 'enabled')
def test_services_disable(self):
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')

View File

@@ -0,0 +1,347 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import fixtures
from cinderclient import client
from cinderclient import shell
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
class ShellTest(utils.TestCase):
FAKE_ENV = {
'CINDER_USERNAME': 'username',
'CINDER_PASSWORD': 'password',
'CINDER_PROJECT_ID': 'project_id',
'OS_VOLUME_API_VERSION': '2',
'CINDER_URL': 'http://no.where',
}
# Patch os.environ to avoid required auth info.
def setUp(self):
"""Run before each test."""
super(ShellTest, self).setUp()
for var in self.FAKE_ENV:
self.useFixture(fixtures.EnvironmentVariable(var,
self.FAKE_ENV[var]))
self.shell = shell.OpenStackCinderShell()
#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
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
# no time to get instantatiated which is OK in this case, so
# we make sure the method is there before launching it.
if hasattr(self.shell, 'cs'):
self.shell.cs.clear_callstack()
#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_anytime(self, method, url, body=None):
return self.shell.cs.assert_called_anytime(method, url, body)
def test_list(self):
self.run_command('list')
# NOTE(jdg): we default to detail currently
self.assert_called('GET', '/volumes/detail')
def test_list_filter_status(self):
self.run_command('list --status=available')
self.assert_called('GET', '/volumes/detail?status=available')
def test_list_filter_name(self):
self.run_command('list --name=1234')
self.assert_called('GET', '/volumes/detail?name=1234')
def test_list_all_tenants(self):
self.run_command('list --all-tenants=1')
self.assert_called('GET', '/volumes/detail?all_tenants=1')
def test_list_availability_zone(self):
self.run_command('availability-zone-list')
self.assert_called('GET', '/os-availability-zone')
def test_show(self):
self.run_command('show 1234')
self.assert_called('GET', '/volumes/1234')
def test_delete(self):
self.run_command('delete 1234')
self.assert_called('DELETE', '/volumes/1234')
def test_delete_by_name(self):
self.run_command('delete sample-volume')
self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1')
self.assert_called('DELETE', '/volumes/1234')
def test_delete_multiple(self):
self.run_command('delete 1234 5678')
self.assert_called_anytime('DELETE', '/volumes/1234')
self.assert_called('DELETE', '/volumes/5678')
def test_backup(self):
self.run_command('backup-create 1234')
self.assert_called('POST', '/backups')
def test_restore(self):
self.run_command('backup-restore 1234')
self.assert_called('POST', '/backups/1234/restore')
def test_snapshot_list_filter_volume_id(self):
self.run_command('snapshot-list --volume-id=1234')
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
def test_snapshot_list_filter_status_and_volume_id(self):
self.run_command('snapshot-list --status=available --volume-id=1234')
self.assert_called('GET', '/snapshots/detail?'
'status=available&volume_id=1234')
def test_rename(self):
# basic rename with positional arguments
self.run_command('rename 1234 new-name')
expected = {'volume': {'name': 'new-name'}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# change description only
self.run_command('rename 1234 --description=new-description')
expected = {'volume': {'description': 'new-description'}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# rename and change description
self.run_command('rename 1234 new-name '
'--description=new-description')
expected = {'volume': {
'name': 'new-name',
'description': 'new-description',
}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# Call rename with no arguments
self.assertRaises(SystemExit, self.run_command, 'rename')
def test_rename_snapshot(self):
# basic rename with positional arguments
self.run_command('snapshot-rename 1234 new-name')
expected = {'snapshot': {'name': 'new-name'}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# change description only
self.run_command('snapshot-rename 1234 '
'--description=new-description')
expected = {'snapshot': {'description': 'new-description'}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# snapshot-rename and change description
self.run_command('snapshot-rename 1234 new-name '
'--description=new-description')
expected = {'snapshot': {
'name': 'new-name',
'description': 'new-description',
}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# Call snapshot-rename with no arguments
self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
def test_set_metadata_set(self):
self.run_command('metadata 1234 set key1=val1 key2=val2')
self.assert_called('POST', '/volumes/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_set_metadata_delete_dict(self):
self.run_command('metadata 1234 unset key1=val1 key2=val2')
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
def test_set_metadata_delete_keys(self):
self.run_command('metadata 1234 unset key1 key2')
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
def test_reset_state(self):
self.run_command('reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_with_flag(self):
self.run_command('reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_multiple(self):
self.run_command('reset-state 1234 5678 --state error')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called_anytime('POST', '/volumes/1234/action',
body=expected)
self.assert_called_anytime('POST', '/volumes/5678/action',
body=expected)
def test_snapshot_reset_state(self):
self.run_command('snapshot-reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)
def test_snapshot_reset_state_with_flag(self):
self.run_command('snapshot-reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)
def test_snapshot_reset_state_multiple(self):
self.run_command('snapshot-reset-state 1234 5678')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called_anytime('POST', '/snapshots/1234/action',
body=expected)
self.assert_called_anytime('POST', '/snapshots/5678/action',
body=expected)
def test_encryption_type_list(self):
"""
Test encryption-type-list shell command.
Verify a series of GET requests are made:
- one to get the volume type list information
- one per volume type to retrieve the encryption type information
"""
self.run_command('encryption-type-list')
self.assert_called_anytime('GET', '/types')
self.assert_called_anytime('GET', '/types/1/encryption')
self.assert_called_anytime('GET', '/types/2/encryption')
def test_encryption_type_show(self):
"""
Test encryption-type-show shell command.
Verify two GET requests are made per command invocation:
- one to get the volume type information
- one to get the encryption type information
"""
self.run_command('encryption-type-show 1')
self.assert_called('GET', '/types/1/encryption')
self.assert_called_anytime('GET', '/types/1')
def test_encryption_type_create(self):
"""
Test encryption-type-create shell command.
Verify GET and POST requests are made per command invocation:
- one GET request to retrieve the relevant volume type information
- one POST request to create the new encryption type
"""
expected = {'encryption': {'cipher': None, 'key_size': None,
'provider': 'TestProvider',
'control_location': None}}
self.run_command('encryption-type-create 2 TestProvider')
self.assert_called('POST', '/types/2/encryption', body=expected)
self.assert_called_anytime('GET', '/types/2')
def test_encryption_type_update(self):
"""
Test encryption-type-update shell command.
Verify two GETs/one PUT requests are made per command invocation:
- one GET request to retrieve the relevant volume type information
- one GET request to retrieve the relevant encryption type information
- one PUT request to update the encryption type information
"""
self.skipTest("Not implemented")
def test_encryption_type_delete(self):
"""
Test encryption-type-delete shell command.
Verify one GET/one DELETE requests are made per command invocation:
- one GET request to retrieve the relevant volume type information
- one DELETE request to delete the encryption type information
"""
self.run_command('encryption-type-delete 1')
self.assert_called('DELETE', '/types/1/encryption/provider')
self.assert_called_anytime('GET', '/types/1')
def test_migrate_volume(self):
self.run_command('migrate 1234 fakehost --force-host-copy=True')
expected = {'os-migrate_volume': {'force_host_copy': 'True',
'host': 'fakehost'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_snapshot_metadata_set(self):
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
self.assert_called('POST', '/snapshots/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_snapshot_metadata_unset_dict(self):
self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
def test_snapshot_metadata_unset_keys(self):
self.run_command('snapshot-metadata 1234 unset key1 key2')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
def test_volume_metadata_update_all(self):
self.run_command('metadata-update-all 1234 key1=val1 key2=val2')
self.assert_called('PUT', '/volumes/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_snapshot_metadata_update_all(self):
self.run_command('snapshot-metadata-update-all\
1234 key1=val1 key2=val2')
self.assert_called('PUT', '/snapshots/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_readonly_mode_update(self):
self.run_command('readonly-mode-update 1234 True')
expected = {'os-update_readonly_flag': {'readonly': True}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
self.run_command('readonly-mode-update 1234 False')
expected = {'os-update_readonly_flag': {'readonly': False}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_service_disable(self):
self.run_command('service-disable host cinder-volume')
self.assert_called('PUT', '/os-services/disable',
{"binary": "cinder-volume", "host": "host"})
def test_service_enable(self):
self.run_command('service-enable host cinder-volume')
self.assert_called('PUT', '/os-services/enable',
{"binary": "cinder-volume", "host": "host"})
def test_retype_with_policy(self):
self.run_command('retype 1234 foo --migration-policy=on-demand')
expected = {'os-retype': {'new_type': 'foo',
'migration_policy': 'on-demand'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_retype_default_policy(self):
self.run_command('retype 1234 foo')
expected = {'os-retype': {'new_type': 'foo',
'migration_policy': 'never'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_snapshot_delete(self):
self.run_command('snapshot-delete 1234')
self.assert_called('DELETE', '/snapshots/1234')

View File

@@ -0,0 +1,35 @@
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class SnapshotActionsTest(utils.TestCase):
def test_update_snapshot_status(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available'})
cs.assert_called('POST', '/snapshots/1234/action')
def test_update_snapshot_status_with_progress(self):
s = cs.volume_snapshots.get('1234')
cs.volume_snapshots.update_snapshot_status(s,
{'status': 'available',
'progress': '73%'})
cs.assert_called('POST', '/snapshots/1234/action')

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -15,8 +15,8 @@
# under the License.
from cinderclient.v2 import volume_types
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()

View File

@@ -0,0 +1,53 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class VolumeBackupsTest(utils.TestCase):
def test_create(self):
cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4')
cs.assert_called('POST', '/backups')
def test_get(self):
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
cs.backups.get(backup_id)
cs.assert_called('GET', '/backups/%s' % backup_id)
def test_list(self):
cs.backups.list()
cs.assert_called('GET', '/backups/detail')
def test_delete(self):
b = cs.backups.list()[0]
b.delete()
cs.assert_called('DELETE',
'/backups/76a17945-3c6f-435c-975b-b5685db10b62')
cs.backups.delete('76a17945-3c6f-435c-975b-b5685db10b62')
cs.assert_called('DELETE',
'/backups/76a17945-3c6f-435c-975b-b5685db10b62')
cs.backups.delete(b)
cs.assert_called('DELETE',
'/backups/76a17945-3c6f-435c-975b-b5685db10b62')
def test_restore(self):
backup_id = '76a17945-3c6f-435c-975b-b5685db10b62'
cs.restores.restore(backup_id)
cs.assert_called('POST', '/backups/%s/restore' % backup_id)

View File

@@ -0,0 +1,100 @@
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.v2.volume_encryption_types import VolumeEncryptionType
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
class VolumeEncryptionTypesTest(utils.TestCase):
"""
Test suite for the Volume Encryption Types Resource and Manager.
"""
def test_list(self):
"""
Unit test for VolumeEncryptionTypesManager.list
Verify that a series of GET requests are made:
- one GET request for the list of volume types
- one GET request per volume type for encryption type information
Verify that all returned information is :class: VolumeEncryptionType
"""
encryption_types = cs.volume_encryption_types.list()
cs.assert_called_anytime('GET', '/types')
cs.assert_called_anytime('GET', '/types/2/encryption')
cs.assert_called_anytime('GET', '/types/1/encryption')
for encryption_type in encryption_types:
self.assertIsInstance(encryption_type, VolumeEncryptionType)
def test_get(self):
"""
Unit test for VolumeEncryptionTypesManager.get
Verify that one GET request is made for the volume type encryption
type information. Verify that returned information is :class:
VolumeEncryptionType
"""
encryption_type = cs.volume_encryption_types.get(1)
cs.assert_called('GET', '/types/1/encryption')
self.assertIsInstance(encryption_type, VolumeEncryptionType)
def test_get_no_encryption(self):
"""
Unit test for VolumeEncryptionTypesManager.get
Verify that a request on a volume type with no associated encryption
type information returns a VolumeEncryptionType with no attributes.
"""
encryption_type = cs.volume_encryption_types.get(2)
self.assertIsInstance(encryption_type, VolumeEncryptionType)
self.assertFalse(hasattr(encryption_type, 'id'),
'encryption type has an id')
def test_create(self):
"""
Unit test for VolumeEncryptionTypesManager.create
Verify that one POST request is made for the encryption type creation.
Verify that encryption type creation returns a VolumeEncryptionType.
"""
result = cs.volume_encryption_types.create(2, {'encryption':
{'provider': 'Test',
'key_size': None,
'cipher': None,
'control_location':
None}})
cs.assert_called('POST', '/types/2/encryption')
self.assertIsInstance(result, VolumeEncryptionType)
def test_update(self):
"""
Unit test for VolumeEncryptionTypesManager.update
"""
self.skipTest("Not implemented")
def test_delete(self):
"""
Unit test for VolumeEncryptionTypesManager.delete
Verify that one DELETE request is made for encryption type deletion
Verify that encryption type deletion returns None
"""
result = cs.volume_encryption_types.delete(1)
cs.assert_called('DELETE', '/types/1/encryption/provider')
self.assertIsNone(result, "delete result must be None")

View File

@@ -0,0 +1,51 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class VolumeTransfersTest(utils.TestCase):
def test_create(self):
cs.transfers.create('1234')
cs.assert_called('POST', '/os-volume-transfer')
def test_get(self):
transfer_id = '5678'
cs.transfers.get(transfer_id)
cs.assert_called('GET', '/os-volume-transfer/%s' % transfer_id)
def test_list(self):
cs.transfers.list()
cs.assert_called('GET', '/os-volume-transfer/detail')
def test_delete(self):
b = cs.transfers.list()[0]
b.delete()
cs.assert_called('DELETE', '/os-volume-transfer/5678')
cs.transfers.delete('5678')
cs.assert_called('DELETE', '/os-volume-transfer/5678')
cs.transfers.delete(b)
cs.assert_called('DELETE', '/os-volume-transfer/5678')
def test_accept(self):
transfer_id = '5678'
auth_key = '12345'
cs.transfers.accept(transfer_id, auth_key)
cs.assert_called('POST', '/os-volume-transfer/%s/accept' % transfer_id)

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -14,8 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
@@ -32,13 +32,13 @@ class VolumesTest(utils.TestCase):
cs.volumes.delete(v)
cs.assert_called('DELETE', '/volumes/1234')
def test_create_keypair(self):
def test_create_volume(self):
cs.volumes.create(1)
cs.assert_called('POST', '/volumes')
def test_attach(self):
v = cs.volumes.get('1234')
cs.volumes.attach(v, 1, '/dev/vdc')
cs.volumes.attach(v, 1, '/dev/vdc', mode='ro')
cs.assert_called('POST', '/volumes/1234/action')
def test_detach(self):
@@ -85,3 +85,34 @@ class VolumesTest(utils.TestCase):
keys = ['key1']
cs.volumes.delete_metadata(1234, keys)
cs.assert_called('DELETE', '/volumes/1234/metadata/key1')
def test_extend(self):
v = cs.volumes.get('1234')
cs.volumes.extend(v, 2)
cs.assert_called('POST', '/volumes/1234/action')
def test_get_encryption_metadata(self):
cs.volumes.get_encryption_metadata('1234')
cs.assert_called('GET', '/volumes/1234/encryption')
def test_migrate(self):
v = cs.volumes.get('1234')
cs.volumes.migrate_volume(v, 'dest', False)
cs.assert_called('POST', '/volumes/1234/action')
def test_metadata_update_all(self):
cs.volumes.update_all_metadata(1234, {'k1': 'v1'})
cs.assert_called('PUT', '/volumes/1234/metadata',
{'metadata': {'k1': 'v1'}})
def test_readonly_mode_update(self):
v = cs.volumes.get('1234')
cs.volumes.update_readonly_flag(v, True)
cs.assert_called('POST', '/volumes/1234/action')
def test_retype(self):
v = cs.volumes.get('1234')
cs.volumes.retype(v, 'foo', 'on-demand')
cs.assert_called('POST', '/volumes/1234/action',
{'os-retype': {'new_type': 'foo',
'migration_policy': 'on-demand'}})

View File

@@ -1,8 +1,26 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import os
import re
import sys
import uuid
import six
import prettytable
from cinderclient import exceptions
@@ -38,7 +56,7 @@ def add_arg(f, *args, **kwargs):
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in f.arguments:
# Because of the sematics of decorator composition if we just append
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
f.arguments.insert(0, (args, kwargs))
@@ -70,8 +88,12 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
if conflicting_keys and not allow_conflicts:
raise Exception("Hook '%(hook_name)s' is attempting to redefine"
" attributes '%(conflicting_keys)s'" % locals())
msg = ("Hook '%(hook_name)s' is attempting to redefine attributes "
"'%(conflicting_keys)s'" % {
'hook_name': hook_name,
'conflicting_keys': conflicting_keys
})
raise Exception(msg)
extra_kwargs.update(hook_kwargs)
@@ -124,7 +146,14 @@ def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def print_list(objs, fields, formatters={}):
def _print(pt, order):
if sys.version_info >= (3, 0):
print(pt.get_string(sortby=order))
else:
print(strutils.safe_encode(pt.get_string(sortby=order)))
def print_list(objs, fields, formatters={}, order_by=None):
mixed_case_fields = ['serverId']
pt = prettytable.PrettyTable([f for f in fields], caching=False)
pt.aligns = ['l' for f in fields]
@@ -139,18 +168,23 @@ def print_list(objs, fields, formatters={}):
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
if type(o) == dict and field in o:
data = o[field]
else:
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)
print strutils.safe_encode(pt.get_string(sortby=fields[0]))
if order_by is None:
order_by = fields[0]
_print(pt, order_by)
def print_dict(d, property="Property"):
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
pt.aligns = ['l', 'l']
[pt.add_row(list(r)) for r in d.iteritems()]
print strutils.safe_encode(pt.get_string(sortby=property))
[pt.add_row(list(r)) for r in six.iteritems(d)]
_print(pt, property)
def find_resource(manager, name_or_id):
@@ -162,9 +196,12 @@ def find_resource(manager, name_or_id):
except exceptions.NotFound:
pass
if sys.version_info <= (3, 0):
name_or_id = strutils.safe_decode(name_or_id)
# now try to get entity as uuid
try:
uuid.UUID(strutils.safe_decode(name_or_id))
uuid.UUID(name_or_id)
return manager.get(name_or_id)
except (ValueError, exceptions.NotFound):
pass
@@ -196,9 +233,14 @@ def find_resource(manager, name_or_id):
raise exceptions.CommandError(msg)
def find_volume(cs, volume):
"""Get a volume by name or ID."""
return find_resource(cs.volumes, volume)
def _format_servers_list_networks(server):
output = []
for (network, addresses) in server.networks.items():
for (network, addresses) in list(server.networks.items()):
if len(addresses) == 0:
continue
addresses_csv = ', '.join(addresses)
@@ -258,8 +300,8 @@ def slugify(value):
From Django's "django/template/defaultfilters.py".
"""
import unicodedata
if not isinstance(value, unicode):
value = unicode(value)
if not isinstance(value, six.text_type):
value = six.text_type(value)
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(_slugify_strip_re.sub('', value).strip().lower())
value = six.text_type(_slugify_strip_re.sub('', value).strip().lower())
return _slugify_hyphenate_re.sub('-', value)

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2012 OpenStack, LLC.
# Copyright (c) 2012 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.v1.client import Client
from cinderclient.v1.client import Client # noqa

View File

@@ -0,0 +1,42 @@
# Copyright 2011-2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Availability Zone interface (v1 extension)"""
from cinderclient import base
class AvailabilityZone(base.Resource):
NAME_ATTR = 'display_name'
def __repr__(self):
return "<AvailabilityZone: %s>" % self.zoneName
class AvailabilityZoneManager(base.ManagerWithFind):
"""Manage :class:`AvailabilityZone` resources."""
resource_class = AvailabilityZone
def list(self, detailed=False):
"""Get a list of all availability zones
:rtype: list of :class:`AvailabilityZone`
"""
if detailed is True:
return self._list("/os-availability-zone/detail",
"availabilityZoneInfo")
else:
return self._list("/os-availability-zone", "availabilityZoneInfo")

View File

@@ -1,10 +1,32 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient import client
from cinderclient.v1 import availability_zones
from cinderclient.v1 import limits
from cinderclient.v1 import qos_specs
from cinderclient.v1 import quota_classes
from cinderclient.v1 import quotas
from cinderclient.v1 import services
from cinderclient.v1 import volumes
from cinderclient.v1 import volume_snapshots
from cinderclient.v1 import volume_types
from cinderclient.v1 import volume_encryption_types
from cinderclient.v1 import volume_backups
from cinderclient.v1 import volume_backups_restore
from cinderclient.v1 import volume_transfers
class Client(object):
@@ -39,8 +61,17 @@ class Client(object):
self.volumes = volumes.VolumeManager(self)
self.volume_snapshots = volume_snapshots.SnapshotManager(self)
self.volume_types = volume_types.VolumeTypeManager(self)
self.volume_encryption_types = \
volume_encryption_types.VolumeEncryptionTypeManager(self)
self.qos_specs = qos_specs.QoSSpecsManager(self)
self.quota_classes = quota_classes.QuotaClassSetManager(self)
self.quotas = quotas.QuotaSetManager(self)
self.backups = volume_backups.VolumeBackupManager(self)
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
# Add in any extensions...
if extensions:
@@ -79,3 +110,6 @@ class Client(object):
credentials are wrong.
"""
self.client.authenticate()
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may

View File

@@ -1,17 +1,30 @@
# Copyright 2011 OpenStack LLC.
# Copyright 2011 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient import base
class Limits(base.Resource):
"""A collection of RateLimit and AbsoluteLimit objects"""
"""A collection of RateLimit and AbsoluteLimit objects."""
def __repr__(self):
return "<Limits>"
@property
def absolute(self):
for (name, value) in self._info['absolute'].items():
for (name, value) in list(self._info['absolute'].items()):
yield AbsoluteLimit(name, value)
@property
@@ -26,7 +39,7 @@ class Limits(base.Resource):
class RateLimit(object):
"""Data model that represents a flattened view of a single rate limit"""
"""Data model that represents a flattened view of a single rate limit."""
def __init__(self, verb, uri, regex, value, remain,
unit, next_available):
@@ -48,11 +61,11 @@ class RateLimit(object):
and self.next_available == other.next_available
def __repr__(self):
return "<RateLimit: method=%s uri=%s>" % (self.method, self.uri)
return "<RateLimit: method=%s uri=%s>" % (self.verb, self.uri)
class AbsoluteLimit(object):
"""Data model that represents a single absolute limit"""
"""Data model that represents a single absolute limit."""
def __init__(self, name, value):
self.name = name
@@ -66,7 +79,7 @@ class AbsoluteLimit(object):
class LimitsManager(base.Manager):
"""Manager object used to interact with limits resource"""
"""Manager object used to interact with limits resource."""
resource_class = Limits

View File

@@ -0,0 +1,149 @@
# Copyright (c) 2013 eBay Inc.
# Copyright (c) OpenStack LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
QoS Specs interface.
"""
from cinderclient import base
class QoSSpecs(base.Resource):
"""QoS specs entity represents quality-of-service parameters/requirements.
A QoS specs is a set of parameters or requirements for quality-of-service
purpose, which can be associated with volume types (for now). In future,
QoS specs may be extended to be associated other entities, such as single
volume.
"""
def __repr__(self):
return "<QoSSpecs: %s>" % self.name
def delete(self):
return self.manager.delete(self)
class QoSSpecsManager(base.ManagerWithFind):
"""
Manage :class:`QoSSpecs` resources.
"""
resource_class = QoSSpecs
def list(self):
"""Get a list of all qos specs.
:rtype: list of :class:`QoSSpecs`.
"""
return self._list("/qos-specs", "qos_specs")
def get(self, qos_specs):
"""Get a specific qos specs.
:param qos_specs: The ID of the :class:`QoSSpecs` to get.
:rtype: :class:`QoSSpecs`
"""
return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs")
def delete(self, qos_specs, force=False):
"""Delete a specific qos specs.
:param qos_specs: The ID of the :class:`QoSSpecs` to be removed.
:param force: Flag that indicates whether to delete target qos specs
if it was in-use.
"""
self._delete("/qos-specs/%s?force=%s" %
(base.getid(qos_specs), force))
def create(self, name, specs):
"""Create a qos specs.
:param name: Descriptive name of the qos specs, must be unique
:param specs: A dict of key/value pairs to be set
:rtype: :class:`QoSSpecs`
"""
body = {
"qos_specs": {
"name": name,
}
}
body["qos_specs"].update(specs)
return self._create("/qos-specs", body, "qos_specs")
def set_keys(self, qos_specs, specs):
"""Update a qos specs with new specifications.
:param qos_specs: The ID of qos specs
:param specs: A dict of key/value pairs to be set
:rtype: :class:`QoSSpecs`
"""
body = {
"qos_specs": {}
}
body["qos_specs"].update(specs)
return self._update("/qos-specs/%s" % qos_specs, body)
def unset_keys(self, qos_specs, specs):
"""Update a qos specs with new specifications.
:param qos_specs: The ID of qos specs
:param specs: A list of key to be unset
:rtype: :class:`QoSSpecs`
"""
body = {'keys': specs}
return self._update("/qos-specs/%s/delete_keys" % qos_specs,
body)
def get_associations(self, qos_specs):
"""Get associated entities of a qos specs.
:param qos_specs: The id of the :class: `QoSSpecs`
:return: a list of entities that associated with specific qos specs.
"""
return self._list("/qos-specs/%s/associations" % base.getid(qos_specs),
"qos_associations")
def associate(self, qos_specs, vol_type_id):
"""Associate a volume type with specific qos specs.
:param qos_specs: The qos specs to be associated with
:param vol_type_id: The volume type id to be associated with
"""
self.api.client.get("/qos-specs/%s/associate?vol_type_id=%s" %
(base.getid(qos_specs), vol_type_id))
def disassociate(self, qos_specs, vol_type_id):
"""Disassociate qos specs from volume type.
:param qos_specs: The qos specs to be associated with
:param vol_type_id: The volume type id to be associated with
"""
self.api.client.get("/qos-specs/%s/disassociate?vol_type_id=%s" %
(base.getid(qos_specs), vol_type_id))
def disassociate_all(self, qos_specs):
"""Disassociate all entities from specific qos specs.
:param qos_specs: The qos specs to be associated with
"""
self.api.client.get("/qos-specs/%s/disassociate_all" %
base.getid(qos_specs))

View File

@@ -1,4 +1,4 @@
# Copyright 2012 OpenStack LLC.
# Copyright (c) 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -21,32 +21,25 @@ class QuotaClassSet(base.Resource):
@property
def id(self):
"""QuotaClassSet does not have a 'id' attribute but base.Resource
needs it to self-refresh and QuotaSet is indexed by class_name"""
needs it to self-refresh and QuotaSet is indexed by class_name.
"""
return self.class_name
def update(self, *args, **kwargs):
self.manager.update(self.class_name, *args, **kwargs)
class QuotaClassSetManager(base.ManagerWithFind):
class QuotaClassSetManager(base.Manager):
resource_class = QuotaClassSet
def get(self, class_name):
return self._get("/os-quota-class-sets/%s" % (class_name),
"quota_class_set")
def update(self,
class_name,
volumes=None,
gigabytes=None):
def update(self, class_name, **updates):
body = {'quota_class_set': {'class_name': class_name}}
body = {'quota_class_set': {
'class_name': class_name,
'volumes': volumes,
'gigabytes': gigabytes}}
for key in body['quota_class_set'].keys():
if body['quota_class_set'][key] is None:
body['quota_class_set'].pop(key)
for update in updates:
body['quota_class_set'][update] = updates[update]
self._update('/os-quota-class-sets/%s' % (class_name), body)

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -20,33 +20,29 @@ class QuotaSet(base.Resource):
@property
def id(self):
"""QuotaSet does not have a 'id' attribute but base.Resource needs it
to self-refresh and QuotaSet is indexed by tenant_id"""
"""QuotaSet does not have a 'id' attribute but base. Resource needs it
to self-refresh and QuotaSet is indexed by tenant_id.
"""
return self.tenant_id
def update(self, *args, **kwargs):
self.manager.update(self.tenant_id, *args, **kwargs)
class QuotaSetManager(base.ManagerWithFind):
class QuotaSetManager(base.Manager):
resource_class = QuotaSet
def get(self, tenant_id):
def get(self, tenant_id, usage=False):
if hasattr(tenant_id, 'tenant_id'):
tenant_id = tenant_id.tenant_id
return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set")
return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage),
"quota_set")
def update(self, tenant_id, volumes=None, snapshots=None, gigabytes=None):
def update(self, tenant_id, **updates):
body = {'quota_set': {'tenant_id': tenant_id}}
body = {'quota_set': {
'tenant_id': tenant_id,
'volumes': volumes,
'snapshots': snapshots,
'gigabytes': gigabytes}}
for key in body['quota_set'].keys():
if body['quota_set'][key] is None:
body['quota_set'].pop(key)
for update in updates:
body['quota_set'][update] = updates[update]
self._update('/os-quota-sets/%s' % (tenant_id), body)

View File

@@ -0,0 +1,58 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
service interface
"""
from cinderclient import base
class Service(base.Resource):
def __repr__(self):
return "<Service: %s>" % self.service
class ServiceManager(base.ManagerWithFind):
resource_class = Service
def list(self, host=None, binary=None):
"""
Describes service list for host.
:param host: destination host name.
:param binary: service binary.
"""
url = "/os-services"
filters = []
if host:
filters.append("host=%s" % host)
if binary:
filters.append("binary=%s" % binary)
if filters:
url = "%s?%s" % (url, "&".join(filters))
return self._list(url, "services")
def enable(self, host, binary):
"""Enable the service specified by hostname and binary."""
body = {"host": host, "binary": binary}
result = self._update("/os-services/enable", body)
return self.resource_class(self, result)
def disable(self, host, binary):
"""Enable the service specified by hostname and binary."""
body = {"host": host, "binary": binary}
result = self._update("/os-services/disable", body)
return self.resource_class(self, result)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Volume Backups interface (1.1 extension).
"""
from cinderclient import base
class VolumeBackup(base.Resource):
"""A volume backup is a block level backup of a volume."""
def __repr__(self):
return "<VolumeBackup: %s>" % self.id
def delete(self):
"""Delete this volume backup."""
return self.manager.delete(self)
class VolumeBackupManager(base.ManagerWithFind):
"""Manage :class:`VolumeBackup` resources."""
resource_class = VolumeBackup
def create(self, volume_id, container=None,
name=None, description=None):
"""Create a volume backup.
:param volume_id: The ID of the volume to backup.
:param container: The name of the backup service container.
:param name: The name of the backup.
:param description: The description of the backup.
:rtype: :class:`VolumeBackup`
"""
body = {'backup': {'volume_id': volume_id,
'container': container,
'name': name,
'description': description}}
return self._create('/backups', body, 'backup')
def get(self, backup_id):
"""Show details of a volume backup.
:param backup_id: The ID of the backup to display.
:rtype: :class:`VolumeBackup`
"""
return self._get("/backups/%s" % backup_id, "backup")
def list(self, detailed=True):
"""Get a list of all volume backups.
:rtype: list of :class:`VolumeBackup`
"""
if detailed is True:
return self._list("/backups/detail", "backups")
else:
return self._list("/backups", "backups")
def delete(self, backup):
"""Delete a volume backup.
:param backup: The :class:`VolumeBackup` to delete.
"""
self._delete("/backups/%s" % base.getid(backup))

View File

@@ -0,0 +1,43 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Volume Backups Restore interface (1.1 extension).
This is part of the Volume Backups interface.
"""
from cinderclient import base
class VolumeBackupsRestore(base.Resource):
"""A Volume Backups Restore represents a restore operation."""
def __repr__(self):
return "<VolumeBackupsRestore: %s>" % self.volume_id
class VolumeBackupRestoreManager(base.Manager):
"""Manage :class:`VolumeBackupsRestore` resources."""
resource_class = VolumeBackupsRestore
def restore(self, backup_id, volume_id=None):
"""Restore a backup to a volume.
:param backup_id: The ID of the backup to restore.
:param volume_id: The ID of the volume to restore the backup to.
:rtype: :class:`Restore`
"""
body = {'restore': {'volume_id': volume_id}}
return self._create("/backups/%s/restore" % backup_id,
body, "restore")

View File

@@ -0,0 +1,97 @@
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Volume Encryption Type interface
"""
from cinderclient import base
class VolumeEncryptionType(base.Resource):
"""
A Volume Encryption Type is a collection of settings used to conduct
encryption for a specific volume type.
"""
def __repr__(self):
return "<VolumeEncryptionType: %s>" % self.name
class VolumeEncryptionTypeManager(base.ManagerWithFind):
"""
Manage :class: `VolumeEncryptionType` resources.
"""
resource_class = VolumeEncryptionType
def list(self, search_opts=None):
"""
List all volume encryption types.
:param volume_types: a list of volume types
:return: a list of :class: VolumeEncryptionType instances
"""
# Since the encryption type is a volume type extension, we cannot get
# all encryption types without going through all volume types.
volume_types = self.api.volume_types.list()
encryption_types = []
for volume_type in volume_types:
encryption_type = self._get("/types/%s/encryption"
% base.getid(volume_type))
if hasattr(encryption_type, 'volume_type_id'):
encryption_types.append(encryption_type)
return encryption_types
def get(self, volume_type):
"""
Get the volume encryption type for the specified volume type.
:param volume_type: the volume type to query
:return: an instance of :class: VolumeEncryptionType
"""
return self._get("/types/%s/encryption" % base.getid(volume_type))
def create(self, volume_type, specs):
"""
Create a new encryption type for the specified volume type.
:param volume_type: the volume type on which to add an encryption type
:param specs: the encryption type specifications to add
:return: an instance of :class: VolumeEncryptionType
"""
body = {'encryption': specs}
return self._create("/types/%s/encryption" % base.getid(volume_type),
body, "encryption")
def update(self, volume_type, specs):
"""
Update the encryption type information for the specified volume type.
:param volume_type: the volume type whose encryption type information
must be updated
:param specs: the encryption type specifications to update
:return: an instance of :class: VolumeEncryptionType
"""
raise NotImplementedError()
def delete(self, volume_type):
"""
Delete the encryption type information for the specified volume type.
:param volume_type: the volume type whose encryption type information
must be deleted
"""
return self._delete("/types/%s/encryption/provider" %
base.getid(volume_type))

View File

@@ -17,8 +17,13 @@
Volume snapshot interface (1.1 extension).
"""
import urllib
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
import six
class Snapshot(base.Resource):
@@ -48,6 +53,22 @@ class Snapshot(base.Resource):
def project_id(self):
return self._info.get('os-extended-snapshot-attributes:project_id')
def reset_state(self, state):
"""Update the snapshot with the privided state."""
self.manager.reset_state(self, state)
def set_metadata(self, metadata):
"""Set metadata of this snapshot."""
return self.manager.set_metadata(self, metadata)
def delete_metadata(self, keys):
"""Delete metadata of this snapshot."""
return self.manager.delete_metadata(self, keys)
def update_all_metadata(self, metadata):
"""Update_all metadata of this snapshot."""
return self.manager.update_all_metadata(self, metadata)
class SnapshotManager(base.ManagerWithFind):
"""
@@ -95,11 +116,17 @@ class SnapshotManager(base.ManagerWithFind):
qparams = {}
for opt, val in search_opts.iteritems():
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
# Transform the dict to a sequence of two-element tuples in fixed
# order, then the encoded string will be consistent in Python 2&3.
if qparams:
new_qparams = sorted(qparams.items(), key=lambda x: x[0])
query_string = "?%s" % urlencode(new_qparams)
else:
query_string = ""
detail = ""
if detailed:
@@ -128,3 +155,48 @@ class SnapshotManager(base.ManagerWithFind):
body = {"snapshot": kwargs}
self._update("/snapshots/%s" % base.getid(snapshot), body)
def reset_state(self, snapshot, state):
"""Update the specified volume with the provided state."""
return self._action('os-reset_status', snapshot, {'status': state})
def _action(self, action, snapshot, info=None, **kwargs):
"""Perform a snapshot action."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/snapshots/%s/action' % base.getid(snapshot)
return self.api.client.post(url, body=body)
def update_snapshot_status(self, snapshot, update_dict):
return self._action('os-update_snapshot_status',
base.getid(snapshot), update_dict)
def set_metadata(self, snapshot, metadata):
"""Update/Set a snapshots metadata.
:param snapshot: The :class:`Snapshot`.
:param metadata: A list of keys to be set.
"""
body = {'metadata': metadata}
return self._create("/snapshots/%s/metadata" % base.getid(snapshot),
body, "metadata")
def delete_metadata(self, snapshot, keys):
"""Delete specified keys from snapshot metadata.
:param snapshot: The :class:`Snapshot`.
:param keys: A list of keys to be removed.
"""
snapshot_id = base.getid(snapshot)
for k in keys:
self._delete("/snapshots/%s/metadata/%s" % (snapshot_id, k))
def update_all_metadata(self, snapshot, metadata):
"""Update_all snapshot metadata.
:param snapshot: The :class:`Snapshot`.
:param metadata: A list of keys to be updated.
"""
body = {'metadata': metadata}
return self._update("/snapshots/%s/metadata" % base.getid(snapshot),
body)

View File

@@ -0,0 +1,82 @@
# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Volume transfer interface (1.1 extension).
"""
from cinderclient import base
class VolumeTransfer(base.Resource):
"""Transfer a volume from one tenant to another"""
def __repr__(self):
return "<VolumeTransfer: %s>" % self.id
def delete(self):
"""Delete this volume transfer."""
return self.manager.delete(self)
class VolumeTransferManager(base.ManagerWithFind):
"""Manage :class:`VolumeTransfer` resources."""
resource_class = VolumeTransfer
def create(self, volume_id, name=None):
"""Create a volume transfer.
:param volume_id: The ID of the volume to transfer.
:param name: The name of the transfer.
:rtype: :class:`VolumeTransfer`
"""
body = {'transfer': {'volume_id': volume_id,
'name': name}}
return self._create('/os-volume-transfer', body, 'transfer')
def accept(self, transfer_id, auth_key):
"""Accept a volume transfer.
:param transfer_id: The ID of the trasnfer to accept.
:param auth_key: The auth_key of the transfer.
:rtype: :class:`VolumeTransfer`
"""
body = {'accept': {'auth_key': auth_key}}
return self._create('/os-volume-transfer/%s/accept' % transfer_id,
body, 'transfer')
def get(self, transfer_id):
"""Show details of a volume transfer.
:param transfer_id: The ID of the volume transfer to display.
:rtype: :class:`VolumeTransfer`
"""
return self._get("/os-volume-transfer/%s" % transfer_id, "transfer")
def list(self, detailed=True, search_opts=None):
"""Get a list of all volume transfer.
:rtype: list of :class:`VolumeTransfer`
"""
if detailed is True:
return self._list("/os-volume-transfer/detail", "transfers")
else:
return self._list("/os-volume-transfer", "transfers")
def delete(self, transfer_id):
"""Delete a volume transfer.
:param transfer_id: The :class:`VolumeTransfer` to delete.
"""
self._delete("/os-volume-transfer/%s" % base.getid(transfer_id))

View File

@@ -55,7 +55,7 @@ class VolumeType(base.Resource):
def unset_keys(self, keys):
"""
Unset extra specs on a volue type.
Unset extra specs on a volume type.
:param type_id: The :class:`VolumeType` to unset extra spec on
:param keys: A list of keys to be unset
@@ -65,7 +65,7 @@ class VolumeType(base.Resource):
# the return in the loop resulted in ony ONE key being unset.
# since on success the return was NONE, we'll only interrupt the loop
# and return if there's an error
result = None
resp = None
for k in keys:
resp = self.manager._delete(
"/types/%s/extra_specs/%s" % (
@@ -80,7 +80,7 @@ class VolumeTypeManager(base.ManagerWithFind):
"""
resource_class = VolumeType
def list(self):
def list(self, search_opts=None):
"""
Get a list of all volume types.

View File

@@ -17,109 +17,125 @@
Volume interface (1.1 extension).
"""
import urllib
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import six
from cinderclient import base
class Volume(base.Resource):
"""
A volume is an extra block level storage to the OpenStack instances.
"""
"""A volume is an extra block level storage to the OpenStack instances."""
def __repr__(self):
return "<Volume: %s>" % self.id
def delete(self):
"""
Delete this volume.
"""
"""Delete this volume."""
self.manager.delete(self)
def update(self, **kwargs):
"""
Update the display_name or display_description for this volume.
"""
"""Update the display_name or display_description for this volume."""
self.manager.update(self, **kwargs)
def attach(self, instance_uuid, mountpoint):
"""
Set attachment metadata.
def attach(self, instance_uuid, mountpoint, mode='rw'):
"""Set attachment metadata.
:param instance_uuid: uuid of the attaching instance.
:param mountpoint: mountpoint on the attaching instance.
:param mode: the access mode
"""
return self.manager.attach(self, instance_uuid, mountpoint)
return self.manager.attach(self, instance_uuid, mountpoint, mode)
def detach(self):
"""
Clear attachment metadata.
"""
"""Clear attachment metadata."""
return self.manager.detach(self)
def reserve(self, volume):
"""
Reserve this volume.
"""
"""Reserve this volume."""
return self.manager.reserve(self)
def unreserve(self, volume):
"""
Unreserve this volume.
"""
"""Unreserve this volume."""
return self.manager.unreserve(self)
def begin_detaching(self, volume):
"""
Begin detaching volume.
"""
"""Begin detaching volume."""
return self.manager.begin_detaching(self)
def roll_detaching(self, volume):
"""
Roll detaching volume.
"""
"""Roll detaching volume."""
return self.manager.roll_detaching(self)
def initialize_connection(self, volume, connector):
"""
Initialize a volume connection.
"""Initialize a volume connection.
:param connector: connector dict from nova.
"""
return self.manager.initialize_connection(self, connector)
def terminate_connection(self, volume, connector):
"""
Terminate a volume connection.
"""Terminate a volume connection.
:param connector: connector dict from nova.
"""
return self.manager.terminate_connection(self, connector)
def set_metadata(self, volume, metadata):
"""
Set or Append metadata to a volume.
"""Set or Append metadata to a volume.
:param type : The :class: `Volume` to set metadata on
:param volume : The :class: `Volume` to set metadata on
:param metadata: A dict of key/value pairs to set
"""
return self.manager.set_metadata(self, metadata)
def upload_to_image(self, force, image_name, container_format,
disk_format):
"""
Upload a volume to image service as an image.
"""
self.manager.upload_to_image(self, force, image_name, container_format,
disk_format)
"""Upload a volume to image service as an image."""
return self.manager.upload_to_image(self, force, image_name,
container_format, disk_format)
def force_delete(self):
"""
Delete the specififed volume ignoring it's current state.
"""Delete the specified volume ignoring its current state.
:param volume: The UUID of the volume to force-delete.
"""
self.manager.force_delete(self)
def reset_state(self, state):
"""Update the volume with the provided state."""
self.manager.reset_state(self, state)
def extend(self, volume, new_size):
"""Extend the size of the specified volume.
:param volume: The UUID of the volume to extend.
:param new_size: The desired size to extend volume to.
"""
self.manager.extend(self, volume, new_size)
def migrate_volume(self, host, force_host_copy):
"""Migrate the volume to a new host."""
self.manager.migrate_volume(self, host, force_host_copy)
# def migrate_volume_completion(self, old_volume, new_volume, error):
# """Complete the migration of the volume."""
# self.manager.migrate_volume_completion(self, old_volume,
# new_volume, error)
def update_all_metadata(self, metadata):
"""Update all metadata of this volume."""
return self.manager.update_all_metadata(self, metadata)
def update_readonly_flag(self, volume, read_only):
"""Update the read-only access mode flag of the specified volume.
:param volume: The UUID of the volume to update.
:param read_only: The value to indicate whether to update volume to
read-only access mode.
"""
self.manager.update_readonly_flag(self, volume, read_only)
class VolumeManager(base.ManagerWithFind):
"""
@@ -140,13 +156,13 @@ class VolumeManager(base.ManagerWithFind):
:param display_name: Name of the volume
:param display_description: Description of the volume
:param volume_type: Type of volume
:rtype: :class:`Volume`
:param user_id: User id derived from context
:param project_id: Project id derived from context
:param availability_zone: Availability Zone to use
:param metadata: Optional metadata to set on volume creation
:param imageRef: reference to an image stored in glance
:param source_volid: ID of source volume to clone from
:rtype: :class:`Volume`
"""
if metadata is None:
@@ -190,11 +206,11 @@ class VolumeManager(base.ManagerWithFind):
qparams = {}
for opt, val in search_opts.iteritems():
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
@@ -215,7 +231,7 @@ class VolumeManager(base.ManagerWithFind):
"""
Update the display_name or display_description for a volume.
:param volume: The :class:`Volume` to delete.
:param volume: The :class:`Volume` to update.
"""
if not kwargs:
return
@@ -233,7 +249,7 @@ class VolumeManager(base.ManagerWithFind):
url = '/volumes/%s/action' % base.getid(volume)
return self.api.client.post(url, body=body)
def attach(self, volume, instance_uuid, mountpoint):
def attach(self, volume, instance_uuid, mountpoint, mode='rw'):
"""
Set attachment metadata.
@@ -241,11 +257,13 @@ class VolumeManager(base.ManagerWithFind):
you would like to attach.
:param instance_uuid: uuid of the attaching instance.
:param mountpoint: mountpoint on the attaching instance.
:param mode: the access mode.
"""
return self._action('os-attach',
volume,
{'instance_uuid': instance_uuid,
'mountpoint': mountpoint})
'mountpoint': mountpoint,
'mode': mode})
def detach(self, volume):
"""
@@ -328,7 +346,7 @@ class VolumeManager(base.ManagerWithFind):
Delete specified keys from volumes metadata.
:param volume: The :class:`Volume`.
:param metadata: A list of keys to be removed.
:param keys: A list of keys to be removed.
"""
for k in keys:
self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k))
@@ -349,3 +367,61 @@ class VolumeManager(base.ManagerWithFind):
def force_delete(self, volume):
return self._action('os-force_delete', base.getid(volume))
def reset_state(self, volume, state):
"""Update the provided volume with the provided state."""
return self._action('os-reset_status', volume, {'status': state})
def extend(self, volume, new_size):
return self._action('os-extend',
base.getid(volume),
{'new_size': new_size})
def get_encryption_metadata(self, volume_id):
"""
Retrieve the encryption metadata from the desired volume.
:param volume_id: the id of the volume to query
:return: a dictionary of volume encryption metadata
"""
return self._get("/volumes/%s/encryption" % volume_id)._info
def migrate_volume(self, volume, host, force_host_copy):
"""Migrate volume to new host.
:param volume: The :class:`Volume` to migrate
:param host: The destination host
:param force_host_copy: Skip driver optimizations
"""
return self._action('os-migrate_volume',
volume,
{'host': host, 'force_host_copy': force_host_copy})
def migrate_volume_completion(self, old_volume, new_volume, error):
"""Complete the migration from the old volume to the temp new one.
:param old_volume: The original :class:`Volume` in the migration
:param new_volume: The new temporary :class:`Volume` in the migration
:param error: Inform of an error to cause migration cleanup
"""
new_volume_id = base.getid(new_volume)
return self._action('os-migrate_volume_completion',
old_volume,
{'new_volume': new_volume_id, 'error': error})[1]
def update_all_metadata(self, volume, metadata):
"""Update all metadata of a volume.
:param volume: The :class:`Volume`.
:param metadata: A list of keys to be updated.
"""
body = {'metadata': metadata}
return self._update("/volumes/%s/metadata" % base.getid(volume),
body)
def update_readonly_flag(self, volume, flag):
return self._action('os-update_readonly_flag',
base.getid(volume),
{'readonly': flag})

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.v2.client import Client
from cinderclient.v2.client import Client # noqa

View File

@@ -0,0 +1,42 @@
# Copyright 2011-2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Availability Zone interface (v2 extension)"""
from cinderclient import base
class AvailabilityZone(base.Resource):
NAME_ATTR = 'display_name'
def __repr__(self):
return "<AvailabilityZone: %s>" % self.zoneName
class AvailabilityZoneManager(base.ManagerWithFind):
"""Manage :class:`AvailabilityZone` resources."""
resource_class = AvailabilityZone
def list(self, detailed=False):
"""Get a list of all availability zones
:rtype: list of :class:`AvailabilityZone`
"""
if detailed is True:
return self._list("/os-availability-zone/detail",
"availabilityZoneInfo")
else:
return self._list("/os-availability-zone", "availabilityZoneInfo")

View File

@@ -1,10 +1,32 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient import client
from cinderclient.v1 import availability_zones
from cinderclient.v2 import limits
from cinderclient.v2 import qos_specs
from cinderclient.v2 import quota_classes
from cinderclient.v2 import quotas
from cinderclient.v2 import services
from cinderclient.v2 import volumes
from cinderclient.v2 import volume_snapshots
from cinderclient.v2 import volume_types
from cinderclient.v2 import volume_encryption_types
from cinderclient.v2 import volume_backups
from cinderclient.v2 import volume_backups_restore
from cinderclient.v1 import volume_transfers
class Client(object):
@@ -24,7 +46,7 @@ class Client(object):
insecure=False, timeout=None, tenant_id=None,
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='volume', service_name=None,
service_type='volumev2', service_name=None,
volume_service_name=None, retries=None,
http_log_debug=False,
cacert=None):
@@ -37,8 +59,17 @@ class Client(object):
self.volumes = volumes.VolumeManager(self)
self.volume_snapshots = volume_snapshots.SnapshotManager(self)
self.volume_types = volume_types.VolumeTypeManager(self)
self.volume_encryption_types = \
volume_encryption_types.VolumeEncryptionTypeManager(self)
self.qos_specs = qos_specs.QoSSpecsManager(self)
self.quota_classes = quota_classes.QuotaClassSetManager(self)
self.quotas = quotas.QuotaSetManager(self)
self.backups = volume_backups.VolumeBackupManager(self)
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
# Add in any extensions...
if extensions:
@@ -76,3 +107,6 @@ class Client(object):
credentials are wrong.
"""
self.client.authenticate()
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()

View File

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

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack LLC.
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may

View File

@@ -1,17 +1,30 @@
# Copyright 2013 OpenStack LLC.
# Copyright 2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient import base
class Limits(base.Resource):
"""A collection of RateLimit and AbsoluteLimit objects"""
"""A collection of RateLimit and AbsoluteLimit objects."""
def __repr__(self):
return "<Limits>"
@property
def absolute(self):
for (name, value) in self._info['absolute'].items():
for (name, value) in list(self._info['absolute'].items()):
yield AbsoluteLimit(name, value)
@property
@@ -26,7 +39,7 @@ class Limits(base.Resource):
class RateLimit(object):
"""Data model that represents a flattened view of a single rate limit"""
"""Data model that represents a flattened view of a single rate limit."""
def __init__(self, verb, uri, regex, value, remain,
unit, next_available):
@@ -48,11 +61,11 @@ class RateLimit(object):
and self.next_available == other.next_available
def __repr__(self):
return "<RateLimit: method=%s uri=%s>" % (self.method, self.uri)
return "<RateLimit: method=%s uri=%s>" % (self.verb, self.uri)
class AbsoluteLimit(object):
"""Data model that represents a single absolute limit"""
"""Data model that represents a single absolute limit."""
def __init__(self, name, value):
self.name = name
@@ -66,7 +79,7 @@ class AbsoluteLimit(object):
class LimitsManager(base.Manager):
"""Manager object used to interact with limits resource"""
"""Manager object used to interact with limits resource."""
resource_class = Limits

View File

@@ -0,0 +1,149 @@
# Copyright (c) 2013 eBay Inc.
# Copyright (c) OpenStack LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
QoS Specs interface.
"""
from cinderclient import base
class QoSSpecs(base.Resource):
"""QoS specs entity represents quality-of-service parameters/requirements.
A QoS specs is a set of parameters or requirements for quality-of-service
purpose, which can be associated with volume types (for now). In future,
QoS specs may be extended to be associated other entities, such as single
volume.
"""
def __repr__(self):
return "<QoSSpecs: %s>" % self.name
def delete(self):
return self.manager.delete(self)
class QoSSpecsManager(base.ManagerWithFind):
"""
Manage :class:`QoSSpecs` resources.
"""
resource_class = QoSSpecs
def list(self):
"""Get a list of all qos specs.
:rtype: list of :class:`QoSSpecs`.
"""
return self._list("/qos-specs", "qos_specs")
def get(self, qos_specs):
"""Get a specific qos specs.
:param qos_specs: The ID of the :class:`QoSSpecs` to get.
:rtype: :class:`QoSSpecs`
"""
return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs")
def delete(self, qos_specs, force=False):
"""Delete a specific qos specs.
:param qos_specs: The ID of the :class:`QoSSpecs` to be removed.
:param force: Flag that indicates whether to delete target qos specs
if it was in-use.
"""
self._delete("/qos-specs/%s?force=%s" %
(base.getid(qos_specs), force))
def create(self, name, specs):
"""Create a qos specs.
:param name: Descriptive name of the qos specs, must be unique
:param specs: A dict of key/value pairs to be set
:rtype: :class:`QoSSpecs`
"""
body = {
"qos_specs": {
"name": name,
}
}
body["qos_specs"].update(specs)
return self._create("/qos-specs", body, "qos_specs")
def set_keys(self, qos_specs, specs):
"""Update a qos specs with new specifications.
:param qos_specs: The ID of qos specs
:param specs: A dict of key/value pairs to be set
:rtype: :class:`QoSSpecs`
"""
body = {
"qos_specs": {}
}
body["qos_specs"].update(specs)
return self._update("/qos-specs/%s" % qos_specs, body)
def unset_keys(self, qos_specs, specs):
"""Update a qos specs with new specifications.
:param qos_specs: The ID of qos specs
:param specs: A list of key to be unset
:rtype: :class:`QoSSpecs`
"""
body = {'keys': specs}
return self._update("/qos-specs/%s/delete_keys" % qos_specs,
body)
def get_associations(self, qos_specs):
"""Get associated entities of a qos specs.
:param qos_specs: The id of the :class: `QoSSpecs`
:return: a list of entities that associated with specific qos specs.
"""
return self._list("/qos-specs/%s/associations" % base.getid(qos_specs),
"qos_associations")
def associate(self, qos_specs, vol_type_id):
"""Associate a volume type with specific qos specs.
:param qos_specs: The qos specs to be associated with
:param vol_type_id: The volume type id to be associated with
"""
self.api.client.get("/qos-specs/%s/associate?vol_type_id=%s" %
(base.getid(qos_specs), vol_type_id))
def disassociate(self, qos_specs, vol_type_id):
"""Disassociate qos specs from volume type.
:param qos_specs: The qos specs to be associated with
:param vol_type_id: The volume type id to be associated with
"""
self.api.client.get("/qos-specs/%s/disassociate?vol_type_id=%s" %
(base.getid(qos_specs), vol_type_id))
def disassociate_all(self, qos_specs):
"""Disassociate all entities from specific qos specs.
:param qos_specs: The qos specs to be associated with
"""
self.api.client.get("/qos-specs/%s/disassociate_all" %
base.getid(qos_specs))

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack LLC.
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -20,32 +20,24 @@ class QuotaClassSet(base.Resource):
@property
def id(self):
"""Needed by base.Resource to self-refresh and be indexed"""
"""Needed by base.Resource to self-refresh and be indexed."""
return self.class_name
def update(self, *args, **kwargs):
self.manager.update(self.class_name, *args, **kwargs)
class QuotaClassSetManager(base.ManagerWithFind):
class QuotaClassSetManager(base.Manager):
resource_class = QuotaClassSet
def get(self, class_name):
return self._get("/os-quota-class-sets/%s" % (class_name),
"quota_class_set")
def update(self,
class_name,
volumes=None,
gigabytes=None):
def update(self, class_name, **updates):
body = {'quota_class_set': {'class_name': class_name}}
body = {'quota_class_set': {
'class_name': class_name,
'volumes': volumes,
'gigabytes': gigabytes}}
for key in body['quota_class_set'].keys():
if body['quota_class_set'][key] is None:
body['quota_class_set'].pop(key)
for update in updates:
body['quota_class_set'][update] = updates[update]
self._update('/os-quota-class-sets/%s' % (class_name), body)

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack LLC.
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -20,32 +20,27 @@ class QuotaSet(base.Resource):
@property
def id(self):
"""Needed by base.Resource to self-refresh and be indexed"""
"""Needed by base.Resource to self-refresh and be indexed."""
return self.tenant_id
def update(self, *args, **kwargs):
self.manager.update(self.tenant_id, *args, **kwargs)
class QuotaSetManager(base.ManagerWithFind):
class QuotaSetManager(base.Manager):
resource_class = QuotaSet
def get(self, tenant_id):
def get(self, tenant_id, usage=False):
if hasattr(tenant_id, 'tenant_id'):
tenant_id = tenant_id.tenant_id
return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set")
return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage),
"quota_set")
def update(self, tenant_id, volumes=None, snapshots=None, gigabytes=None):
def update(self, tenant_id, **updates):
body = {'quota_set': {'tenant_id': tenant_id}}
body = {'quota_set': {
'tenant_id': tenant_id,
'volumes': volumes,
'snapshots': snapshots,
'gigabytes': gigabytes}}
for key in body['quota_set'].keys():
if body['quota_set'][key] is None:
body['quota_set'].pop(key)
for update in updates:
body['quota_set'][update] = updates[update]
self._update('/os-quota-sets/%s' % (tenant_id), body)

View File

@@ -0,0 +1,58 @@
# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
service interface
"""
from cinderclient import base
class Service(base.Resource):
def __repr__(self):
return "<Service: %s>" % self.service
class ServiceManager(base.ManagerWithFind):
resource_class = Service
def list(self, host=None, binary=None):
"""
Describes service list for host.
:param host: destination host name.
:param binary: service binary.
"""
url = "/os-services"
filters = []
if host:
filters.append("host=%s" % host)
if binary:
filters.append("binary=%s" % binary)
if filters:
url = "%s?%s" % (url, "&".join(filters))
return self._list(url, "services")
def enable(self, host, binary):
"""Enable the service specified by hostname and binary."""
body = {"host": host, "binary": binary}
result = self._update("/os-services/enable", body)
return self.resource_class(self, result)
def disable(self, host, binary):
"""Enable the service specified by hostname and binary."""
body = {"host": host, "binary": binary}
result = self._update("/os-services/disable", body)
return self.resource_class(self, result)

Some files were not shown because too many files have changed in this diff Show More