Merge tag '1.0.6' into debian/unstable

Tag python-cinderclient version 1.0.6
This commit is contained in:
Thomas Goirand
2013-10-07 01:17:18 +08:00
76 changed files with 4662 additions and 342 deletions

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

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

View File

@@ -1,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
@@ -79,6 +79,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
@@ -133,6 +134,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,
@@ -192,7 +195,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))

View File

@@ -53,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):

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

@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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.

View File

@@ -0,0 +1,227 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 logging
import os
from stevedore import extension
from cinderclient.openstack.common.apiclient import exceptions
logger = logging.getLogger(__name__)
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
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 _discovered_plugins.iteritems():
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 requred 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(_discovered_plugins.iterkeys()):
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"])
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
__metaclass__ = abc.ABCMeta
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,492 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 urllib
from cinderclient.openstack.common.apiclient import exceptions
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)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
__metaclass__ = abc.ABCMeta
@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 kwargs.copy().iteritems():
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' % urllib.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' % urllib.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 info.iteritems():
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,360 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 exeptions 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,446 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 sys
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 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"
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
# so we can do this:
# _code_map = dict((c.http_status, c)
# for c in HttpError.__subclasses__())
_code_map = {}
for obj in sys.modules[__name__].__dict__.values():
if isinstance(obj, type):
try:
http_status = obj.http_status
except AttributeError:
pass
else:
if http_status:
_code_map[http_status] = obj
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,172 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 urlparse
import requests
from cinderclient.openstack.common.apiclient import client
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 = {}
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 = 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))
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,365 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 logging
import os
import re
try:
import UserString as _userString
except ImportError:
import collections as _userString
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, '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.
#
# Also included below is an example LocaleHandler that translates
# Messages to an associated locale, effectively allowing many logs,
# each with their own locale.
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)
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(_userString.UserString, object):
"""Class used to encapsulate translatable messages."""
def __init__(self, msg, domain):
# _msg is the gettext msgid and should never change
self._msg = msg
self._left_extra_msg = ''
self._right_extra_msg = ''
self._locale = None
self.params = None
self.domain = domain
@property
def data(self):
# NOTE(mrodden): this should always resolve to a unicode string
# that best represents the state of the message currently
localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
if self.locale:
lang = gettext.translation(self.domain,
localedir=localedir,
languages=[self.locale],
fallback=True)
else:
# use system locale for translations
lang = gettext.translation(self.domain,
localedir=localedir,
fallback=True)
if six.PY3:
ugettext = lang.gettext
else:
ugettext = lang.ugettext
full_msg = (self._left_extra_msg +
ugettext(self._msg) +
self._right_extra_msg)
if self.params is not None:
full_msg = full_msg % self.params
return six.text_type(full_msg)
@property
def locale(self):
return self._locale
@locale.setter
def locale(self, value):
self._locale = value
if not self.params:
return
# This Message object may have been constructed with one or more
# Message objects as substitution parameters, given as a single
# Message, or a tuple or Map containing some, so when setting the
# locale for this Message we need to set it for those Messages too.
if isinstance(self.params, Message):
self.params.locale = value
return
if isinstance(self.params, tuple):
for param in self.params:
if isinstance(param, Message):
param.locale = value
return
if isinstance(self.params, dict):
for param in self.params.values():
if isinstance(param, Message):
param.locale = value
def _save_dictionary_parameter(self, dict_param):
full_msg = self.data
# look for %(blah) fields in string;
# ignore %% and deal with the
# case where % is first character on the line
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
# if we don't find any %(blah) blocks but have a %s
if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
# apparently the full dictionary is the parameter
params = copy.deepcopy(dict_param)
else:
params = {}
for key in keys:
try:
params[key] = copy.deepcopy(dict_param[key])
except TypeError:
# cast uncopyable thing to unicode string
params[key] = six.text_type(dict_param[key])
return params
def _save_parameters(self, other):
# we check for None later to see if
# we actually have parameters to inject,
# so encapsulate if our parameter is actually None
if other is None:
self.params = (other, )
elif isinstance(other, dict):
self.params = self._save_dictionary_parameter(other)
else:
# fallback to casting to unicode,
# this will handle the problematic python code-like
# objects that cannot be deep-copied
try:
self.params = copy.deepcopy(other)
except TypeError:
self.params = six.text_type(other)
return self
# overrides to be more string-like
def __unicode__(self):
return self.data
def __str__(self):
if six.PY3:
return self.__unicode__()
return self.data.encode('utf-8')
def __getstate__(self):
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
'domain', 'params', '_locale']
new_dict = self.__dict__.fromkeys(to_copy)
for attr in to_copy:
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
return new_dict
def __setstate__(self, state):
for (k, v) in state.items():
setattr(self, k, v)
# operator overloads
def __add__(self, other):
copied = copy.deepcopy(self)
copied._right_extra_msg += other.__str__()
return copied
def __radd__(self, other):
copied = copy.deepcopy(self)
copied._left_extra_msg += other.__str__()
return copied
def __mod__(self, other):
# do a format string to catch and raise
# any possible KeyErrors from missing parameters
self.data % other
copied = copy.deepcopy(self)
return copied._save_parameters(other)
def __mul__(self, other):
return self.data * other
def __rmul__(self, other):
return other * self.data
def __getitem__(self, key):
return self.data[key]
def __getslice__(self, start, end):
return self.data.__getslice__(start, end)
def __getattribute__(self, name):
# NOTE(mrodden): handle lossy operations that we can't deal with yet
# These override the UserString implementation, since UserString
# uses our __class__ attribute to try and build a new message
# after running the inner data string through the operation.
# At that point, we have lost the gettext message id and can just
# safely resolve to a string instead.
ops = ['capitalize', 'center', 'decode', 'encode',
'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
if name in ops:
return getattr(self.data, name)
else:
return _userString.UserString.__getattribute__(self, name)
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 all projects udpate
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)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
def get_localized_message(message, user_locale):
"""Gets a localized version of the given message in the given locale."""
if isinstance(message, Message):
if user_locale:
message.locale = user_locale
return six.text_type(message)
else:
return message
class LocaleHandler(logging.Handler):
"""Handler that can have a locale associated to translate Messages.
A quick example of how to utilize the Message class above.
LocaleHandler takes a locale and a target logging.Handler object
to forward LogRecord objects to after translating the internal Message.
"""
def __init__(self, locale, target):
"""Initialize a LocaleHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
logging.Handler.__init__(self)
self.locale = locale
self.target = target
def emit(self, record):
if isinstance(record.msg, Message):
# set the locale and resolve to a string
record.msg.locale = self.locale
self.target.emit(record)

View File

@@ -0,0 +1,68 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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

@@ -1,6 +1,6 @@
# 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 +19,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 _ # noqa
# 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 +60,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):
"""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 is considered False.
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 False
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 isntance 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 +133,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,16 +144,16 @@ 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 isntance 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):
if isinstance(text, six.text_type):
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
@@ -131,3 +161,58 @@ def safe_encode(text, incoming=None,
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,4 +1,4 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# Copyright 2011, Piston Cloud Computing, Inc.
#
# All Rights Reserved.
@@ -52,15 +52,22 @@ class ServiceCatalog(object):
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

@@ -1,5 +1,5 @@
# Copyright 2011 OpenStack LLC.
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -41,7 +41,7 @@ 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'
logger = logging.getLogger(__name__)
@@ -145,7 +145,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)
@@ -173,7 +173,7 @@ 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),
help='Accepts 1 or 2,defaults '

View File

@@ -67,7 +67,7 @@ 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:
@@ -78,8 +78,6 @@ class FakeClient(object):
print(body)
raise
self.client.callstack = []
def clear_callstack(self):
self.client.callstack = []

View File

@@ -72,7 +72,7 @@ SERVICE_CATALOG = {
"endpoints_links": [],
},
{
"name": "Nova Volumes",
"name": "Cinder Volume Service",
"type": "volume",
"endpoints": [
{
@@ -101,6 +101,128 @@ SERVICE_CATALOG = {
},
],
},
{
"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": [
{
@@ -136,5 +258,18 @@ class ServiceCatalogTest(utils.TestCase):
self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'),
"https://volume1.host/v1/3456")
self.assertEquals(sc.url_for('tenantId', '2', service_type='volumev2'),
"https://volume1.host/v2/3456")
self.assertEquals(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.assertEquals(sc.url_for('tenantId', '1', service_type='volume'),
"https://volume1.host/v2/1234")
self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'),
"https://volume1.host/v2/3456")

View File

@@ -1,5 +1,5 @@
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
# Copyright 2011 OpenStack, LLC
# 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.
@@ -116,6 +116,34 @@ 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,
@@ -244,8 +272,10 @@ class FakeHTTPClient(base_client.HTTPClient):
action = body.keys()[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')
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
#
@@ -276,6 +306,10 @@ class FakeHTTPClient(base_client.HTTPClient):
r = {'volume': self.get_volumes_detail()[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
@@ -302,6 +336,9 @@ class FakeHTTPClient(base_client.HTTPClient):
assert 'status' in body[action]
elif action == 'os-extend':
assert body[action].keys() == ['new_size']
elif action == 'os-migrate_volume':
assert 'host' in body[action]
assert 'force_host_copy' in body[action]
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
@@ -383,6 +420,11 @@ class FakeHTTPClient(base_client.HTTPClient):
'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',
@@ -398,6 +440,23 @@ class FakeHTTPClient(base_client.HTTPClient):
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, {}, {})
#
# Set/Unset metadata
#
@@ -474,6 +533,68 @@ class FakeHTTPClient(base_client.HTTPClient):
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
#

View File

@@ -24,7 +24,7 @@ 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": {
@@ -33,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",
},
],
},
@@ -89,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": {
@@ -105,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",
},
],
},
@@ -164,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,
@@ -181,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": {
@@ -190,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/",
},
],
},
@@ -265,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": {
@@ -274,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,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 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,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,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,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
@@ -127,7 +127,7 @@ class ShellTest(utils.TestCase):
'status=available&volume_id=1234')
def test_rename(self):
# basic rename with positional agruments
# 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)
@@ -143,12 +143,12 @@ class ShellTest(utils.TestCase):
'display_description': 'new-description',
}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# noop, the only all will be the lookup
self.run_command('rename 1234')
self.assert_called('GET', '/volumes/1234')
# Call rename with no arguments
self.assertRaises(SystemExit, self.run_command, 'rename')
def test_rename_snapshot(self):
# basic rename with positional agruments
# 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)
@@ -165,9 +165,9 @@ class ShellTest(utils.TestCase):
'display_description': 'new-description',
}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# noop, the only all will be the lookup
self.run_command('snapshot-rename 1234')
self.assert_called('GET', '/snapshots/1234')
# 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')
@@ -203,3 +203,66 @@ class ShellTest(utils.TestCase):
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_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.
"""
self.skipTest("Not implemented")
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)

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

@@ -0,0 +1,95 @@
# 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
"""
self.skipTest("Not implemented")

View File

@@ -20,7 +20,7 @@ from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class VolumeTRansfersTest(utils.TestCase):
class VolumeTransfersTest(utils.TestCase):
def test_create(self):
cs.transfers.create('1234')

View File

@@ -87,3 +87,12 @@ class VolumesTest(utils.TestCase):
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')

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack, LLC
# 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.
@@ -119,6 +119,34 @@ def _stub_backup(id, base_uri, tenant_id):
}
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'}
@@ -251,8 +279,10 @@ class FakeHTTPClient(base_client.HTTPClient):
action = body.keys()[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')
raise AssertionError('Unexpected action: %s' % action)
return (resp, {}, _body)
#
@@ -283,6 +313,10 @@ class FakeHTTPClient(base_client.HTTPClient):
r = {'volume': self.get_volumes_detail()[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
@@ -309,6 +343,9 @@ class FakeHTTPClient(base_client.HTTPClient):
assert 'status' in body[action]
elif action == 'os-extend':
assert body[action].keys() == ['new_size']
elif action == 'os-migrate_volume':
assert 'host' in body[action]
assert 'force_host_copy' in body[action]
else:
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
@@ -390,6 +427,11 @@ class FakeHTTPClient(base_client.HTTPClient):
'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',
@@ -405,6 +447,23 @@ class FakeHTTPClient(base_client.HTTPClient):
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, {}, {})
#
# Set/Unset metadata
#
@@ -481,6 +540,69 @@ class FakeHTTPClient(base_client.HTTPClient):
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
#

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -27,7 +27,7 @@ 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/",
},
],
},
@@ -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,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 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,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,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,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
@@ -105,7 +105,7 @@ class ShellTest(utils.TestCase):
'status=available&volume_id=1234')
def test_rename(self):
# basic rename with positional agruments
# 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)
@@ -121,12 +121,12 @@ class ShellTest(utils.TestCase):
'description': 'new-description',
}}
self.assert_called('PUT', '/volumes/1234', body=expected)
# noop, the only all will be the lookup
self.run_command('rename 1234')
self.assert_called('GET', '/volumes/1234')
# Call rename with no arguments
self.assertRaises(SystemExit, self.run_command, 'rename')
def test_rename_snapshot(self):
# basic rename with positional agruments
# 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)
@@ -143,9 +143,9 @@ class ShellTest(utils.TestCase):
'description': 'new-description',
}}
self.assert_called('PUT', '/snapshots/1234', body=expected)
# noop, the only all will be the lookup
self.run_command('snapshot-rename 1234')
self.assert_called('GET', '/snapshots/1234')
# 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')
@@ -181,3 +181,66 @@ class ShellTest(utils.TestCase):
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_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.
"""
self.skipTest("Not implemented")
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)

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.
#

View File

@@ -0,0 +1,95 @@
# 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
"""
self.skipTest("Not implemented")

View File

@@ -20,7 +20,7 @@ from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
class VolumeTRansfersTest(utils.TestCase):
class VolumeTransfersTest(utils.TestCase):
def test_create(self):
cs.transfers.create('1234')

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#
@@ -90,3 +90,12 @@ class VolumesTest(utils.TestCase):
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')

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
@@ -230,6 +230,11 @@ 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 list(server.networks.items()):

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2012 OpenStack, LLC.
# Copyright (c) 2012 OpenStack Foundation
#
# All Rights Reserved.
#

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
@@ -16,12 +16,14 @@
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
@@ -59,6 +61,9 @@ 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)

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,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

@@ -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

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,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,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
@@ -24,6 +24,7 @@ import sys
import time
from cinderclient import exceptions
from cinderclient.openstack.common import strutils
from cinderclient import utils
from cinderclient.v1 import availability_zones
@@ -60,26 +61,26 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
time.sleep(poll_period)
def _find_volume(cs, volume):
"""Get a volume by ID."""
return utils.find_resource(cs.volumes, volume)
def _find_volume_snapshot(cs, snapshot):
"""Get a volume snapshot by ID."""
"""Get a volume snapshot by name or ID."""
return utils.find_resource(cs.volume_snapshots, snapshot)
def _find_backup(cs, backup):
"""Get a backup by ID."""
"""Get a backup by name or ID."""
return utils.find_resource(cs.backups, backup)
def _find_transfer(cs, transfer):
"""Get a transfer by ID."""
"""Get a transfer by name or ID."""
return utils.find_resource(cs.transfers, transfer)
def _find_qos_specs(cs, qos_specs):
"""Get a qos specs by ID."""
return utils.find_resource(cs.qos_specs, qos_specs)
def _print_volume(volume):
utils.print_dict(volume._info)
@@ -154,6 +155,13 @@ def _extract_metadata(args):
metavar='<status>',
default=None,
help='Filter results by status')
@utils.arg(
'--metadata',
type=str,
nargs='*',
metavar='<key=value>',
help='Filter results by metadata',
default=None)
@utils.service_type('volume')
def do_list(cs, args):
"""List all the volumes."""
@@ -162,6 +170,7 @@ def do_list(cs, args):
'all_tenants': all_tenants,
'display_name': args.display_name,
'status': args.status,
'metadata': _extract_metadata(args) if args.metadata else None,
}
volumes = cs.volumes.list(search_opts=search_opts)
_translate_volume_keys(volumes)
@@ -174,11 +183,11 @@ def do_list(cs, args):
'Size', 'Volume Type', 'Bootable', 'Attached to'])
@utils.arg('volume', metavar='<volume>', help='ID of the volume.')
@utils.arg('volume', metavar='<volume>', help='Name or ID of the volume.')
@utils.service_type('volume')
def do_show(cs, args):
"""Show details about a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
_print_volume(volume)
@@ -268,23 +277,26 @@ def do_create(cs, args):
_print_volume(volume)
@utils.arg('volume', metavar='<volume>', help='ID of the volume to delete.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to delete.')
@utils.service_type('volume')
def do_delete(cs, args):
"""Remove a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
volume.delete()
@utils.arg('volume', metavar='<volume>', help='ID of the volume to delete.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to delete.')
@utils.service_type('volume')
def do_force_delete(cs, args):
"""Attempt forced removal of a volume, regardless of its state."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
volume.force_delete()
@utils.arg('volume', metavar='<volume>', help='ID of the volume to modify.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to modify.')
@utils.arg('--state', metavar='<state>', default='available',
help=('Indicate which state to assign the volume. Options include '
'available, error, creating, deleting, error_deleting. If no '
@@ -292,11 +304,12 @@ def do_force_delete(cs, args):
@utils.service_type('volume')
def do_reset_state(cs, args):
"""Explicitly update the state of a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
volume.reset_state(args.state)
@utils.arg('volume', metavar='<volume>', help='ID of the volume to rename.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to rename.')
@utils.arg('display_name', nargs='?', metavar='<display-name>',
help='New display-name for the volume.')
@utils.arg('--display-description', metavar='<display-description>',
@@ -310,12 +323,17 @@ def do_rename(cs, args):
kwargs['display_name'] = args.display_name
if args.display_description is not None:
kwargs['display_description'] = args.display_description
_find_volume(cs, args.volume).update(**kwargs)
if not any(kwargs):
msg = 'Must supply either display-name or display-description.'
raise exceptions.ClientException(code=1, message=msg)
utils.find_volume(cs, args.volume).update(**kwargs)
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume to update metadata on.')
help='Name or ID of the volume to update metadata on.')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
@@ -328,7 +346,7 @@ def do_rename(cs, args):
@utils.service_type('volume')
def do_metadata(cs, args):
"""Set or Delete metadata on a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
metadata = _extract_metadata(args)
if args.action == 'set':
@@ -384,7 +402,8 @@ def do_snapshot_list(cs, args):
['ID', 'Volume ID', 'Status', 'Display Name', 'Size'])
@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
@utils.arg('snapshot', metavar='<snapshot>',
help='Name or ID of the snapshot.')
@utils.service_type('volume')
def do_snapshot_show(cs, args):
"""Show details about a snapshot."""
@@ -392,9 +411,9 @@ def do_snapshot_show(cs, args):
_print_volume_snapshot(snapshot)
@utils.arg('volume_id',
metavar='<volume-id>',
help='ID of the volume to snapshot')
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of the volume to snapshot')
@utils.arg('--force',
metavar='<True|False>',
help='Optional flag to indicate whether '
@@ -420,24 +439,26 @@ def do_snapshot_show(cs, args):
@utils.service_type('volume')
def do_snapshot_create(cs, args):
"""Add a new snapshot."""
snapshot = cs.volume_snapshots.create(args.volume_id,
volume = utils.find_volume(cs, args.volume)
snapshot = cs.volume_snapshots.create(volume.id,
args.force,
args.display_name,
args.display_description)
_print_volume_snapshot(snapshot)
@utils.arg('snapshot_id',
metavar='<snapshot-id>',
help='ID of the snapshot to delete.')
@utils.arg('snapshot',
metavar='<snapshot>',
help='Name or ID of the snapshot to delete.')
@utils.service_type('volume')
def do_snapshot_delete(cs, args):
"""Remove a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot_id)
snapshot = _find_volume_snapshot(cs, args.snapshot)
snapshot.delete()
@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
@utils.arg('snapshot', metavar='<snapshot>',
help='Name or ID of the snapshot.')
@utils.arg('display_name', nargs='?', metavar='<display-name>',
help='New display-name for the snapshot.')
@utils.arg('--display-description', metavar='<display-description>',
@@ -451,11 +472,16 @@ def do_snapshot_rename(cs, args):
kwargs['display_name'] = args.display_name
if args.display_description is not None:
kwargs['display_description'] = args.display_description
if not any(kwargs):
msg = 'Must supply either display-name or display-description.'
raise exceptions.ClientException(code=1, message=msg)
_find_volume_snapshot(cs, args.snapshot).update(**kwargs)
@utils.arg('snapshot', metavar='<snapshot>',
help='ID of the snapshot to modify.')
help='Name or ID of the snapshot to modify.')
@utils.arg('--state', metavar='<state>',
default='available',
help=('Indicate which state to assign the snapshot. '
@@ -525,7 +551,7 @@ def do_type_delete(cs, args):
help='Extra_specs to set/unset (only key is necessary on unset)')
@utils.service_type('volume')
def do_type_key(cs, args):
"Set or unset extra_spec for a volume type."""
"""Set or unset extra_spec for a volume type."""
vtype = _find_volume_type(cs, args.vtype)
if args.metadata is not None:
@@ -685,9 +711,9 @@ def _find_volume_type(cs, vtype):
return utils.find_resource(cs.volume_types, vtype)
@utils.arg('volume_id',
metavar='<volume-id>',
help='ID of the volume to upload to an image')
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of the volume to upload to an image')
@utils.arg('--force',
metavar='<True|False>',
help='Optional flag to indicate whether '
@@ -710,7 +736,7 @@ def _find_volume_type(cs, vtype):
@utils.service_type('volume')
def do_upload_to_image(cs, args):
"""Upload volume to image service as image."""
volume = _find_volume(cs, args.volume_id)
volume = utils.find_volume(cs, args.volume)
_print_volume_image(volume.upload_to_image(args.force,
args.image_name,
args.container_format,
@@ -718,7 +744,7 @@ def do_upload_to_image(cs, args):
@utils.arg('volume', metavar='<volume>',
help='ID of the volume to backup.')
help='Name or ID of the volume to backup.')
@utils.arg('--container', metavar='<container>',
help='Optional Backup container name. (Default=None)',
default=None)
@@ -731,13 +757,22 @@ def do_upload_to_image(cs, args):
@utils.service_type('volume')
def do_backup_create(cs, args):
"""Creates a backup."""
cs.backups.create(args.volume,
args.container,
args.display_name,
args.display_description)
volume = utils.find_volume(cs, args.volume)
backup = cs.backups.create(volume.id,
args.container,
args.display_name,
args.display_description)
info = {"volume_id": volume.id}
info.update(backup._info)
if 'links' in info:
info.pop('links')
utils.print_dict(info)
@utils.arg('backup', metavar='<backup>', help='ID of the backup.')
@utils.arg('backup', metavar='<backup>', help='Name or ID of the backup.')
@utils.service_type('volume')
def do_backup_show(cs, args):
"""Show details about a backup."""
@@ -761,7 +796,7 @@ def do_backup_list(cs, args):
@utils.arg('backup', metavar='<backup>',
help='ID of the backup to delete.')
help='Name or ID of the backup to delete.')
@utils.service_type('volume')
def do_backup_delete(cs, args):
"""Remove a backup."""
@@ -771,25 +806,29 @@ def do_backup_delete(cs, args):
@utils.arg('backup', metavar='<backup>',
help='ID of the backup to restore.')
@utils.arg('--volume-id', metavar='<volume-id>',
help='Optional ID of the volume to restore to.',
@utils.arg('--volume-id', metavar='<volume>',
help='Optional ID(or name) of the volume to restore to.',
default=None)
@utils.service_type('volume')
def do_backup_restore(cs, args):
"""Restore a backup."""
cs.restores.restore(args.backup,
args.volume_id)
if args.volume:
volume_id = utils.find_volume(cs, args.volume).id
else:
volume_id = None
cs.restores.restore(args.backup, volume_id)
@utils.arg('volume', metavar='<volume>',
help='ID of the volume to transfer.')
help='Name or ID of the volume to transfer.')
@utils.arg('--display-name', metavar='<display-name>',
help='Optional transfer name. (Default=None)',
default=None)
@utils.service_type('volume')
def do_transfer_create(cs, args):
"""Creates a volume transfer."""
transfer = cs.transfers.create(args.volume,
volume = utils.find_volume(cs, args.volume)
transfer = cs.transfers.create(volume.id,
args.display_name)
info = dict()
info.update(transfer._info)
@@ -801,7 +840,7 @@ def do_transfer_create(cs, args):
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to delete.')
help='Name or ID of the transfer to delete.')
@utils.service_type('volume')
def do_transfer_delete(cs, args):
"""Undo a transfer."""
@@ -835,7 +874,7 @@ def do_transfer_list(cs, args):
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to accept.')
help='Name or ID of the transfer to accept.')
@utils.service_type('volume')
def do_transfer_show(cs, args):
"""Show details about a transfer."""
@@ -849,7 +888,8 @@ def do_transfer_show(cs, args):
utils.print_dict(info)
@utils.arg('volume', metavar='<volume>', help='ID of the volume to extend.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to extend.')
@utils.arg('new_size',
metavar='<new-size>',
type=int,
@@ -857,7 +897,7 @@ def do_transfer_show(cs, args):
@utils.service_type('volume')
def do_extend(cs, args):
"""Attempt to extend the size of an existing volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
cs.volumes.extend(volume, args.new_size)
@@ -947,3 +987,224 @@ def do_availability_zone_list(cs, _args):
result += _treeizeAvailabilityZone(zone)
_translate_availability_zone_keys(result)
utils.print_list(result, ['Name', 'Status'])
def _print_volume_encryption_type_list(encryption_types):
"""
Display a tabularized list of volume encryption types.
:param encryption_types: a list of :class: VolumeEncryptionType instances
"""
utils.print_list(encryption_types, ['Volume Type ID', 'Provider',
'Cipher', 'Key Size',
'Control Location'])
@utils.service_type('volume')
def do_encryption_type_list(cs, args):
"""List encryption type information for all volume types (Admin Only)."""
result = cs.volume_encryption_types.list()
utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher',
'Key Size', 'Control Location'])
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of the volume type")
@utils.service_type('volume')
def do_encryption_type_show(cs, args):
"""Show the encryption type information for a volume type (Admin Only)."""
volume_type = _find_volume_type(cs, args.volume_type)
result = cs.volume_encryption_types.get(volume_type)
# Display result or an empty table if no result
if hasattr(result, 'volume_type_id'):
_print_volume_encryption_type_list([result])
else:
_print_volume_encryption_type_list([])
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of the volume type")
@utils.arg('provider',
metavar='<provider>',
type=str,
help="Class providing encryption support (e.g. LuksEncryptor)")
@utils.arg('--cipher',
metavar='<cipher>',
type=str,
required=False,
default=None,
help="Encryption algorithm/mode to use (e.g., aes-xts-plain64) "
"(Optional, Default=None)")
@utils.arg('--key_size',
metavar='<key_size>',
type=int,
required=False,
default=None,
help="Size of the encryption key, in bits (e.g., 128, 256) "
"(Optional, Default=None)")
@utils.arg('--control_location',
metavar='<control_location>',
choices=['front-end', 'back-end'],
type=str,
required=False,
default=None,
help="Notional service where encryption is performed (e.g., "
"front-end=Nova). Values: 'front-end', 'back-end' "
"(Optional, Default=None)")
@utils.service_type('volume')
def do_encryption_type_create(cs, args):
"""Create a new encryption type for a volume type (Admin Only)."""
volume_type = _find_volume_type(cs, args.volume_type)
body = {}
body['provider'] = args.provider
body['cipher'] = args.cipher
body['key_size'] = args.key_size
body['control_location'] = args.control_location
result = cs.volume_encryption_types.create(volume_type, body)
_print_volume_encryption_type_list([result])
@utils.arg('volume', metavar='<volume>', help='ID of the volume to migrate')
@utils.arg('host', metavar='<host>', help='Destination host')
@utils.arg('--force-host-copy', metavar='<True|False>',
choices=['True', 'False'], required=False,
help='Optional flag to force the use of the generic '
'host-based migration mechanism, bypassing driver '
'optimizations (Default=False).',
default=False)
@utils.service_type('volume')
def do_migrate(cs, args):
"""Migrate the volume to the new host."""
volume = utils.find_volume(cs, args.volume)
volume.migrate_volume(args.host, args.force_host_copy)
def _print_qos_specs(qos_specs):
utils.print_dict(qos_specs._info)
def _print_qos_specs_list(q_specs):
utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs'])
def _print_qos_specs_and_associations_list(q_specs):
utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs'])
def _print_associations_list(associations):
utils.print_list(associations, ['Association_Type', 'Name', 'ID'])
@utils.arg('name',
metavar='<name>',
help="Name of the new QoS specs")
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='Specifications for QoS')
@utils.service_type('volume')
def do_qos_create(cs, args):
"""Create a new qos specs."""
keypair = None
if args.metadata is not None:
keypair = _extract_metadata(args)
qos_specs = cs.qos_specs.create(args.name, keypair)
_print_qos_specs(qos_specs)
@utils.service_type('volume')
def do_qos_list(cs, args):
"""Get full list of qos specs."""
qos_specs = cs.qos_specs.list()
_print_qos_specs_list(qos_specs)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of the qos_specs to show.')
@utils.service_type('volume')
def do_qos_show(cs, args):
"""Get a specific qos specs."""
qos_specs = _find_qos_specs(cs, args.qos_specs)
_print_qos_specs(qos_specs)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of the qos_specs to delete.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
help='Optional flag that indicates whether to delete '
'specified qos specs even if it is in-use.')
@utils.service_type('volume')
def do_qos_delete(cs, args):
"""Delete a specific qos specs."""
force = strutils.bool_from_string(args.force)
qos_specs = _find_qos_specs(cs, args.qos_specs)
cs.qos_specs.delete(qos_specs, force)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos_specs.')
@utils.arg('vol_type_id', metavar='<volume_type_id>',
help='ID of volume type to be associated with.')
@utils.service_type('volume')
def do_qos_associate(cs, args):
"""Associate qos specs with specific volume type."""
cs.qos_specs.associate(args.qos_specs, args.vol_type_id)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos_specs.')
@utils.arg('vol_type_id', metavar='<volume_type_id>',
help='ID of volume type to be associated with.')
@utils.service_type('volume')
def do_qos_disassociate(cs, args):
"""Disassociate qos specs from specific volume type."""
cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos_specs to be operate on.')
@utils.service_type('volume')
def do_qos_disassociate_all(cs, args):
"""Disassociate qos specs from all of its associations."""
cs.qos_specs.disassociate_all(args.qos_specs)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos specs')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="Actions: 'set' or 'unset'")
@utils.arg('metadata', metavar='key=value',
nargs='+',
default=[],
help='QoS specs to set/unset (only key is necessary on unset)')
def do_qos_key(cs, args):
"""Set or unset specifications for a qos spec."""
keypair = _extract_metadata(args)
if args.action == 'set':
cs.qos_specs.set_keys(args.qos_specs, keypair)
elif args.action == 'unset':
cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys()))
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of the qos_specs.')
@utils.service_type('volume')
def do_qos_get_association(cs, args):
"""Get all associations of specific qos specs."""
associations = cs.qos_specs.get_associations(args.qos_specs)
_print_associations_list(associations)

View File

@@ -0,0 +1,96 @@
# 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):
"""
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
"""
raise NotImplementedError()

View File

@@ -148,3 +148,7 @@ class SnapshotManager(base.ManagerWithFind):
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)

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

View File

@@ -114,6 +114,15 @@ class Volume(base.Resource):
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)
class VolumeManager(base.ManagerWithFind):
"""
@@ -134,13 +143,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:
@@ -352,3 +361,37 @@ class VolumeManager(base.ManagerWithFind):
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]

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#

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
@@ -16,12 +16,14 @@
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
@@ -44,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):
@@ -57,6 +59,9 @@ 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)

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2013 OpenStack, LLC.
# Copyright (c) 2013 OpenStack Foundation
#
# All Rights Reserved.
#

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

@@ -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

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,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,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
@@ -25,6 +25,7 @@ import six
from cinderclient import exceptions
from cinderclient import utils
from cinderclient.openstack.common import strutils
from cinderclient.v2 import availability_zones
@@ -58,26 +59,26 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
time.sleep(poll_period)
def _find_volume(cs, volume):
"""Get a volume by ID."""
return utils.find_resource(cs.volumes, volume)
def _find_volume_snapshot(cs, snapshot):
"""Get a volume snapshot by ID."""
"""Get a volume snapshot by name or ID."""
return utils.find_resource(cs.volume_snapshots, snapshot)
def _find_backup(cs, backup):
"""Get a backup by ID."""
"""Get a backup by name or ID."""
return utils.find_resource(cs.backups, backup)
def _find_transfer(cs, transfer):
"""Get a transfer by ID."""
"""Get a transfer by name or ID."""
return utils.find_resource(cs.transfers, transfer)
def _find_qos_specs(cs, qos_specs):
"""Get a qos specs by ID."""
return utils.find_resource(cs.qos_specs, qos_specs)
def _print_volume_snapshot(snapshot):
utils.print_dict(snapshot._info)
@@ -111,7 +112,7 @@ def _translate_availability_zone_keys(collection):
def _extract_metadata(args):
metadata = {}
for metadatum in args.metadata[0]:
for metadatum in args.metadata:
# unset doesn't require a val, so we have the if/else
if '=' in metadatum:
(key, value) = metadatum.split('=', 1)
@@ -146,7 +147,13 @@ def _extract_metadata(args):
metavar='<status>',
default=None,
help='Filter results by status')
@utils.service_type('volume')
@utils.arg('--metadata',
type=str,
nargs='*',
metavar='<key=value>',
help='Filter results by metadata',
default=None)
@utils.service_type('volumev2')
def do_list(cs, args):
"""List all the volumes."""
# NOTE(thingee): Backwards-compatibility with v1 args
@@ -158,6 +165,7 @@ def do_list(cs, args):
'all_tenants': all_tenants,
'name': args.name,
'status': args.status,
'metadata': _extract_metadata(args) if args.metadata else None,
}
volumes = cs.volumes.list(search_opts=search_opts)
_translate_volume_keys(volumes)
@@ -173,12 +181,12 @@ def do_list(cs, args):
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume.')
@utils.service_type('volume')
help='Name or ID of the volume.')
@utils.service_type('volumev2')
def do_show(cs, args):
"""Show details about a volume."""
info = dict()
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
info.update(volume._info)
info.pop('links', None)
@@ -247,7 +255,7 @@ def do_show(cs, args):
action='append',
default=[],
help='Scheduler hint like in nova')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_create(cs, args):
"""Add a new volume."""
# NOTE(thingee): Backwards-compatibility with v1 args
@@ -297,39 +305,40 @@ def do_create(cs, args):
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume to delete.')
@utils.service_type('volume')
help='Name or ID of the volume to delete.')
@utils.service_type('volumev2')
def do_delete(cs, args):
"""Remove a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
volume.delete()
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume to delete.')
@utils.service_type('volume')
help='Name or ID of the volume to delete.')
@utils.service_type('volumev2')
def do_force_delete(cs, args):
"""Attempt forced removal of a volume, regardless of its state."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
volume.force_delete()
@utils.arg('volume', metavar='<volume>', help='ID of the volume to modify.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to modify.')
@utils.arg('--state', metavar='<state>', default='available',
help=('Indicate which state to assign the volume. Options include '
'available, error, creating, deleting, error_deleting. If no '
'state is provided, available will be used.'))
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_reset_state(cs, args):
"""Explicitly update the state of a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
volume.reset_state(args.state)
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume to rename.')
help='Name or ID of the volume to rename.')
@utils.arg('name',
nargs='?',
metavar='<name>',
@@ -341,7 +350,7 @@ def do_reset_state(cs, args):
help=argparse.SUPPRESS)
@utils.arg('--display_description',
help=argparse.SUPPRESS)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_rename(cs, args):
"""Rename a volume."""
kwargs = {}
@@ -353,12 +362,16 @@ def do_rename(cs, args):
elif args.description is not None:
kwargs['description'] = args.description
_find_volume(cs, args.volume).update(**kwargs)
if not any(kwargs):
msg = 'Must supply either name or description.'
raise exceptions.ClientException(code=1, message=msg)
utils.find_volume(cs, args.volume).update(**kwargs)
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume to update metadata on.')
help='Name or ID of the volume to update metadata on.')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
@@ -366,13 +379,12 @@ def do_rename(cs, args):
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
action='append',
default=[],
help='Metadata to set/unset (only key is necessary on unset)')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_metadata(cs, args):
"""Set or Delete metadata on a volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
metadata = _extract_metadata(args)
if args.action == 'set':
@@ -412,7 +424,7 @@ def do_metadata(cs, args):
help='Filter results by volume-id')
@utils.arg('--volume_id',
help=argparse.SUPPRESS)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_snapshot_list(cs, args):
"""List all the snapshots."""
all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
@@ -435,17 +447,17 @@ def do_snapshot_list(cs, args):
@utils.arg('snapshot',
metavar='<snapshot>',
help='ID of the snapshot.')
@utils.service_type('volume')
help='Name or ID of the snapshot.')
@utils.service_type('volumev2')
def do_snapshot_show(cs, args):
"""Show details about a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot)
_print_volume_snapshot(snapshot)
@utils.arg('volume-id',
metavar='<volume-id>',
help='ID of the volume to snapshot')
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of the volume to snapshot')
@utils.arg('--force',
metavar='<True|False>',
help='Optional flag to indicate whether '
@@ -468,7 +480,7 @@ def do_snapshot_show(cs, args):
help=argparse.SUPPRESS)
@utils.arg('--display_description',
help=argparse.SUPPRESS)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_snapshot_create(cs, args):
"""Add a new snapshot."""
if args.display_name is not None:
@@ -477,24 +489,26 @@ def do_snapshot_create(cs, args):
if args.display_description is not None:
args.description = args.display_description
snapshot = cs.volume_snapshots.create(args.volume_id,
volume = utils.find_volume(cs, args.volume)
snapshot = cs.volume_snapshots.create(volume.id,
args.force,
args.name,
args.description)
_print_volume_snapshot(snapshot)
@utils.arg('snapshot-id',
metavar='<snapshot-id>',
help='ID of the snapshot to delete.')
@utils.service_type('volume')
@utils.arg('snapshot',
metavar='<snapshot>',
help='Name or ID of the snapshot to delete.')
@utils.service_type('volumev2')
def do_snapshot_delete(cs, args):
"""Remove a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot_id)
snapshot = _find_volume_snapshot(cs, args.snapshot)
snapshot.delete()
@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
@utils.arg('snapshot', metavar='<snapshot>',
help='Name or ID of the snapshot.')
@utils.arg('name', nargs='?', metavar='<name>',
help='New name for the snapshot.')
@utils.arg('--description', metavar='<description>',
@@ -504,7 +518,7 @@ def do_snapshot_delete(cs, args):
help=argparse.SUPPRESS)
@utils.arg('--display_description',
help=argparse.SUPPRESS)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_snapshot_rename(cs, args):
"""Rename a snapshot."""
kwargs = {}
@@ -517,18 +531,22 @@ def do_snapshot_rename(cs, args):
elif args.display_description is not None:
kwargs['description'] = args.display_description
if not any(kwargs):
msg = 'Must supply either name or description.'
raise exceptions.ClientException(code=1, message=msg)
_find_volume_snapshot(cs, args.snapshot).update(**kwargs)
@utils.arg('snapshot', metavar='<snapshot>',
help='ID of the snapshot to modify.')
help='Name or ID of the snapshot to modify.')
@utils.arg('--state', metavar='<state>',
default='available',
help=('Indicate which state to assign the snapshot. '
'Options include available, error, creating, '
'deleting, error_deleting. If no state is provided, '
'available will be used.'))
@utils.service_type('snapshot')
@utils.service_type('volumev2')
def do_snapshot_reset_state(cs, args):
"""Explicitly update the state of a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot)
@@ -544,14 +562,14 @@ def _print_type_and_extra_specs_list(vtypes):
utils.print_list(vtypes, ['ID', 'Name', 'extra_specs'], formatters)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_type_list(cs, args):
"""Print a list of available 'volume types'."""
vtypes = cs.volume_types.list()
_print_volume_type_list(vtypes)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_extra_specs_list(cs, args):
"""Print a list of current 'volume types and extra specs' (Admin Only)."""
vtypes = cs.volume_types.list()
@@ -561,7 +579,7 @@ def do_extra_specs_list(cs, args):
@utils.arg('name',
metavar='<name>',
help="Name of the new volume type")
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_type_create(cs, args):
"""Create a new volume type."""
vtype = cs.volume_types.create(args.name)
@@ -571,7 +589,7 @@ def do_type_create(cs, args):
@utils.arg('id',
metavar='<id>',
help="Unique ID of the volume type to delete")
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_type_delete(cs, args):
"""Delete a specific volume type."""
cs.volume_types.delete(args.id)
@@ -587,12 +605,11 @@ def do_type_delete(cs, args):
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
action='append',
default=[],
help='Extra_specs to set/unset (only key is necessary on unset)')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_type_key(cs, args):
"Set or unset extra_spec for a volume type."""
"""Set or unset extra_spec for a volume type."""
vtype = _find_volume_type(cs, args.vtype)
keypair = _extract_metadata(args)
@@ -648,7 +665,7 @@ def _quota_update(manager, identifier, args):
@utils.arg('tenant',
metavar='<tenant_id>',
help='UUID of tenant to list the quotas for.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_quota_show(cs, args):
"""List the quotas for a tenant."""
@@ -658,7 +675,7 @@ def do_quota_show(cs, args):
@utils.arg('tenant',
metavar='<tenant_id>',
help='UUID of tenant to list the default quotas for.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_quota_defaults(cs, args):
"""List the default quotas for a tenant."""
@@ -684,7 +701,7 @@ def do_quota_defaults(cs, args):
metavar='<volume_type_name>',
default=None,
help='Volume type (Optional, Default=None)')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_quota_update(cs, args):
"""Update the quotas for a tenant."""
@@ -694,7 +711,7 @@ def do_quota_update(cs, args):
@utils.arg('class_name',
metavar='<class>',
help='Name of quota class to list the quotas for.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_quota_class_show(cs, args):
"""List the quotas for a quota class."""
@@ -720,14 +737,14 @@ def do_quota_class_show(cs, args):
metavar='<volume_type_name>',
default=None,
help='Volume type (Optional, Default=None)')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_quota_class_update(cs, args):
"""Update the quotas for a quota class."""
_quota_update(cs.quota_classes, args.class_name, args)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_absolute_limits(cs, args):
"""Print a list of absolute limits for a user"""
limits = cs.limits.get().absolute
@@ -735,7 +752,7 @@ def do_absolute_limits(cs, args):
utils.print_list(limits, columns)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_rate_limits(cs, args):
"""Print a list of rate limits for a user"""
limits = cs.limits.get().rate
@@ -755,9 +772,9 @@ def _find_volume_type(cs, vtype):
return utils.find_resource(cs.volume_types, vtype)
@utils.arg('volume-id',
metavar='<volume-id>',
help='ID of the volume to snapshot')
@utils.arg('volume',
metavar='<volume>',
help='Name or ID of the volume to snapshot')
@utils.arg('--force',
metavar='<True|False>',
help='Optional flag to indicate whether '
@@ -783,18 +800,33 @@ def _find_volume_type(cs, vtype):
help='Name for created image')
@utils.arg('--image_name',
help=argparse.SUPPRESS)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_upload_to_image(cs, args):
"""Upload volume to image service as image."""
volume = _find_volume(cs, args.volume_id)
volume = utils.find_volume(cs, args.volume)
_print_volume_image(volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format))
@utils.arg('volume', metavar='<volume>', help='ID of the volume to migrate')
@utils.arg('host', metavar='<host>', help='Destination host')
@utils.arg('--force-host-copy', metavar='<True|False>',
choices=['True', 'False'], required=False,
help='Optional flag to force the use of the generic '
'host-based migration mechanism, bypassing driver '
'optimizations (Default=False).',
default=False)
@utils.service_type('volumev2')
def do_migrate(cs, args):
"""Migrate the volume to the new host."""
volume = utils.find_volume(cs, args.volume)
volume.migrate_volume(args.host, args.force_host_copy)
@utils.arg('volume', metavar='<volume>',
help='ID of the volume to backup.')
help='Name or ID of the volume to backup.')
@utils.arg('--container', metavar='<container>',
help='Optional backup container name. (Default=None)',
default=None)
@@ -809,7 +841,7 @@ def do_upload_to_image(cs, args):
metavar='<description>',
default=None,
help='Options backup description (Default=None)')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_backup_create(cs, args):
"""Creates a backup."""
if args.display_name is not None:
@@ -818,14 +850,23 @@ def do_backup_create(cs, args):
if args.display_description is not None:
args.description = args.display_description
cs.backups.create(args.volume,
args.container,
args.name,
args.description)
volume = utils.find_volume(cs, args.volume)
backup = cs.backups.create(volume.id,
args.container,
args.name,
args.description)
info = {"volume_id": volume.id}
info.update(backup._info)
if 'links' in info:
info.pop('links')
utils.print_dict(info)
@utils.arg('backup', metavar='<backup>', help='ID of the backup.')
@utils.service_type('volume')
@utils.arg('backup', metavar='<backup>', help='Name or ID of the backup.')
@utils.service_type('volumev2')
def do_backup_show(cs, args):
"""Show details about a backup."""
backup = _find_backup(cs, args.backup)
@@ -836,7 +877,7 @@ def do_backup_show(cs, args):
utils.print_dict(info)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_backup_list(cs, args):
"""List all the backups."""
backups = cs.backups.list()
@@ -846,8 +887,8 @@ def do_backup_list(cs, args):
@utils.arg('backup', metavar='<backup>',
help='ID of the backup to delete.')
@utils.service_type('volume')
help='Name or ID of the backup to delete.')
@utils.service_type('volumev2')
def do_backup_delete(cs, args):
"""Remove a backup."""
backup = _find_backup(cs, args.backup)
@@ -856,31 +897,35 @@ def do_backup_delete(cs, args):
@utils.arg('backup', metavar='<backup>',
help='ID of the backup to restore.')
@utils.arg('--volume-id', metavar='<volume-id>',
help='Optional ID of the volume to restore to.',
@utils.arg('--volume-id', metavar='<volume>',
help='Optional ID(or name) of the volume to restore to.',
default=None)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_backup_restore(cs, args):
"""Restore a backup."""
cs.restores.restore(args.backup,
args.volume_id)
if args.volume:
volume_id = utils.find_volume(cs, args.volume).id
else:
volume_id = None
cs.restores.restore(args.backup, volume_id)
@utils.arg('volume', metavar='<volume>',
help='ID of the volume to transfer.')
help='Name or ID of the volume to transfer.')
@utils.arg('--name',
metavar='<name>',
default=None,
help='Optional transfer name. (Default=None)')
@utils.arg('--display-name',
help=argparse.SUPPRESS)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_transfer_create(cs, args):
"""Creates a volume transfer."""
if args.display_name is not None:
args.name = args.display_name
transfer = cs.transfers.create(args.volume,
volume = utils.find_volume(cs, args.volume)
transfer = cs.transfers.create(volume.id,
args.name)
info = dict()
info.update(transfer._info)
@@ -890,8 +935,8 @@ def do_transfer_create(cs, args):
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to delete.')
@utils.service_type('volume')
help='Name or ID of the transfer to delete.')
@utils.service_type('volumev2')
def do_transfer_delete(cs, args):
"""Undo a transfer."""
transfer = _find_transfer(cs, args.transfer)
@@ -902,7 +947,7 @@ def do_transfer_delete(cs, args):
help='ID of the transfer to accept.')
@utils.arg('auth_key', metavar='<auth_key>',
help='Auth key of the transfer to accept.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_transfer_accept(cs, args):
"""Accepts a volume transfer."""
transfer = cs.transfers.accept(args.transfer, args.auth_key)
@@ -913,7 +958,7 @@ def do_transfer_accept(cs, args):
utils.print_dict(info)
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_transfer_list(cs, args):
"""List all the transfers."""
transfers = cs.transfers.list()
@@ -922,8 +967,8 @@ def do_transfer_list(cs, args):
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to accept.')
@utils.service_type('volume')
help='Name or ID of the transfer to accept.')
@utils.service_type('volumev2')
def do_transfer_show(cs, args):
"""Show details about a transfer."""
transfer = _find_transfer(cs, args.transfer)
@@ -934,15 +979,16 @@ def do_transfer_show(cs, args):
utils.print_dict(info)
@utils.arg('volume', metavar='<volume>', help='ID of the volume to extend.')
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to extend.')
@utils.arg('new-size',
metavar='<new_size>',
type=int,
help='New size of volume in GB')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_extend(cs, args):
"""Attempt to extend the size of an existing volume."""
volume = _find_volume(cs, args.volume)
volume = utils.find_volume(cs, args.volume)
cs.volumes.extend(volume, args.new_size)
@@ -950,7 +996,7 @@ def do_extend(cs, args):
help='Name of host.')
@utils.arg('--binary', metavar='<binary>', default=None,
help='Service binary.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_service_list(cs, args):
"""List all the services. Filter by host & service binary."""
result = cs.services.list(host=args.host, binary=args.binary)
@@ -960,7 +1006,7 @@ def do_service_list(cs, args):
@utils.arg('host', metavar='<hostname>', help='Name of host.')
@utils.arg('binary', metavar='<binary>', help='Service binary.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_service_enable(cs, args):
"""Enable the service."""
cs.services.enable(args.host, args.binary)
@@ -968,7 +1014,7 @@ def do_service_enable(cs, args):
@utils.arg('host', metavar='<hostname>', help='Name of host.')
@utils.arg('binary', metavar='<binary>', help='Service binary.')
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_service_disable(cs, args):
"""Disable the service."""
cs.services.disable(args.host, args.binary)
@@ -1016,7 +1062,7 @@ def _treeizeAvailabilityZone(zone):
return result
@utils.service_type('volume')
@utils.service_type('volumev2')
def do_availability_zone_list(cs, _args):
"""List all the availability zones."""
try:
@@ -1032,3 +1078,208 @@ def do_availability_zone_list(cs, _args):
result += _treeizeAvailabilityZone(zone)
_translate_availability_zone_keys(result)
utils.print_list(result, ['Name', 'Status'])
def _print_volume_encryption_type_list(encryption_types):
"""
Display a tabularized list of volume encryption types.
:param encryption_types: a list of :class: VolumeEncryptionType instances
"""
utils.print_list(encryption_types, ['Volume Type ID', 'Provider',
'Cipher', 'Key Size',
'Control Location'])
@utils.service_type('volumev2')
def do_encryption_type_list(cs, args):
"""List encryption type information for all volume types (Admin Only)."""
result = cs.volume_encryption_types.list()
utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher',
'Key Size', 'Control Location'])
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of the volume type")
@utils.service_type('volumev2')
def do_encryption_type_show(cs, args):
"""Show the encryption type information for a volume type (Admin Only)."""
volume_type = _find_volume_type(cs, args.volume_type)
result = cs.volume_encryption_types.get(volume_type)
# Display result or an empty table if no result
if hasattr(result, 'volume_type_id'):
_print_volume_encryption_type_list([result])
else:
_print_volume_encryption_type_list([])
@utils.arg('volume_type',
metavar='<volume_type>',
type=str,
help="Name or ID of the volume type")
@utils.arg('provider',
metavar='<provider>',
type=str,
help="Class providing encryption support (e.g. LuksEncryptor)")
@utils.arg('--cipher',
metavar='<cipher>',
type=str,
required=False,
default=None,
help="Encryption algorithm/mode to use (e.g., aes-xts-plain64) "
"(Optional, Default=None)")
@utils.arg('--key_size',
metavar='<key_size>',
type=int,
required=False,
default=None,
help="Size of the encryption key, in bits (e.g., 128, 256) "
"(Optional, Default=None)")
@utils.arg('--control_location',
metavar='<control_location>',
choices=['front-end', 'back-end'],
type=str,
required=False,
default=None,
help="Notional service where encryption is performed (e.g., "
"front-end=Nova). Values: 'front-end', 'back-end' "
"(Optional, Default=None)")
@utils.service_type('volumev2')
def do_encryption_type_create(cs, args):
"""Create a new encryption type for a volume type (Admin Only)."""
volume_type = _find_volume_type(cs, args.volume_type)
body = {}
body['provider'] = args.provider
body['cipher'] = args.cipher
body['key_size'] = args.key_size
body['control_location'] = args.control_location
result = cs.volume_encryption_types.create(volume_type, body)
_print_volume_encryption_type_list([result])
def _print_qos_specs(qos_specs):
utils.print_dict(qos_specs._info)
def _print_qos_specs_list(q_specs):
utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs'])
def _print_qos_specs_and_associations_list(q_specs):
utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs'])
def _print_associations_list(associations):
utils.print_list(associations, ['Association_Type', 'Name', 'ID'])
@utils.arg('name',
metavar='<name>',
help="Name of the new QoS specs")
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
default=[],
help='Specifications for QoS')
@utils.service_type('volumev2')
def do_qos_create(cs, args):
"""Create a new qos specs."""
keypair = None
if args.metadata is not None:
keypair = _extract_metadata(args)
qos_specs = cs.qos_specs.create(args.name, keypair)
_print_qos_specs(qos_specs)
@utils.service_type('volumev2')
def do_qos_list(cs, args):
"""Get full list of qos specs."""
qos_specs = cs.qos_specs.list()
_print_qos_specs_list(qos_specs)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of the qos_specs to show.')
@utils.service_type('volumev2')
def do_qos_show(cs, args):
"""Get a specific qos specs."""
qos_specs = _find_qos_specs(cs, args.qos_specs)
_print_qos_specs(qos_specs)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of the qos_specs to delete.')
@utils.arg('--force',
metavar='<True|False>',
default=False,
help='Optional flag that indicates whether to delete '
'specified qos specs even if it is in-use.')
@utils.service_type('volumev2')
def do_qos_delete(cs, args):
"""Delete a specific qos specs."""
force = strutils.bool_from_string(args.force)
qos_specs = _find_qos_specs(cs, args.qos_specs)
cs.qos_specs.delete(qos_specs, force)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos_specs.')
@utils.arg('vol_type_id', metavar='<volume_type_id>',
help='ID of volume type to be associated with.')
@utils.service_type('volumev2')
def do_qos_associate(cs, args):
"""Associate qos specs with specific volume type."""
cs.qos_specs.associate(args.qos_specs, args.vol_type_id)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos_specs.')
@utils.arg('vol_type_id', metavar='<volume_type_id>',
help='ID of volume type to be associated with.')
@utils.service_type('volumev2')
def do_qos_disassociate(cs, args):
"""Disassociate qos specs from specific volume type."""
cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos_specs to be operate on.')
@utils.service_type('volumev2')
def do_qos_disassociate_all(cs, args):
"""Disassociate qos specs from all of its associations."""
cs.qos_specs.disassociate_all(args.qos_specs)
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of qos specs')
@utils.arg('action',
metavar='<action>',
choices=['set', 'unset'],
help="Actions: 'set' or 'unset'")
@utils.arg('metadata', metavar='key=value',
nargs='+',
default=[],
help='QoS specs to set/unset (only key is necessary on unset)')
def do_qos_key(cs, args):
"""Set or unset specifications for a qos spec."""
keypair = _extract_metadata(args)
if args.action == 'set':
cs.qos_specs.set_keys(args.qos_specs, keypair)
elif args.action == 'unset':
cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys()))
@utils.arg('qos_specs', metavar='<qos_specs>',
help='ID of the qos_specs.')
@utils.service_type('volumev2')
def do_qos_get_association(cs, args):
"""Get all associations of specific qos specs."""
associations = cs.qos_specs.get_associations(args.qos_specs)
_print_associations_list(associations)

View File

@@ -0,0 +1,96 @@
# 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):
"""
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
"""
raise NotImplementedError()

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
@@ -133,3 +133,7 @@ class SnapshotManager(base.ManagerWithFind):
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)

View File

@@ -1,4 +1,4 @@
# Copyright 2013 OpenStack LLC.
# 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.

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
@@ -112,6 +112,15 @@ class Volume(base.Resource):
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)
class VolumeManager(base.ManagerWithFind):
"""Manage :class:`Volume` resources."""
@@ -129,7 +138,6 @@ class VolumeManager(base.ManagerWithFind):
:param name: Name of the volume
:param 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
@@ -138,6 +146,7 @@ class VolumeManager(base.ManagerWithFind):
:param source_volid: ID of source volume to clone from
:param scheduler_hints: (optional extension) arbitrary key-value pairs
specified by the client to help boot an instance
:rtype: :class:`Volume`
"""
if metadata is None:
@@ -334,3 +343,37 @@ class VolumeManager(base.ManagerWithFind):
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]

View File

@@ -29,6 +29,31 @@ See also :doc:`/man/cinder`.
Release Notes
=============
1.0.6
-----
* Add support for multiple endpoints
* Add response info for backup command
* Add metadata option to cinder list command
* Add timeout parameter for requests
* Add update action for snapshot metadata
* Add encryption metadata support
* Add volume migrate support
.. _1221104: http://bugs.launchpad.net/python-cinderclient/+bug/1221104
.. _1220590: http://bugs.launchpad.net/python-cinderclient/+bug/1220590
.. _1220147: http://bugs.launchpad.net/python-cinderclient/+bug/1220147
.. _1214176: http://bugs.launchpad.net/python-cinderclient/+bug/1214176
.. _1210874: http://bugs.launchpad.net/python-cinderclient/+bug/1210874
.. _1210296: http://bugs.launchpad.net/python-cinderclient/+bug/1210296
.. _1210292: http://bugs.launchpad.net/python-cinderclient/+bug/1210292
.. _1207635: http://bugs.launchpad.net/python-cinderclient/+bug/1207635
.. _1207609: http://bugs.launchpad.net/python-cinderclient/+bug/1207609
.. _1207260: http://bugs.launchpad.net/python-cinderclient/+bug/1207260
.. _1206968: http://bugs.launchpad.net/python-cinderclient/+bug/1206968
.. _1203471: http://bugs.launchpad.net/python-cinderclient/+bug/1203471
.. _1200214: http://bugs.launchpad.net/python-cinderclient/+bug/1200214
.. _1195014: http://bugs.launchpad.net/python-cinderclient/+bug/1195014
1.0.5
-----
* Add CLI man page

View File

@@ -1,6 +1,7 @@
pbr>=0.5.16,<0.6
pbr>=0.5.21,<1.0
argparse
PrettyTable>=0.6,<0.8
requests>=1.1,<1.2.3
requests>=1.1
simplejson>=2.0.9
Babel>=0.9.6
six

View File

@@ -102,11 +102,6 @@ if [ $no_site_packages -eq 1 ]; then
installvenvopts="--no-site-packages"
fi
function init_testr {
if [ ! -d .testrepository ]; then
${wrapper} testr init
fi
}
function run_tests {
# Cleanup *pyc
@@ -223,7 +218,6 @@ if [ $recreate_db -eq 1 ]; then
rm -f tests.sqlite
fi
init_testr
run_tests
# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,

View File

@@ -18,5 +18,5 @@
import setuptools
setuptools.setup(
setup_requires=['pbr>=0.5.20'],
setup_requires=['pbr>=0.5.21,<1.0'],
pbr=True)

View File

@@ -114,12 +114,12 @@ class InstallVenv(object):
print('Installing dependencies with pip (this can take a while)...')
# First things first, make sure our venv has the latest pip and
# setuptools.
self.pip_install('pip>=1.3')
# setuptools and pbr
self.pip_install('pip>=1.4')
self.pip_install('setuptools')
self.pip_install('pbr')
self.pip_install('-r', self.requirements)
self.pip_install('-r', self.test_requirements)
self.pip_install('-r', self.requirements, '-r', self.test_requirements)
def post_process(self):
self.get_distro().post_process()
@@ -201,12 +201,13 @@ class Fedora(Distro):
RHEL: https://bugzilla.redhat.com/958868
"""
# Install "patch" program if it's not there
if not self.check_pkg('patch'):
self.die("Please install 'patch'.")
if os.path.exists('contrib/redhat-eventlet.patch'):
# Install "patch" program if it's not there
if not self.check_pkg('patch'):
self.die("Please install 'patch'.")
# Apply the eventlet patch
self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
'site-packages',
'eventlet/green/subprocess.py'),
'contrib/redhat-eventlet.patch')
# Apply the eventlet patch
self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
'site-packages',
'eventlet/green/subprocess.py'),
'contrib/redhat-eventlet.patch')

View File

@@ -1,4 +1,5 @@
[tox]
distribute = False
envlist = py26,py27,py33,pep8
[testenv]