Merge tag '1.0.5' into debian/unstable

Push python-cinderclient 1.0.5 to PyPi
This commit is contained in:
Thomas Goirand
2013-08-13 22:35:01 +02:00
93 changed files with 3411 additions and 1205 deletions

7
.coveragerc Normal file
View File

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

View File

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

115
HACKING
View File

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

70
HACKING.rst Normal file
View File

@@ -0,0 +1,70 @@
Cinder Client Style Commandments
=========================
- Step 1: Read the OpenStack Style Commandments
https://github.com/openstack-dev/hacking/blob/master/HACKING.rst
- Step 2: Read on
Cinder Client Specific Commandments
----------------------------
General
-------
- Do not use locals(). Example::
LOG.debug(_("volume %(vol_name)s: creating size %(vol_size)sG") %
locals()) # BAD
LOG.debug(_("volume %(vol_name)s: creating size %(vol_size)sG") %
{'vol_name': vol_name,
'vol_size': vol_size}) # OKAY
- Use 'raise' instead of 'raise e' to preserve original traceback or exception being reraised::
except Exception as e:
...
raise e # BAD
except Exception:
...
raise # OKAY
Text encoding
----------
- All text within python code should be of type 'unicode'.
WRONG:
>>> s = 'foo'
>>> s
'foo'
>>> type(s)
<type 'str'>
RIGHT:
>>> u = u'foo'
>>> u
u'foo'
>>> type(u)
<type 'unicode'>
- Transitions between internal unicode and external strings should always
be immediately and explicitly encoded or decoded.
- All external text that is not explicitly encoded (database storage,
commandline arguments, etc.) should be presumed to be encoded as utf-8.
WRONG:
mystring = infile.readline()
myreturnstring = do_some_magic_with(mystring)
outfile.write(myreturnstring)
RIGHT:
mystring = infile.readline()
mytext = s.decode('utf-8')
returntext = do_some_magic_with(mytext)
returnstring = returntext.encode('utf-8')
outfile.write(returnstring)

View File

@@ -14,9 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.openstack.common import version
__all__ = ['__version__']
version_info = version.VersionInfo('python-cinderclient')
import pbr.version
version_info = pbr.version.VersionInfo('python-cinderclient')
# We have a circular import problem when we first run python setup.py sdist
# It's harmless, so deflect it.
try:

View File

@@ -18,10 +18,13 @@
"""
Base utilities to build API operation managers and objects on top of.
"""
import abc
import contextlib
import hashlib
import os
import six
from cinderclient import exceptions
from cinderclient import utils
@@ -99,12 +102,13 @@ class Manager(utils.HookableMixin):
# pair
username = utils.env('OS_USERNAME', 'CINDER_USERNAME')
url = utils.env('OS_URL', 'CINDER_URL')
uniqifier = hashlib.md5(username + url).hexdigest()
uniqifier = hashlib.md5(username.encode('utf-8') +
url.encode('utf-8')).hexdigest()
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
try:
os.makedirs(cache_dir, 0755)
os.makedirs(cache_dir, 0o755)
except OSError:
# NOTE(kiall): This is typicaly either permission denied while
# attempting to create the directory, or the directory
@@ -163,10 +167,15 @@ class Manager(utils.HookableMixin):
return body
class ManagerWithFind(Manager):
class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)):
"""
Like a `Manager`, but with additional `find()`/`findall()` methods.
"""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""
Find a single item with attributes matching ``**kwargs``.
@@ -192,7 +201,7 @@ class ManagerWithFind(Manager):
the Python side.
"""
found = []
searches = kwargs.items()
searches = list(kwargs.items())
for obj in self.list():
try:
@@ -204,9 +213,6 @@ class ManagerWithFind(Manager):
return found
def list(self):
raise NotImplementedError
class Resource(object):
"""
@@ -245,7 +251,7 @@ class Resource(object):
return None
def _add_details(self, info):
for (k, v) in info.iteritems():
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
except AttributeError:
@@ -264,8 +270,8 @@ class Resource(object):
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
reprkeys = sorted(k for k in list(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)

View File

@@ -1,15 +1,34 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
from __future__ import print_function
import logging
import os
import urlparse
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
try:
from eventlet import sleep
except ImportError:
@@ -70,7 +89,7 @@ class HTTPClient(object):
self.verify_cert = True
self._logger = logging.getLogger(__name__)
if self.http_log_debug:
if self.http_log_debug and not self._logger.handlers:
ch = logging.StreamHandler()
self._logger.setLevel(logging.DEBUG)
self._logger.addHandler(ch)
@@ -195,7 +214,8 @@ class HTTPClient(object):
def _extract_service_catalog(self, url, resp, body, extract_token=True):
"""See what the auth service told us and process the response.
We may get redirected to another site, fail or actually get
back a service catalog with a token and our endpoints."""
back a service catalog with a token and our endpoints.
"""
if resp.status_code == 200: # content must always present
try:
@@ -216,13 +236,13 @@ class HTTPClient(object):
self.management_url = management_url.rstrip('/')
return None
except exceptions.AmbiguousEndpoints:
print "Found more than one valid endpoint. Use a more " \
"restrictive filter"
print("Found more than one valid endpoint. Use a more "
"restrictive filter")
raise
except KeyError:
raise exceptions.AuthorizationFailure()
except exceptions.EndpointNotFound:
print "Could not find any suitable endpoint. Correct region?"
print("Could not find any suitable endpoint. Correct region?")
raise
elif resp.status_code == 305:
@@ -357,6 +377,17 @@ class HTTPClient(object):
return self._extract_service_catalog(url, resp, body)
def get_volume_api_version_from_endpoint(self):
magic_tuple = urlparse.urlsplit(self.management_url)
scheme, netloc, path, query, frag = magic_tuple
v = path.split("/")[1]
valid_versions = ['v1', 'v2']
if v not in valid_versions:
msg = "Invalid client version '%s'. must be one of: %s" % (
(v, ', '.join(valid_versions)))
raise exceptions.UnsupportedVersion(msg)
return v[1:]
def get_client_class(version):
version_map = {
@@ -367,7 +398,7 @@ def get_client_class(version):
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = "Invalid client version '%s'. must be one of: %s" % (
(version, ', '.join(version_map.keys())))
(version, ', '.join(list(version_map.keys()))))
raise exceptions.UnsupportedVersion(msg)
return utils.import_class(client_path)

View File

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

View File

@@ -29,7 +29,7 @@ class Extension(utils.HookableMixin):
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
for attr_name, attr_value in list(self.module.__dict__.items()):
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
elif utils.safe_issubclass(attr_value, base.Manager):

View File

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

View File

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

View File

@@ -33,7 +33,8 @@ class ServiceCatalog(object):
service_name=None, volume_service_name=None):
"""Fetch the public URL from the Compute service for
a particular endpoint attribute. If none given, return
the first. See tests for sample service catalog."""
the first. See tests for sample service catalog.
"""
matching_endpoints = []
if 'endpoints' in self.catalog:
# We have a bastardized service catalog. Treat it special. :/
@@ -44,7 +45,7 @@ class ServiceCatalog(object):
raise cinderclient.exceptions.EndpointNotFound()
# We don't always get a service catalog back ...
if not 'serviceCatalog' in self.catalog['access']:
if 'serviceCatalog' not in self.catalog['access']:
return None
# Full catalog ...

View File

@@ -18,6 +18,8 @@
Command-line interface to the OpenStack Cinder API.
"""
from __future__ import print_function
import argparse
import glob
import imp
@@ -27,6 +29,8 @@ import pkgutil
import sys
import logging
import six
from cinderclient import client
from cinderclient import exceptions as exc
import cinderclient.extension
@@ -448,6 +452,15 @@ class OpenStackCinderShell(object):
except exc.AuthorizationFailure:
raise exc.CommandError("Unable to authorize user")
endpoint_api_version = self.cs.get_volume_api_version_from_endpoint()
if endpoint_api_version != options.os_volume_api_version:
msg = (("Volume API version is set to %s "
"but you are accessing a %s endpoint. "
"Change its value via either --os-volume-api-version "
"or env[OS_VOLUME_API_VERSION]")
% (options.os_volume_api_version, endpoint_api_version))
raise exc.InvalidAPIVersion(msg)
args.func(self.cs, args)
def _run_extension_hooks(self, hook_type, *args, **kwargs):
@@ -463,14 +476,14 @@ class OpenStackCinderShell(object):
"""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
for sc_str, sc in list(self.subcommands.items()):
commands.add(sc_str)
for option in sc._optionals._option_string_actions.keys():
for option in list(sc._optionals._option_string_actions.keys()):
options.add(option)
commands.remove('bash-completion')
commands.remove('bash_completion')
print ' '.join(commands | options)
print(' '.join(commands | options))
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
@@ -498,16 +511,20 @@ class OpenStackHelpFormatter(argparse.HelpFormatter):
def main():
try:
OpenStackCinderShell().main(map(strutils.safe_decode, sys.argv[1:]))
if sys.version_info >= (3, 0):
OpenStackCinderShell().main(sys.argv[1:])
else:
OpenStackCinderShell().main(map(strutils.safe_decode,
sys.argv[1:]))
except KeyboardInterrupt:
print >> sys.stderr, "... terminating cinder client"
print("... terminating cinder client", file=sys.stderr)
sys.exit(130)
except Exception, e:
except Exception as e:
logger.debug(e, exc_info=1)
message = e.message
if not isinstance(message, basestring):
if not isinstance(message, six.string_types):
message = str(message)
print >> sys.stderr, "ERROR: %s" % strutils.safe_encode(message)
print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr)
sys.exit(1)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,12 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import urlparse
from datetime import datetime
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from cinderclient import client as base_client
from cinderclient.tests import fakes
import cinderclient.tests.utils as utils
from cinderclient.v1 import client
from tests import fakes
import tests.utils as utils
def _stub_volume(**kwargs):
@@ -111,6 +116,48 @@ def _stub_restore():
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
def _stub_transfer_full(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'created_at': '2013-04-12T08:16:37.000000',
'auth_key': '123456',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_transfer(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_extend(id, new_size):
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
@@ -119,6 +166,9 @@ class FakeClient(fakes.FakeClient, client.Client):
extensions=kwargs.get('extensions'))
self.client = FakeHTTPClient(**kwargs)
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()
class FakeHTTPClient(base_client.HTTPClient):
@@ -127,6 +177,7 @@ class FakeHTTPClient(base_client.HTTPClient):
self.password = 'password'
self.auth_url = 'auth_url'
self.callstack = []
self.management_url = 'http://10.0.2.15:8776/v1/fake'
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
@@ -164,6 +215,11 @@ class FakeHTTPClient(base_client.HTTPClient):
else:
return utils.TestResponse({"status": status}), body
def get_volume_api_version_from_endpoint(self):
magic_tuple = urlparse.urlsplit(self.management_url)
scheme, netloc, path, query, frag = magic_tuple
return path.lstrip('/').split('/')[0][1:]
#
# Snapshots
#
@@ -181,6 +237,17 @@ class FakeHTTPClient(base_client.HTTPClient):
snapshot.update(kw['body']['snapshot'])
return (200, {}, {'snapshot': snapshot})
def post_snapshots_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(body.keys()) == 1
action = body.keys()[0]
if action == 'os-reset_status':
assert 'status' in body['os-reset_status']
else:
raise AssertionError('Unexpected action: %s" % action')
return (resp, {}, _body)
#
# Volumes
#
@@ -212,10 +279,10 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_volumes_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(body.keys()) == 1
action = body.keys()[0]
assert len(list(body.keys())) == 1
action = list(body.keys())[0]
if action == 'os-attach':
assert body[action].keys() == ['instance_uuid', 'mountpoint']
assert list(body[action].keys()) == ['instance_uuid', 'mountpoint']
elif action == 'os-detach':
assert body[action] is None
elif action == 'os-reserve':
@@ -223,16 +290,20 @@ class FakeHTTPClient(base_client.HTTPClient):
elif action == 'os-unreserve':
assert body[action] is None
elif action == 'os-initialize_connection':
assert body[action].keys() == ['connector']
assert list(body[action].keys()) == ['connector']
return (202, {}, {'connection_info': 'foos'})
elif action == 'os-terminate_connection':
assert body[action].keys() == ['connector']
assert list(body[action].keys()) == ['connector']
elif action == 'os-begin_detaching':
assert body[action] is None
elif action == 'os-roll_detaching':
assert body[action] is None
elif action == 'os-reset_status':
assert 'status' in body[action]
elif action == 'os-extend':
assert body[action].keys() == ['new_size']
else:
raise AssertionError("Unexpected server action: %s" % action)
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
def post_volumes(self, **kw):
@@ -262,7 +333,7 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1}})
def put_os_quota_sets_test(self, body, **kw):
assert body.keys() == ['quota_set']
assert list(body.keys()) == ['quota_set']
fakes.assert_has_keys(body['quota_set'],
required=['tenant_id'])
return (200, {}, {'quota_set': {
@@ -285,7 +356,7 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1}})
def put_os_quota_class_sets_test(self, body, **kw):
assert body.keys() == ['quota_class_set']
assert list(body.keys()) == ['quota_class_set']
fakes.assert_has_keys(body['quota_class_set'],
required=['class_name'])
return (200, {}, {'quota_class_set': {
@@ -302,10 +373,10 @@ class FakeHTTPClient(base_client.HTTPClient):
return (200, {}, {
'volume_types': [{'id': 1,
'name': 'test-type-1',
'extra_specs':{}},
'extra_specs': {}},
{'id': 2,
'name': 'test-type-2',
'extra_specs':{}}]})
'extra_specs': {}}]})
def get_types_1(self, **kw):
return (200, {}, {'volume_type': {'id': 1,
@@ -318,7 +389,7 @@ class FakeHTTPClient(base_client.HTTPClient):
'extra_specs': {}}})
def post_types_1_extra_specs(self, body, **kw):
assert body.keys() == ['extra_specs']
assert list(body.keys()) == ['extra_specs']
return (200, {}, {'extra_specs': {'k': 'v'}})
def delete_types_1_extra_specs_k(self, **kw):
@@ -402,3 +473,143 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw):
return (200, {},
{'restore': _stub_restore()})
#
# VolumeTransfers
#
def get_os_volume_transfer_5678(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer':
_stub_transfer_full(transfer1, base_uri, tenant_id)})
def get_os_volume_transfer_detail(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41'
return (200, {},
{'transfers': [
_stub_transfer_full(transfer1, base_uri, tenant_id),
_stub_transfer_full(transfer2, base_uri, tenant_id)]})
def delete_os_volume_transfer_5678(self, **kw):
return (202, {}, None)
def post_os_volume_transfer(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (202, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
def post_os_volume_transfer_5678_accept(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
#
# Services
#
def get_os_services(self, **kw):
host = kw.get('host', None)
binary = kw.get('binary', None)
services = [
{
'binary': 'cinder-volume',
'host': 'host1',
'zone': 'cinder',
'status': 'enabled',
'state': 'up',
'updated_at': datetime(2012, 10, 29, 13, 42, 2)
},
{
'binary': 'cinder-volume',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
{
'binary': 'cinder-scheduler',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
]
if host:
services = filter(lambda i: i['host'] == host, services)
if binary:
services = filter(lambda i: i['binary'] == binary, services)
return (200, {}, {'services': services})
def put_os_services_enable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled'})
def put_os_services_disable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'enabled'})
def get_os_availability_zone(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": None,
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})
def get_os_availability_zone_detail(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-volume": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 25, 0)
}
}
}
},
{
"zoneName": "internal",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-sched": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 24, 0)
}
}
}
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})

View File

@@ -1,3 +1,16 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import mock
@@ -5,7 +18,7 @@ import requests
from cinderclient.v1 import client
from cinderclient import exceptions
from tests import utils
from cinderclient.tests import utils
class AuthenticateAgainstKeystoneTests(utils.TestCase):
@@ -192,7 +205,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
}
correct_response = json.dumps(dict_correct_response)
dict_responses = [
{"headers": {'location':'http://127.0.0.1:5001'},
{"headers": {'location': 'http://127.0.0.1:5001'},
"status_code": 305,
"text": "Use proxy"},
# Configured on admin port, cinder redirects to v2.0 port.

View File

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

View File

@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
@@ -29,7 +29,7 @@ class QuotaClassSetsTest(utils.TestCase):
def test_update_quota(self):
q = cs.quota_classes.get('test')
q.update(volumes=2)
q.update(volumes=2, snapshots=2)
cs.assert_called('PUT', '/os-quota-class-sets/test')
def test_refresh_quota(self):

View File

@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()

View File

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

View File

@@ -15,15 +15,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import fixtures
from cinderclient import client
from cinderclient import shell
from cinderclient.v1 import shell as shell_v1
from tests.v1 import fakes
from tests import utils
from cinderclient.tests.v1 import fakes
from cinderclient.tests import utils
class ShellTest(utils.TestCase):
@@ -32,7 +30,7 @@ class ShellTest(utils.TestCase):
'CINDER_USERNAME': 'username',
'CINDER_PASSWORD': 'password',
'CINDER_PROJECT_ID': 'project_id',
'OS_VOLUME_API_VERSION': '1.1',
'OS_VOLUME_API_VERSION': '1',
'CINDER_URL': 'http://no.where',
}
@@ -107,6 +105,10 @@ class ShellTest(utils.TestCase):
self.run_command('list --all-tenants=1')
self.assert_called('GET', '/volumes/detail?all_tenants=1')
def test_list_availability_zone(self):
self.run_command('availability-zone-list')
self.assert_called('GET', '/os-availability-zone')
def test_show(self):
self.run_command('show 1234')
self.assert_called('GET', '/volumes/1234')
@@ -181,3 +183,23 @@ class ShellTest(utils.TestCase):
self.run_command('metadata 1234 unset key1 key2')
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
def test_reset_state(self):
self.run_command('reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_with_flag(self):
self.run_command('reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_snapshot_reset_state(self):
self.run_command('snapshot-reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)
def test_snapshot_reset_state_with_flag(self):
self.run_command('snapshot-reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)

View File

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

View File

@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()

View File

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

View File

@@ -1,5 +1,18 @@
from tests import utils
from tests.v1 import fakes
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
cs = fakes.FakeClient()
@@ -69,3 +82,8 @@ class VolumesTest(utils.TestCase):
keys = ['key1']
cs.volumes.delete_metadata(1234, keys)
cs.assert_called('DELETE', '/volumes/1234/metadata/key1')
def test_extend(self):
v = cs.volumes.get('1234')
cs.volumes.extend(v, 2)
cs.assert_called('POST', '/volumes/1234/action')

View File

@@ -16,8 +16,8 @@
from cinderclient import extension
from cinderclient.v2.contrib import list_extensions
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v1 import fakes
extensions = [

View File

@@ -12,12 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import urlparse
from datetime import datetime
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from cinderclient import client as base_client
from cinderclient.tests import fakes
import cinderclient.tests.utils as utils
from cinderclient.v2 import client
from tests import fakes
import tests.utils as utils
def _stub_volume(**kwargs):
@@ -118,6 +123,48 @@ def _stub_restore():
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
def _stub_transfer_full(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'created_at': '2013-04-12T08:16:37.000000',
'auth_key': '123456',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_transfer(id, base_uri, tenant_id):
return {
'id': id,
'name': 'transfer',
'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc',
'links': [
{
'href': _self_href(base_uri, tenant_id, id),
'rel': 'self'
},
{
'href': _bookmark_href(base_uri, tenant_id, id),
'rel': 'bookmark'
}
]
}
def _stub_extend(id, new_size):
return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'}
class FakeClient(fakes.FakeClient, client.Client):
def __init__(self, *args, **kwargs):
@@ -126,6 +173,9 @@ class FakeClient(fakes.FakeClient, client.Client):
extensions=kwargs.get('extensions'))
self.client = FakeHTTPClient(**kwargs)
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()
class FakeHTTPClient(base_client.HTTPClient):
@@ -134,6 +184,7 @@ class FakeHTTPClient(base_client.HTTPClient):
self.password = 'password'
self.auth_url = 'auth_url'
self.callstack = []
self.management_url = 'http://10.0.2.15:8776/v2/fake'
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
@@ -171,6 +222,11 @@ class FakeHTTPClient(base_client.HTTPClient):
else:
return utils.TestResponse({"status": status}), body
def get_volume_api_version_from_endpoint(self):
magic_tuple = urlparse.urlsplit(self.management_url)
scheme, netloc, path, query, frag = magic_tuple
return path.lstrip('/').split('/')[0][1:]
#
# Snapshots
#
@@ -188,6 +244,17 @@ class FakeHTTPClient(base_client.HTTPClient):
snapshot.update(kw['body']['snapshot'])
return (200, {}, {'snapshot': snapshot})
def post_snapshots_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(body.keys()) == 1
action = body.keys()[0]
if action == 'os-reset_status':
assert 'status' in body['os-reset_status']
else:
raise AssertionError('Unexpected action: %s" % action')
return (resp, {}, _body)
#
# Volumes
#
@@ -219,10 +286,10 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_volumes_1234_action(self, body, **kw):
_body = None
resp = 202
assert len(body.keys()) == 1
action = body.keys()[0]
assert len(list(body.keys())) == 1
action = list(body.keys())[0]
if action == 'os-attach':
assert body[action].keys() == ['instance_uuid', 'mountpoint']
assert list(body[action].keys()) == ['instance_uuid', 'mountpoint']
elif action == 'os-detach':
assert body[action] is None
elif action == 'os-reserve':
@@ -230,16 +297,20 @@ class FakeHTTPClient(base_client.HTTPClient):
elif action == 'os-unreserve':
assert body[action] is None
elif action == 'os-initialize_connection':
assert body[action].keys() == ['connector']
assert list(body[action].keys()) == ['connector']
return (202, {}, {'connection_info': 'foos'})
elif action == 'os-terminate_connection':
assert body[action].keys() == ['connector']
assert list(body[action].keys()) == ['connector']
elif action == 'os-begin_detaching':
assert body[action] is None
elif action == 'os-roll_detaching':
assert body[action] is None
elif action == 'os-reset_status':
assert 'status' in body[action]
elif action == 'os-extend':
assert body[action].keys() == ['new_size']
else:
raise AssertionError("Unexpected server action: %s" % action)
raise AssertionError("Unexpected action: %s" % action)
return (resp, {}, _body)
def post_volumes(self, **kw):
@@ -269,7 +340,7 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1}})
def put_os_quota_sets_test(self, body, **kw):
assert body.keys() == ['quota_set']
assert list(body.keys()) == ['quota_set']
fakes.assert_has_keys(body['quota_set'],
required=['tenant_id'])
return (200, {}, {'quota_set': {
@@ -292,7 +363,7 @@ class FakeHTTPClient(base_client.HTTPClient):
'gigabytes': 1}})
def put_os_quota_class_sets_test(self, body, **kw):
assert body.keys() == ['quota_class_set']
assert list(body.keys()) == ['quota_class_set']
fakes.assert_has_keys(body['quota_class_set'],
required=['class_name'])
return (200, {}, {'quota_class_set': {
@@ -309,10 +380,10 @@ class FakeHTTPClient(base_client.HTTPClient):
return (200, {}, {
'volume_types': [{'id': 1,
'name': 'test-type-1',
'extra_specs':{}},
'extra_specs': {}},
{'id': 2,
'name': 'test-type-2',
'extra_specs':{}}]})
'extra_specs': {}}]})
def get_types_1(self, **kw):
return (200, {}, {'volume_type': {'id': 1,
@@ -325,7 +396,7 @@ class FakeHTTPClient(base_client.HTTPClient):
'extra_specs': {}}})
def post_types_1_extra_specs(self, body, **kw):
assert body.keys() == ['extra_specs']
assert list(body.keys()) == ['extra_specs']
return (200, {}, {'extra_specs': {'k': 'v'}})
def delete_types_1_extra_specs_k(self, **kw):
@@ -409,3 +480,143 @@ class FakeHTTPClient(base_client.HTTPClient):
def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw):
return (200, {},
{'restore': _stub_restore()})
#
# VolumeTransfers
#
def get_os_volume_transfer_5678(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer':
_stub_transfer_full(transfer1, base_uri, tenant_id)})
def get_os_volume_transfer_detail(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41'
return (200, {},
{'transfers': [
_stub_transfer_full(transfer1, base_uri, tenant_id),
_stub_transfer_full(transfer2, base_uri, tenant_id)]})
def delete_os_volume_transfer_5678(self, **kw):
return (202, {}, None)
def post_os_volume_transfer(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (202, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
def post_os_volume_transfer_5678_accept(self, **kw):
base_uri = 'http://localhost:8776'
tenant_id = '0fa851f6668144cf9cd8c8419c1646c1'
transfer1 = '5678'
return (200, {},
{'transfer': _stub_transfer(transfer1, base_uri, tenant_id)})
#
# Services
#
def get_os_services(self, **kw):
host = kw.get('host', None)
binary = kw.get('binary', None)
services = [
{
'binary': 'cinder-volume',
'host': 'host1',
'zone': 'cinder',
'status': 'enabled',
'state': 'up',
'updated_at': datetime(2012, 10, 29, 13, 42, 2)
},
{
'binary': 'cinder-volume',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
{
'binary': 'cinder-scheduler',
'host': 'host2',
'zone': 'cinder',
'status': 'disabled',
'state': 'down',
'updated_at': datetime(2012, 9, 18, 8, 3, 38)
},
]
if host:
services = filter(lambda i: i['host'] == host, services)
if binary:
services = filter(lambda i: i['binary'] == binary, services)
return (200, {}, {'services': services})
def put_os_services_enable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'disabled'})
def put_os_services_disable(self, body, **kw):
return (200, {}, {'host': body['host'], 'binary': body['binary'],
'status': 'enabled'})
def get_os_availability_zone(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": None,
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})
def get_os_availability_zone_detail(self, **kw):
return (200, {}, {
"availabilityZoneInfo": [
{
"zoneName": "zone-1",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-volume": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 25, 0)
}
}
}
},
{
"zoneName": "internal",
"zoneState": {"available": True},
"hosts": {
"fake_host-1": {
"cinder-sched": {
"active": True,
"available": True,
"updated_at":
datetime(2012, 12, 26, 14, 45, 24, 0)
}
}
}
},
{
"zoneName": "zone-2",
"zoneState": {"available": False},
"hosts": None,
},
]
})

View File

@@ -21,7 +21,7 @@ import requests
from cinderclient import exceptions
from cinderclient.v2 import client
from tests import utils
from cinderclient.tests import utils
class AuthenticateAgainstKeystoneTests(utils.TestCase):
@@ -208,7 +208,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
}
correct_response = json.dumps(dict_correct_response)
dict_responses = [
{"headers": {'location':'http://127.0.0.1:5001'},
{"headers": {'location': 'http://127.0.0.1:5001'},
"status_code": 305,
"text": "Use proxy"},
# Configured on admin port, cinder redirects to v2.0 port.

View File

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

View File

@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
@@ -29,7 +29,7 @@ class QuotaClassSetsTest(utils.TestCase):
def test_update_quota(self):
q = cs.quota_classes.get('test')
q.update(volumes=2)
q.update(volumes=2, snapshots=2)
cs.assert_called('PUT', '/os-quota-class-sets/test')
def test_refresh_quota(self):

View File

@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()

View File

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

View File

@@ -17,8 +17,8 @@ import fixtures
from cinderclient import client
from cinderclient import shell
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
class ShellTest(utils.TestCase):
@@ -83,6 +83,10 @@ class ShellTest(utils.TestCase):
self.run_command('list --all-tenants=1')
self.assert_called('GET', '/volumes/detail?all_tenants=1')
def test_list_availability_zone(self):
self.run_command('availability-zone-list')
self.assert_called('GET', '/os-availability-zone')
def test_show(self):
self.run_command('show 1234')
self.assert_called('GET', '/volumes/1234')
@@ -157,3 +161,23 @@ class ShellTest(utils.TestCase):
self.run_command('metadata 1234 unset key1 key2')
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
def test_reset_state(self):
self.run_command('reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_reset_state_with_flag(self):
self.run_command('reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_snapshot_reset_state(self):
self.run_command('snapshot-reset-state 1234')
expected = {'os-reset_status': {'status': 'available'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)
def test_snapshot_reset_state_with_flag(self):
self.run_command('snapshot-reset-state --state error 1234')
expected = {'os-reset_status': {'status': 'error'}}
self.assert_called('POST', '/snapshots/1234/action', body=expected)

View File

@@ -15,8 +15,8 @@
# under the License.
from cinderclient.v2 import volume_types
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()

View File

@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v1 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()

View File

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

View File

@@ -14,8 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from tests import utils
from tests.v2 import fakes
from cinderclient.tests import utils
from cinderclient.tests.v2 import fakes
cs = fakes.FakeClient()
@@ -85,3 +85,8 @@ class VolumesTest(utils.TestCase):
keys = ['key1']
cs.volumes.delete_metadata(1234, keys)
cs.assert_called('DELETE', '/volumes/1234/metadata/key1')
def test_extend(self):
v = cs.volumes.get('1234')
cs.volumes.extend(v, 2)
cs.assert_called('POST', '/volumes/1234/action')

View File

@@ -1,8 +1,26 @@
# Copyright 2013 OpenStack LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import os
import re
import sys
import uuid
import six
import prettytable
from cinderclient import exceptions
@@ -70,8 +88,12 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
if conflicting_keys and not allow_conflicts:
raise Exception("Hook '%(hook_name)s' is attempting to redefine"
" attributes '%(conflicting_keys)s'" % locals())
msg = ("Hook '%(hook_name)s' is attempting to redefine attributes "
"'%(conflicting_keys)s'" % {
'hook_name': hook_name,
'conflicting_keys': conflicting_keys
})
raise Exception(msg)
extra_kwargs.update(hook_kwargs)
@@ -124,7 +146,14 @@ def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def print_list(objs, fields, formatters={}):
def _print(pt, order):
if sys.version_info >= (3, 0):
print(pt.get_string(sortby=order))
else:
print(strutils.safe_encode(pt.get_string(sortby=order)))
def print_list(objs, fields, formatters={}, order_by=None):
mixed_case_fields = ['serverId']
pt = prettytable.PrettyTable([f for f in fields], caching=False)
pt.aligns = ['l' for f in fields]
@@ -143,15 +172,16 @@ def print_list(objs, fields, formatters={}):
row.append(data)
pt.add_row(row)
if len(objs) > 0:
print strutils.safe_encode(pt.get_string(sortby=fields[0]))
if order_by is None:
order_by = fields[0]
_print(pt, order_by)
def print_dict(d, property="Property"):
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
pt.aligns = ['l', 'l']
[pt.add_row(list(r)) for r in d.iteritems()]
print strutils.safe_encode(pt.get_string(sortby=property))
[pt.add_row(list(r)) for r in six.iteritems(d)]
_print(pt, property)
def find_resource(manager, name_or_id):
@@ -163,9 +193,12 @@ def find_resource(manager, name_or_id):
except exceptions.NotFound:
pass
if sys.version_info <= (3, 0):
name_or_id = strutils.safe_decode(name_or_id)
# now try to get entity as uuid
try:
uuid.UUID(strutils.safe_decode(name_or_id))
uuid.UUID(name_or_id)
return manager.get(name_or_id)
except (ValueError, exceptions.NotFound):
pass
@@ -199,7 +232,7 @@ def find_resource(manager, name_or_id):
def _format_servers_list_networks(server):
output = []
for (network, addresses) in server.networks.items():
for (network, addresses) in list(server.networks.items()):
if len(addresses) == 0:
continue
addresses_csv = ', '.join(addresses)
@@ -259,8 +292,8 @@ def slugify(value):
From Django's "django/template/defaultfilters.py".
"""
import unicodedata
if not isinstance(value, unicode):
value = unicode(value)
if not isinstance(value, six.text_type):
value = six.text_type(value)
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(_slugify_strip_re.sub('', value).strip().lower())
value = six.text_type(_slugify_strip_re.sub('', value).strip().lower())
return _slugify_hyphenate_re.sub('-', value)

View File

@@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.v1.client import Client
from cinderclient.v1.client import Client # noqa

View File

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

View File

@@ -1,12 +1,30 @@
# Copyright 2013 OpenStack LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient import client
from cinderclient.v1 import availability_zones
from cinderclient.v1 import limits
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_backups
from cinderclient.v1 import volume_backups_restore
from cinderclient.v1 import volume_transfers
class Client(object):
@@ -45,6 +63,10 @@ class Client(object):
self.quotas = quotas.QuotaSetManager(self)
self.backups = volume_backups.VolumeBackupManager(self)
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
# Add in any extensions...
if extensions:
@@ -83,3 +105,6 @@ class Client(object):
credentials are wrong.
"""
self.client.authenticate()
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()

View File

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

View File

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

View File

@@ -21,32 +21,25 @@ class QuotaClassSet(base.Resource):
@property
def id(self):
"""QuotaClassSet does not have a 'id' attribute but base.Resource
needs it to self-refresh and QuotaSet is indexed by class_name"""
needs it to self-refresh and QuotaSet is indexed by class_name.
"""
return self.class_name
def update(self, *args, **kwargs):
self.manager.update(self.class_name, *args, **kwargs)
class QuotaClassSetManager(base.ManagerWithFind):
class QuotaClassSetManager(base.Manager):
resource_class = QuotaClassSet
def get(self, class_name):
return self._get("/os-quota-class-sets/%s" % (class_name),
"quota_class_set")
def update(self,
class_name,
volumes=None,
gigabytes=None):
def update(self, class_name, **updates):
body = {'quota_class_set': {'class_name': class_name}}
body = {'quota_class_set': {
'class_name': class_name,
'volumes': volumes,
'gigabytes': gigabytes}}
for key in body['quota_class_set'].keys():
if body['quota_class_set'][key] is None:
body['quota_class_set'].pop(key)
for update in updates.keys():
body['quota_class_set'][update] = updates[update]
self._update('/os-quota-class-sets/%s' % (class_name), body)

View File

@@ -20,15 +20,16 @@ class QuotaSet(base.Resource):
@property
def id(self):
"""QuotaSet does not have a 'id' attribute but base.Resource needs it
to self-refresh and QuotaSet is indexed by tenant_id"""
"""QuotaSet does not have a 'id' attribute but base. Resource needs it
to self-refresh and QuotaSet is indexed by tenant_id.
"""
return self.tenant_id
def update(self, *args, **kwargs):
self.manager.update(self.tenant_id, *args, **kwargs)
class QuotaSetManager(base.ManagerWithFind):
class QuotaSetManager(base.Manager):
resource_class = QuotaSet
def get(self, tenant_id):
@@ -36,17 +37,11 @@ class QuotaSetManager(base.ManagerWithFind):
tenant_id = tenant_id.tenant_id
return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set")
def update(self, tenant_id, volumes=None, snapshots=None, gigabytes=None):
def update(self, tenant_id, **updates):
body = {'quota_set': {'tenant_id': tenant_id}}
body = {'quota_set': {
'tenant_id': tenant_id,
'volumes': volumes,
'snapshots': snapshots,
'gigabytes': gigabytes}}
for key in body['quota_set'].keys():
if body['quota_set'][key] is None:
body['quota_set'].pop(key)
for update in updates.keys():
body['quota_set'][update] = updates[update]
self._update('/os-quota-sets/%s' % (tenant_id), body)

View File

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

View File

@@ -15,13 +15,17 @@
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import argparse
import copy
import os
import sys
import time
from cinderclient import exceptions
from cinderclient import utils
from cinderclient.v1 import availability_zones
def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
@@ -39,17 +43,17 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
sys.stdout.write(msg)
sys.stdout.flush()
print
print()
while True:
obj = poll_fn(obj_id)
status = obj.status.lower()
progress = getattr(obj, 'progress', None) or 0
if status in final_ok_states:
print_progress(100)
print "\nFinished"
print("\nFinished")
break
elif status == "error":
print "\nError %(action)s instance" % locals()
print("\nError %(action)s instance" % {'action': action})
break
else:
print_progress(progress)
@@ -71,6 +75,11 @@ def _find_backup(cs, backup):
return utils.find_resource(cs.backups, backup)
def _find_transfer(cs, transfer):
"""Get a transfer by ID."""
return utils.find_resource(cs.transfers, transfer)
def _print_volume(volume):
utils.print_dict(volume._info)
@@ -79,9 +88,13 @@ def _print_volume_snapshot(snapshot):
utils.print_dict(snapshot._info)
def _print_volume_image(image):
utils.print_dict(image[1]['os-volume_upload_image'])
def _translate_keys(collection, convert):
for item in collection:
keys = item.__dict__.keys()
keys = list(item.__dict__.keys())
for from_key, to_key in convert:
if from_key in keys and to_key not in keys:
setattr(item, to_key, item._info[from_key])
@@ -97,6 +110,11 @@ def _translate_volume_snapshot_keys(collection):
_translate_keys(collection, convert)
def _translate_availability_zone_keys(collection):
convert = [('zoneName', 'name'), ('zoneState', 'status')]
_translate_keys(collection, convert)
def _extract_metadata(args):
metadata = {}
for metadatum in args.metadata:
@@ -266,6 +284,18 @@ def do_force_delete(cs, args):
volume.force_delete()
@utils.arg('volume', metavar='<volume>', help='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')
def do_reset_state(cs, args):
"""Explicitly update the state of a volume."""
volume = _find_volume(cs, args.volume)
volume.reset_state(args.state)
@utils.arg('volume', metavar='<volume>', help='ID of the volume to rename.')
@utils.arg('display_name', nargs='?', metavar='<display-name>',
help='New display-name for the volume.')
@@ -304,7 +334,7 @@ def do_metadata(cs, args):
if args.action == 'set':
cs.volumes.set_metadata(volume, metadata)
elif args.action == 'unset':
cs.volumes.delete_metadata(volume, metadata.keys())
cs.volumes.delete_metadata(volume, list(metadata.keys()))
@utils.arg(
@@ -424,6 +454,21 @@ def do_snapshot_rename(cs, args):
_find_volume_snapshot(cs, args.snapshot).update(**kwargs)
@utils.arg('snapshot', metavar='<snapshot>',
help='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('volume')
def do_snapshot_reset_state(cs, args):
"""Explicitly update the state of a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot)
snapshot.reset_state(args.state)
def _print_volume_type_list(vtypes):
utils.print_list(vtypes, ['ID', 'Name'])
@@ -462,7 +507,7 @@ def do_type_create(cs, args):
help="Unique ID of the volume type to delete")
@utils.service_type('volume')
def do_type_delete(cs, args):
"""Delete a specific volume type"""
"""Delete a specific volume type."""
cs.volume_types.delete(args.id)
@@ -489,28 +534,35 @@ def do_type_key(cs, args):
if args.action == 'set':
vtype.set_keys(keypair)
elif args.action == 'unset':
vtype.unset_keys(keypair.keys())
vtype.unset_keys(list(keypair.keys()))
def do_endpoints(cs, args):
"""Discover endpoints that get returned from the authenticate services"""
"""Discover endpoints that get returned from the authenticate services."""
catalog = cs.client.service_catalog.catalog
for e in catalog['access']['serviceCatalog']:
utils.print_dict(e['endpoints'][0], e['name'])
def do_credentials(cs, args):
"""Show user credentials returned from auth"""
"""Show user credentials returned from auth."""
catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials")
utils.print_dict(catalog['access']['token'], "Token")
_quota_resources = ['volumes', 'snapshots', 'gigabytes']
def _quota_show(quotas):
quota_dict = {}
for resource in _quota_resources:
for resource in quotas._info.keys():
good_name = False
for name in _quota_resources:
if resource.startswith(name):
good_name = True
if not good_name:
continue
quota_dict[resource] = getattr(quotas, resource, None)
utils.print_dict(quota_dict)
@@ -520,6 +572,8 @@ def _quota_update(manager, identifier, args):
for resource in _quota_resources:
val = getattr(args, resource, None)
if val is not None:
if args.volume_type:
resource = resource + '_%s' % args.volume_type
updates[resource] = val
if updates:
@@ -558,6 +612,10 @@ def do_quota_defaults(cs, args):
metavar='<gigabytes>',
type=int, default=None,
help='New value for the "gigabytes" quota.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
help='Volume type (Optional, Default=None)')
@utils.service_type('volume')
def do_quota_update(cs, args):
"""Update the quotas for a tenant."""
@@ -588,6 +646,10 @@ def do_quota_class_show(cs, args):
metavar='<gigabytes>',
type=int, default=None,
help='New value for the "gigabytes" quota.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
help='Volume type (Optional, Default=None)')
@utils.service_type('volume')
def do_quota_class_update(cs, args):
"""Update the quotas for a quota class."""
@@ -649,10 +711,10 @@ def _find_volume_type(cs, vtype):
def do_upload_to_image(cs, args):
"""Upload volume to image service as image."""
volume = _find_volume(cs, args.volume_id)
volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format)
_print_volume_image(volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format))
@utils.arg('volume', metavar='<volume>',
@@ -717,3 +779,171 @@ def do_backup_restore(cs, args):
"""Restore a backup."""
cs.restores.restore(args.backup,
args.volume_id)
@utils.arg('volume', metavar='<volume>',
help='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,
args.display_name)
info = dict()
info.update(transfer._info)
if 'links' in info:
info.pop('links')
utils.print_dict(info)
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to delete.')
@utils.service_type('volume')
def do_transfer_delete(cs, args):
"""Undo a transfer."""
transfer = _find_transfer(cs, args.transfer)
transfer.delete()
@utils.arg('transfer', metavar='<transfer>',
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')
def do_transfer_accept(cs, args):
"""Accepts a volume transfer."""
transfer = cs.transfers.accept(args.transfer, args.auth_key)
info = dict()
info.update(transfer._info)
if 'links' in info:
info.pop('links')
utils.print_dict(info)
@utils.service_type('volume')
def do_transfer_list(cs, args):
"""List all the transfers."""
transfers = cs.transfers.list()
columns = ['ID', 'Volume ID', 'Name']
utils.print_list(transfers, columns)
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to accept.')
@utils.service_type('volume')
def do_transfer_show(cs, args):
"""Show details about a transfer."""
transfer = _find_transfer(cs, args.transfer)
info = dict()
info.update(transfer._info)
if 'links' in info:
info.pop('links')
utils.print_dict(info)
@utils.arg('volume', metavar='<volume>', help='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')
def do_extend(cs, args):
"""Attempt to extend the size of an existing volume."""
volume = _find_volume(cs, args.volume)
cs.volumes.extend(volume, args.new_size)
@utils.arg('--host', metavar='<hostname>', default=None,
help='Name of host.')
@utils.arg('--binary', metavar='<binary>', default=None,
help='Service binary.')
@utils.service_type('volume')
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)
columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"]
utils.print_list(result, columns)
@utils.arg('host', metavar='<hostname>', help='Name of host.')
@utils.arg('binary', metavar='<binary>', help='Service binary.')
@utils.service_type('volume')
def do_service_enable(cs, args):
"""Enable the service."""
cs.services.enable(args.host, args.binary)
@utils.arg('host', metavar='<hostname>', help='Name of host.')
@utils.arg('binary', metavar='<binary>', help='Service binary.')
@utils.service_type('volume')
def do_service_disable(cs, args):
"""Disable the service."""
cs.services.disable(args.host, args.binary)
def _treeizeAvailabilityZone(zone):
"""Build a tree view for availability zones."""
AvailabilityZone = availability_zones.AvailabilityZone
az = AvailabilityZone(zone.manager,
copy.deepcopy(zone._info), zone._loaded)
result = []
# Zone tree view item
az.zoneName = zone.zoneName
az.zoneState = ('available'
if zone.zoneState['available'] else 'not available')
az._info['zoneName'] = az.zoneName
az._info['zoneState'] = az.zoneState
result.append(az)
if getattr(zone, "hosts", None) and zone.hosts is not None:
for (host, services) in zone.hosts.items():
# Host tree view item
az = AvailabilityZone(zone.manager,
copy.deepcopy(zone._info), zone._loaded)
az.zoneName = '|- %s' % host
az.zoneState = ''
az._info['zoneName'] = az.zoneName
az._info['zoneState'] = az.zoneState
result.append(az)
for (svc, state) in services.items():
# Service tree view item
az = AvailabilityZone(zone.manager,
copy.deepcopy(zone._info), zone._loaded)
az.zoneName = '| |- %s' % svc
az.zoneState = '%s %s %s' % (
'enabled' if state['active'] else 'disabled',
':-)' if state['available'] else 'XXX',
state['updated_at'])
az._info['zoneName'] = az.zoneName
az._info['zoneState'] = az.zoneState
result.append(az)
return result
@utils.service_type('volume')
def do_availability_zone_list(cs, _args):
"""List all the availability zones."""
try:
availability_zones = cs.availability_zones.list()
except exceptions.Forbidden as e: # policy doesn't allow probably
try:
availability_zones = cs.availability_zones.list(detailed=False)
except Exception:
raise e
result = []
for zone in availability_zones:
result += _treeizeAvailabilityZone(zone)
_translate_availability_zone_keys(result)
utils.print_list(result, ['Name', 'Status'])

View File

@@ -27,7 +27,7 @@ class VolumeBackupsRestore(base.Resource):
return "<VolumeBackupsRestore: %s>" % self.id
class VolumeBackupRestoreManager(base.ManagerWithFind):
class VolumeBackupRestoreManager(base.Manager):
"""Manage :class:`VolumeBackupsRestore` resources."""
resource_class = VolumeBackupsRestore

View File

@@ -17,8 +17,13 @@
Volume snapshot interface (1.1 extension).
"""
import urllib
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
import six
class Snapshot(base.Resource):
@@ -48,6 +53,10 @@ class Snapshot(base.Resource):
def project_id(self):
return self._info.get('os-extended-snapshot-attributes:project_id')
def reset_state(self, state):
"""Update the snapshot with the privided state."""
self.manager.reset_state(self, state)
class SnapshotManager(base.ManagerWithFind):
"""
@@ -95,11 +104,11 @@ class SnapshotManager(base.ManagerWithFind):
qparams = {}
for opt, val in search_opts.iteritems():
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
@@ -128,3 +137,14 @@ class SnapshotManager(base.ManagerWithFind):
body = {"snapshot": kwargs}
self._update("/snapshots/%s" % base.getid(snapshot), body)
def reset_state(self, snapshot, state):
"""Update the specified volume with the provided state."""
return self._action('os-reset_status', snapshot, {'status': state})
def _action(self, action, snapshot, info=None, **kwargs):
"""Perform a snapshot action."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/snapshots/%s/action' % base.getid(snapshot)
return self.api.client.post(url, body=body)

View File

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

View File

@@ -17,7 +17,11 @@
Volume interface (1.1 extension).
"""
import urllib
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import six
from cinderclient import base
@@ -87,8 +91,8 @@ class Volume(base.Resource):
def upload_to_image(self, force, image_name, container_format,
disk_format):
"""Upload a volume to image service as an image."""
self.manager.upload_to_image(self, force, image_name, container_format,
disk_format)
return self.manager.upload_to_image(self, force, image_name,
container_format, disk_format)
def force_delete(self):
"""Delete the specified volume ignoring its current state.
@@ -97,6 +101,19 @@ class Volume(base.Resource):
"""
self.manager.force_delete(self)
def reset_state(self, state):
"""Update the volume with the provided state."""
self.manager.reset_state(self, state)
def extend(self, volume, new_size):
"""Extend the size of the specified volume.
:param volume: The UUID of the volume to extend
:param new_size: The desired size to extend volume to.
"""
self.manager.extend(self, volume, new_size)
class VolumeManager(base.ManagerWithFind):
"""
@@ -167,11 +184,11 @@ class VolumeManager(base.ManagerWithFind):
qparams = {}
for opt, val in search_opts.iteritems():
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
@@ -326,3 +343,12 @@ class VolumeManager(base.ManagerWithFind):
def force_delete(self, volume):
return self._action('os-force_delete', base.getid(volume))
def reset_state(self, volume, state):
"""Update the provided volume with the provided state."""
return self._action('os-reset_status', volume, {'status': state})
def extend(self, volume, new_size):
return self._action('os-extend',
base.getid(volume),
{'new_size': new_size})

View File

@@ -14,4 +14,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient.v2.client import Client
from cinderclient.v2.client import Client # noqa

View File

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

View File

@@ -1,12 +1,30 @@
# Copyright 2013 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient import client
from cinderclient.v1 import availability_zones
from cinderclient.v2 import limits
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_backups
from cinderclient.v2 import volume_backups_restore
from cinderclient.v1 import volume_transfers
class Client(object):
@@ -43,6 +61,10 @@ class Client(object):
self.quotas = quotas.QuotaSetManager(self)
self.backups = volume_backups.VolumeBackupManager(self)
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
self.availability_zones = \
availability_zones.AvailabilityZoneManager(self)
# Add in any extensions...
if extensions:
@@ -80,3 +102,6 @@ class Client(object):
credentials are wrong.
"""
self.client.authenticate()
def get_volume_api_version_from_endpoint(self):
return self.client.get_volume_api_version_from_endpoint()

View File

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

View File

@@ -20,32 +20,24 @@ class QuotaClassSet(base.Resource):
@property
def id(self):
"""Needed by base.Resource to self-refresh and be indexed"""
"""Needed by base.Resource to self-refresh and be indexed."""
return self.class_name
def update(self, *args, **kwargs):
self.manager.update(self.class_name, *args, **kwargs)
class QuotaClassSetManager(base.ManagerWithFind):
class QuotaClassSetManager(base.Manager):
resource_class = QuotaClassSet
def get(self, class_name):
return self._get("/os-quota-class-sets/%s" % (class_name),
"quota_class_set")
def update(self,
class_name,
volumes=None,
gigabytes=None):
def update(self, class_name, **updates):
body = {'quota_class_set': {'class_name': class_name}}
body = {'quota_class_set': {
'class_name': class_name,
'volumes': volumes,
'gigabytes': gigabytes}}
for key in body['quota_class_set'].keys():
if body['quota_class_set'][key] is None:
body['quota_class_set'].pop(key)
for update in updates.keys():
body['quota_class_set'][update] = updates[update]
self._update('/os-quota-class-sets/%s' % (class_name), body)

View File

@@ -20,14 +20,14 @@ class QuotaSet(base.Resource):
@property
def id(self):
"""Needed by base.Resource to self-refresh and be indexed"""
"""Needed by base.Resource to self-refresh and be indexed."""
return self.tenant_id
def update(self, *args, **kwargs):
self.manager.update(self.tenant_id, *args, **kwargs)
class QuotaSetManager(base.ManagerWithFind):
class QuotaSetManager(base.Manager):
resource_class = QuotaSet
def get(self, tenant_id):
@@ -35,17 +35,11 @@ class QuotaSetManager(base.ManagerWithFind):
tenant_id = tenant_id.tenant_id
return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set")
def update(self, tenant_id, volumes=None, snapshots=None, gigabytes=None):
def update(self, tenant_id, **updates):
body = {'quota_set': {'tenant_id': tenant_id}}
body = {'quota_set': {
'tenant_id': tenant_id,
'volumes': volumes,
'snapshots': snapshots,
'gigabytes': gigabytes}}
for key in body['quota_set'].keys():
if body['quota_set'][key] is None:
body['quota_set'].pop(key)
for update in updates.keys():
body['quota_set'][update] = updates[update]
self._update('/os-quota-sets/%s' % (tenant_id), body)

View File

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

View File

@@ -13,13 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import argparse
import copy
import os
import sys
import time
import six
from cinderclient import exceptions
from cinderclient import utils
from cinderclient.v2 import availability_zones
def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
@@ -35,17 +41,17 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
sys.stdout.write(msg)
sys.stdout.flush()
print
print()
while True:
obj = poll_fn(obj_id)
status = obj.status.lower()
progress = getattr(obj, 'progress', None) or 0
if status in final_ok_states:
print_progress(100)
print "\nFinished"
print("\nFinished")
break
elif status == "error":
print "\nError %(action)s instance" % locals()
print("\nError %(action)s instance" % {'action': action})
break
else:
print_progress(progress)
@@ -67,13 +73,22 @@ def _find_backup(cs, backup):
return utils.find_resource(cs.backups, backup)
def _find_transfer(cs, transfer):
"""Get a transfer by ID."""
return utils.find_resource(cs.transfers, transfer)
def _print_volume_snapshot(snapshot):
utils.print_dict(snapshot._info)
def _print_volume_image(image):
utils.print_dict(image[1]['os-volume_upload_image'])
def _translate_keys(collection, convert):
for item in collection:
keys = item.__dict__.keys()
keys = list(item.__dict__.keys())
for from_key, to_key in convert:
if from_key in keys and to_key not in keys:
setattr(item, to_key, item._info[from_key])
@@ -89,6 +104,11 @@ def _translate_volume_snapshot_keys(collection):
_translate_keys(collection, convert)
def _translate_availability_zone_keys(collection):
convert = [('zoneName', 'name'), ('zoneState', 'status')]
_translate_keys(collection, convert)
def _extract_metadata(args):
metadata = {}
for metadatum in args.metadata[0]:
@@ -161,9 +181,7 @@ def do_show(cs, args):
volume = _find_volume(cs, args.volume)
info.update(volume._info)
if 'links' in info:
info.pop('links')
info.pop('links', None)
utils.print_dict(info)
@@ -223,6 +241,12 @@ def do_show(cs, args):
metavar='<key=value>',
help='Metadata key=value pairs (Optional, Default=None)',
default=None)
@utils.arg('--hint',
metavar='<key=value>',
dest='scheduler_hints',
action='append',
default=[],
help='Scheduler hint like in nova')
@utils.service_type('volume')
def do_create(cs, args):
"""Add a new volume."""
@@ -237,6 +261,21 @@ def do_create(cs, args):
if args.metadata is not None:
volume_metadata = _extract_metadata(args)
#NOTE(N.S.): take this piece from novaclient
hints = {}
if args.scheduler_hints:
for hint in args.scheduler_hints:
key, _sep, value = hint.partition('=')
# NOTE(vish): multiple copies of the same hint will
# result in a list of values
if key in hints:
if isinstance(hints[key], six.string_types):
hints[key] = [hints[key]]
hints[key] += [value]
else:
hints[key] = value
#NOTE(N.S.): end of the taken piece
volume = cs.volumes.create(args.size,
args.snapshot_id,
args.source_volid,
@@ -245,14 +284,14 @@ def do_create(cs, args):
args.volume_type,
availability_zone=args.availability_zone,
imageRef=args.image_id,
metadata=volume_metadata)
metadata=volume_metadata,
scheduler_hints=hints)
info = dict()
volume = cs.volumes.get(info['id'])
volume = cs.volumes.get(volume.id)
info.update(volume._info)
info.pop('links')
info.pop('links', None)
utils.print_dict(info)
@@ -276,6 +315,18 @@ def do_force_delete(cs, args):
volume.force_delete()
@utils.arg('volume', metavar='<volume>', help='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')
def do_reset_state(cs, args):
"""Explicitly update the state of a volume."""
volume = _find_volume(cs, args.volume)
volume.reset_state(args.state)
@utils.arg('volume',
metavar='<volume>',
help='ID of the volume to rename.')
@@ -327,7 +378,7 @@ def do_metadata(cs, args):
if args.action == 'set':
cs.volumes.set_metadata(volume, metadata)
elif args.action == 'unset':
cs.volumes.delete_metadata(volume, metadata.keys())
cs.volumes.delete_metadata(volume, list(metadata.keys()))
@utils.arg('--all-tenants',
@@ -469,6 +520,21 @@ def do_snapshot_rename(cs, args):
_find_volume_snapshot(cs, args.snapshot).update(**kwargs)
@utils.arg('snapshot', metavar='<snapshot>',
help='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')
def do_snapshot_reset_state(cs, args):
"""Explicitly update the state of a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot)
snapshot.reset_state(args.state)
def _print_volume_type_list(vtypes):
utils.print_list(vtypes, ['ID', 'Name'])
@@ -507,7 +573,7 @@ def do_type_create(cs, args):
help="Unique ID of the volume type to delete")
@utils.service_type('volume')
def do_type_delete(cs, args):
"""Delete a specific volume type"""
"""Delete a specific volume type."""
cs.volume_types.delete(args.id)
@@ -533,28 +599,35 @@ def do_type_key(cs, args):
if args.action == 'set':
vtype.set_keys(keypair)
elif args.action == 'unset':
vtype.unset_keys(keypair.keys())
vtype.unset_keys(list(keypair.keys()))
def do_endpoints(cs, args):
"""Discover endpoints that get returned from the authenticate services"""
"""Discover endpoints that get returned from the authenticate services."""
catalog = cs.client.service_catalog.catalog
for e in catalog['access']['serviceCatalog']:
utils.print_dict(e['endpoints'][0], e['name'])
def do_credentials(cs, args):
"""Show user credentials returned from auth"""
"""Show user credentials returned from auth."""
catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials")
utils.print_dict(catalog['access']['token'], "Token")
_quota_resources = ['volumes', 'snapshots', 'gigabytes']
def _quota_show(quotas):
quota_dict = {}
for resource in _quota_resources:
for resource in quotas._info.keys():
good_name = False
for name in _quota_resources:
if resource.startswith(name):
good_name = True
if not good_name:
continue
quota_dict[resource] = getattr(quotas, resource, None)
utils.print_dict(quota_dict)
@@ -564,6 +637,8 @@ def _quota_update(manager, identifier, args):
for resource in _quota_resources:
val = getattr(args, resource, None)
if val is not None:
if args.volume_type:
resource = resource + '_%s' % args.volume_type
updates[resource] = val
if updates:
@@ -605,6 +680,10 @@ def do_quota_defaults(cs, args):
metavar='<gigabytes>',
type=int, default=None,
help='New value for the "gigabytes" quota.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
help='Volume type (Optional, Default=None)')
@utils.service_type('volume')
def do_quota_update(cs, args):
"""Update the quotas for a tenant."""
@@ -637,6 +716,10 @@ def do_quota_class_show(cs, args):
metavar='<gigabytes>',
type=int, default=None,
help='New value for the "gigabytes" quota.')
@utils.arg('--volume-type',
metavar='<volume_type_name>',
default=None,
help='Volume type (Optional, Default=None)')
@utils.service_type('volume')
def do_quota_class_update(cs, args):
"""Update the quotas for a quota class."""
@@ -704,10 +787,10 @@ def _find_volume_type(cs, vtype):
def do_upload_to_image(cs, args):
"""Upload volume to image service as image."""
volume = _find_volume(cs, args.volume_id)
volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format)
_print_volume_image(volume.upload_to_image(args.force,
args.image_name,
args.container_format,
args.disk_format))
@utils.arg('volume', metavar='<volume>',
@@ -749,9 +832,7 @@ def do_backup_show(cs, args):
info = dict()
info.update(backup._info)
if 'links' in info:
info.pop('links')
info.pop('links', None)
utils.print_dict(info)
@@ -783,3 +864,171 @@ def do_backup_restore(cs, args):
"""Restore a backup."""
cs.restores.restore(args.backup,
args.volume_id)
@utils.arg('volume', metavar='<volume>',
help='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')
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,
args.name)
info = dict()
info.update(transfer._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to delete.')
@utils.service_type('volume')
def do_transfer_delete(cs, args):
"""Undo a transfer."""
transfer = _find_transfer(cs, args.transfer)
transfer.delete()
@utils.arg('transfer', metavar='<transfer>',
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')
def do_transfer_accept(cs, args):
"""Accepts a volume transfer."""
transfer = cs.transfers.accept(args.transfer, args.auth_key)
info = dict()
info.update(transfer._info)
info.pop('links', None)
utils.print_dict(info)
@utils.service_type('volume')
def do_transfer_list(cs, args):
"""List all the transfers."""
transfers = cs.transfers.list()
columns = ['ID', 'Volume ID', 'Name']
utils.print_list(transfers, columns)
@utils.arg('transfer', metavar='<transfer>',
help='ID of the transfer to accept.')
@utils.service_type('volume')
def do_transfer_show(cs, args):
"""Show details about a transfer."""
transfer = _find_transfer(cs, args.transfer)
info = dict()
info.update(transfer._info)
info.pop('links', None)
utils.print_dict(info)
@utils.arg('volume', metavar='<volume>', help='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')
def do_extend(cs, args):
"""Attempt to extend the size of an existing volume."""
volume = _find_volume(cs, args.volume)
cs.volumes.extend(volume, args.new_size)
@utils.arg('--host', metavar='<hostname>', default=None,
help='Name of host.')
@utils.arg('--binary', metavar='<binary>', default=None,
help='Service binary.')
@utils.service_type('volume')
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)
columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"]
utils.print_list(result, columns)
@utils.arg('host', metavar='<hostname>', help='Name of host.')
@utils.arg('binary', metavar='<binary>', help='Service binary.')
@utils.service_type('volume')
def do_service_enable(cs, args):
"""Enable the service."""
cs.services.enable(args.host, args.binary)
@utils.arg('host', metavar='<hostname>', help='Name of host.')
@utils.arg('binary', metavar='<binary>', help='Service binary.')
@utils.service_type('volume')
def do_service_disable(cs, args):
"""Disable the service."""
cs.services.disable(args.host, args.binary)
def _treeizeAvailabilityZone(zone):
"""Build a tree view for availability zones."""
AvailabilityZone = availability_zones.AvailabilityZone
az = AvailabilityZone(zone.manager,
copy.deepcopy(zone._info), zone._loaded)
result = []
# Zone tree view item
az.zoneName = zone.zoneName
az.zoneState = ('available'
if zone.zoneState['available'] else 'not available')
az._info['zoneName'] = az.zoneName
az._info['zoneState'] = az.zoneState
result.append(az)
if getattr(zone, "hosts", None) and zone.hosts is not None:
for (host, services) in zone.hosts.items():
# Host tree view item
az = AvailabilityZone(zone.manager,
copy.deepcopy(zone._info), zone._loaded)
az.zoneName = '|- %s' % host
az.zoneState = ''
az._info['zoneName'] = az.zoneName
az._info['zoneState'] = az.zoneState
result.append(az)
for (svc, state) in services.items():
# Service tree view item
az = AvailabilityZone(zone.manager,
copy.deepcopy(zone._info), zone._loaded)
az.zoneName = '| |- %s' % svc
az.zoneState = '%s %s %s' % (
'enabled' if state['active'] else 'disabled',
':-)' if state['available'] else 'XXX',
state['updated_at'])
az._info['zoneName'] = az.zoneName
az._info['zoneState'] = az.zoneState
result.append(az)
return result
@utils.service_type('volume')
def do_availability_zone_list(cs, _args):
"""List all the availability zones."""
try:
availability_zones = cs.availability_zones.list()
except exceptions.Forbidden as e: # policy doesn't allow probably
try:
availability_zones = cs.availability_zones.list(detailed=False)
except Exception:
raise e
result = []
for zone in availability_zones:
result += _treeizeAvailabilityZone(zone)
_translate_availability_zone_keys(result)
utils.print_list(result, ['Name', 'Status'])

View File

@@ -27,7 +27,7 @@ class VolumeBackupsRestore(base.Resource):
return "<VolumeBackupsRestore: %s>" % self.id
class VolumeBackupRestoreManager(base.ManagerWithFind):
class VolumeBackupRestoreManager(base.Manager):
"""Manage :class:`VolumeBackupsRestore` resources."""
resource_class = VolumeBackupsRestore

View File

@@ -15,7 +15,11 @@
"""Volume snapshot interface (1.1 extension)."""
import urllib
import six
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
@@ -41,6 +45,10 @@ class Snapshot(base.Resource):
def project_id(self):
return self._info.get('os-extended-snapshot-attributes:project_id')
def reset_state(self, state):
"""Update the snapshot with the provided state."""
self.manager.reset_state(self, state)
class SnapshotManager(base.ManagerWithFind):
"""Manage :class:`Snapshot` resources."""
@@ -83,11 +91,11 @@ class SnapshotManager(base.ManagerWithFind):
qparams = {}
for opt, val in search_opts.iteritems():
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
@@ -114,3 +122,14 @@ class SnapshotManager(base.ManagerWithFind):
body = {"snapshot": kwargs}
self._update("/snapshots/%s" % base.getid(snapshot), body)
def reset_state(self, snapshot, state):
"""Update the specified snapshot with the provided state."""
return self._action('os-reset_status', snapshot, {'status': state})
def _action(self, action, snapshot, info=None, **kwargs):
"""Perform a snapshot action."""
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
url = '/snapshots/%s/action' % base.getid(snapshot)
return self.api.client.post(url, body=body)

View File

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

View File

@@ -15,7 +15,11 @@
"""Volume interface (v2 extension)."""
import urllib
import six
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from cinderclient import base
@@ -86,8 +90,8 @@ class Volume(base.Resource):
def upload_to_image(self, force, image_name, container_format,
disk_format):
"""Upload a volume to image service as an image."""
self.manager.upload_to_image(self, force, image_name, container_format,
disk_format)
return self.manager.upload_to_image(self, force, image_name,
container_format, disk_format)
def force_delete(self):
"""Delete the specified volume ignoring its current state.
@@ -96,6 +100,18 @@ class Volume(base.Resource):
"""
self.manager.force_delete(self)
def reset_state(self, state):
"""Update the volume with the provided state."""
self.manager.reset_state(self, state)
def extend(self, volume, new_size):
"""Extend the size of the specified volume.
:param volume: The UUID of the volume to extend
:param new_size: The desired size to extend volume to.
"""
self.manager.extend(self, volume, new_size)
class VolumeManager(base.ManagerWithFind):
"""Manage :class:`Volume` resources."""
@@ -105,7 +121,7 @@ class VolumeManager(base.ManagerWithFind):
name=None, description=None,
volume_type=None, user_id=None,
project_id=None, availability_zone=None,
metadata=None, imageRef=None):
metadata=None, imageRef=None, scheduler_hints=None):
"""Create a volume.
:param size: Size of volume in GB
@@ -120,7 +136,9 @@ class VolumeManager(base.ManagerWithFind):
:param metadata: Optional metadata to set on volume creation
:param imageRef: reference to an image stored in glance
:param source_volid: ID of source volume to clone from
"""
:param scheduler_hints: (optional extension) arbitrary key-value pairs
specified by the client to help boot an instance
"""
if metadata is None:
volume_metadata = {}
@@ -140,6 +158,7 @@ class VolumeManager(base.ManagerWithFind):
'metadata': volume_metadata,
'imageRef': imageRef,
'source_volid': source_volid,
'scheduler_hints': scheduler_hints,
}}
return self._create('/volumes', body, 'volume')
@@ -161,11 +180,11 @@ class VolumeManager(base.ManagerWithFind):
qparams = {}
for opt, val in search_opts.iteritems():
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
query_string = "?%s" % urlencode(qparams) if qparams else ""
detail = ""
if detailed:
@@ -306,3 +325,12 @@ class VolumeManager(base.ManagerWithFind):
def force_delete(self, volume):
return self._action('os-force_delete', base.getid(volume))
def reset_state(self, volume, state):
"""Update the provided volume with the provided state."""
return self._action('os-reset_status', volume, {'status': state})
def extend(self, volume, new_size):
return self._action('os-extend',
base.getid(volume),
{'new_size': new_size})

View File

@@ -13,6 +13,7 @@
import os
import sys
import pbr.version
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -42,17 +43,17 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'python-cinderclient'
copyright = u'Rackspace, based on work by Jacob Kaplan-Moss'
project = 'python-cinderclient'
copyright = 'OpenStack Contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
version_info = pbr.version.VersionInfo('python-cinderclient')
# The short X.Y version.
version = '2.6'
version = version_info.version_string()
# The full version, including alpha/beta/rc tags.
release = '2.6.10'
release = version_info.release_string()
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -93,6 +94,10 @@ pygments_style = 'sphinx'
#modindex_common_prefix = []
man_pages = [
('man/cinder', 'cinder', u'Client for OpenStack Block Storage API',
[u'OpenStack Contributors'], 1),
]
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
@@ -179,8 +184,8 @@ htmlhelp_basename = 'python-cinderclientdoc'
# (source start file, target name, title, author, documentclass [howto/manual])
# .
latex_documents = [
('index', 'python-cinderclient.tex', u'python-cinderclient Documentation',
u'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'),
('index', 'python-cinderclient.tex', 'python-cinderclient Documentation',
'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of

View File

@@ -24,9 +24,30 @@ In order to use the CLI, you must provide your OpenStack username, password, ten
Once you've configured your authentication parameters, you can run ``cinder help`` to see a complete listing of available commands.
See also :doc:`/man/cinder`.
Release Notes
=============
1.0.5
-----
* Add CLI man page
* Add Availability Zone list command
* Add support for scheduler-hints
* Add support to extend volumes
* Add support to reset state on volumes and snapshots
* Add snapshot support for quota class
.. _1190853: http://bugs.launchpad.net/python-cinderclient/+bug/1190853
.. _1190731: http://bugs.launchpad.net/python-cinderclient/+bug/1190731
.. _1169455: http://bugs.launchpad.net/python-cinderclient/+bug/1169455
.. _1188452: http://bugs.launchpad.net/python-cinderclient/+bug/1188452
.. _1180393: http://bugs.launchpad.net/python-cinderclient/+bug/1180393
.. _1182678: http://bugs.launchpad.net/python-cinderclient/+bug/1182678
.. _1179008: http://bugs.launchpad.net/python-cinderclient/+bug/1179008
.. _1180059: http://bugs.launchpad.net/python-cinderclient/+bug/1180059
.. _1170565: http://bugs.launchpad.net/python-cinderclient/+bug/1170565
1.0.4
-----
* Added suport for backup-service commands

58
doc/source/man/cinder.rst Normal file
View File

@@ -0,0 +1,58 @@
==============================
:program:`cinder` CLI man page
==============================
.. program:: cinder
.. highlight:: bash
SYNOPSIS
========
:program:`cinder` [options] <command> [command-options]
:program:`cinder help`
:program:`cinder help` <command>
DESCRIPTION
===========
The :program:`cinder` command line utility interacts with OpenStack Block
Storage Service (Cinder).
In order to use the CLI, you must provide your OpenStack username, password,
project (historically called tenant), and auth endpoint. You can use
configuration options :option:`--os-username`, :option:`--os-password`,
:option:`--os-tenant-name` or :option:`--os-tenant-id`, and
:option:`--os-auth-url` or set corresponding environment variables::
export OS_USERNAME=user
export OS_PASSWORD=pass
export OS_TENANT_NAME=myproject
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
You can select an API version to use by :option:`--os-volume-api-version`
option or by setting corresponding environment variable::
export OS_VOLUME_API_VERSION=2
OPTIONS
=======
To get a list of available commands and options run::
cinder help
To get usage and options of a command::
cinder help <command>
BUGS
====
Cinder client is hosted in Launchpad so you can view current bugs at
https://bugs.launchpad.net/python-cinderclient/.

View File

@@ -1,7 +1,9 @@
[DEFAULT]
# The list of modules to copy from openstack-common
modules=setup,version,strutils
module=apiclient
module=strutils
module=install_venv_common
# The base module to hold the copy of openstack.common
base=cinderclient

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
pbr>=0.5.16,<0.6
argparse
PrettyTable>=0.6,<0.8
requests>=1.1,<1.2.3
simplejson>=2.0.9
six

View File

@@ -4,17 +4,27 @@ set -eu
function usage {
echo "Usage: $0 [OPTION]..."
echo "Run python-cinderclient test suite"
echo "Run cinderclient's test suite(s)"
echo ""
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -p, --pep8 Just run pep8"
echo " -P, --no-pep8 Don't run pep8"
echo " -c, --coverage Generate coverage report"
echo " -h, --help Print this usage message"
echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)."
echo " -n, --no-recreate-db Don't recreate the test database."
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -u, --update Update the virtual environment with any newer package versions"
echo " -p, --pep8 Just run PEP8 and HACKING compliance check"
echo " -P, --no-pep8 Don't run static code checks"
echo " -c, --coverage Generate coverage report"
echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger."
echo " -h, --help Print this usage message"
echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
echo " --virtual-env-path <path> Location of the virtualenv directory"
echo " Default: \$(pwd)"
echo " --virtual-env-name <name> Name of the virtualenv directory"
echo " Default: .venv"
echo " --tools-path <dir> Location of the tools directory"
echo " Default: \$(pwd)"
echo ""
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
@@ -22,23 +32,44 @@ function usage {
exit
}
function process_option {
case "$1" in
-h|--help) usage;;
-V|--virtual-env) always_venv=1; never_venv=0;;
-N|--no-virtual-env) always_venv=0; never_venv=1;;
-s|--no-site-packages) no_site_packages=1;;
-f|--force) force=1;;
-p|--pep8) just_pep8=1;;
-P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;;
-d|--debug) debug=1;;
-*) testropts="$testropts $1";;
*) testrargs="$testrargs $1"
esac
function process_options {
i=1
while [ $i -le $# ]; do
case "${!i}" in
-h|--help) usage;;
-V|--virtual-env) always_venv=1; never_venv=0;;
-N|--no-virtual-env) always_venv=0; never_venv=1;;
-s|--no-site-packages) no_site_packages=1;;
-r|--recreate-db) recreate_db=1;;
-n|--no-recreate-db) recreate_db=0;;
-f|--force) force=1;;
-u|--update) update=1;;
-p|--pep8) just_pep8=1;;
-P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;;
-d|--debug) debug=1;;
--virtual-env-path)
(( i++ ))
venv_path=${!i}
;;
--virtual-env-name)
(( i++ ))
venv_dir=${!i}
;;
--tools-path)
(( i++ ))
tools_path=${!i}
;;
-*) testropts="$testropts ${!i}";;
*) testrargs="$testrargs ${!i}"
esac
(( i++ ))
done
}
venv=.venv
tool_path=${tools_path:-$(pwd)}
venv_path=${venv_path:-$(pwd)}
venv_dir=${venv_name:-.venv}
with_venv=tools/with_venv.sh
always_venv=0
never_venv=0
@@ -52,14 +83,20 @@ just_pep8=0
no_pep8=0
coverage=0
debug=0
recreate_db=1
update=0
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
for arg in "$@"; do
process_option $arg
done
process_options $@
# Make our paths available to other scripts we call
export venv_path
export venv_dir
export venv_name
export tools_dir
export venv=${venv_path}/${venv_dir}
if [ $no_site_packages -eq 1 ]; then
installvenvopts="--no-site-packages"
@@ -79,7 +116,7 @@ function run_tests {
if [ "$testropts" = "" ] && [ "$testrargs" = "" ]; then
# Default to running all tests if specific test is not
# provided.
testrargs="discover ./tests"
testrargs="discover ./cinderclient/tests"
fi
${wrapper} python -m testtools.run $testropts $testrargs
@@ -90,22 +127,40 @@ function run_tests {
fi
if [ $coverage -eq 1 ]; then
# Do not test test_coverage_ext when gathering coverage.
if [ "x$testrargs" = "x" ]; then
testrargs="^(?!.*test_coverage_ext).*$"
fi
export PYTHON="${wrapper} coverage run --source cinderclient --parallel-mode"
TESTRTESTS="$TESTRTESTS --coverage"
else
TESTRTESTS="$TESTRTESTS"
fi
# Just run the test suites in current environment
set +e
TESTRTESTS="$TESTRTESTS $testrargs"
testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'`
TESTRTESTS="$TESTRTESTS --testr-args='--subunit $testropts $testrargs'"
if [ setup.cfg -nt cinderclient.egg-info/entry_points.txt ]
then
${wrapper} python setup.py egg_info
fi
echo "Running \`${wrapper} $TESTRTESTS\`"
${wrapper} $TESTRTESTS
if ${wrapper} which subunit-2to1 2>&1 > /dev/null
then
# subunit-2to1 is present, testr subunit stream should be in version 2
# format. Convert to version one before colorizing.
bash -c "${wrapper} $TESTRTESTS | ${wrapper} subunit-2to1 | ${wrapper} tools/colorizer.py"
else
bash -c "${wrapper} $TESTRTESTS | ${wrapper} tools/colorizer.py"
fi
RESULT=$?
set -e
copy_subunit_log
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
# Don't compute coverage for common code, which is tested elsewhere
${wrapper} coverage combine
${wrapper} coverage html --include='cinderclient/*' --omit='cinderclient/openstack/common/*' -d covhtml -i
fi
return $RESULT
}
@@ -117,29 +172,12 @@ function copy_subunit_log {
}
function run_pep8 {
echo "Running pep8 ..."
srcfiles="cinderclient tests"
# Just run PEP8 in current environment
#
# NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the
# following reasons:
#
# 1. It's needed to preserve traceback information when re-raising
# exceptions; this is needed b/c Eventlet will clear exceptions when
# switching contexts.
#
# 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this
# in Python 2 (in Python 3 `with_traceback` could be used).
#
# 3. Can find no corroborating evidence that this is deprecated in Python 2
# other than what the PEP8 tool claims. It is deprecated in Python 3, so,
# perhaps the mistake was thinking that the deprecation applied to Python 2
# as well.
pep8_opts="--ignore=E202,W602 --repeat"
${wrapper} pep8 ${pep8_opts} ${srcfiles}
echo "Running flake8 ..."
bash -c "${wrapper} flake8"
}
TESTRTESTS="testr run --parallel $testropts"
TESTRTESTS="python setup.py testr"
if [ $never_venv -eq 0 ]
then
@@ -148,6 +186,10 @@ then
echo "Cleaning virtualenv..."
rm -rf ${venv}
fi
if [ $update -eq 1 ]; then
echo "Updating virtualenv..."
python tools/install_venv.py $installvenvopts
fi
if [ -e ${venv} ]; then
wrapper="${with_venv}"
else
@@ -177,19 +219,19 @@ if [ $just_pep8 -eq 1 ]; then
exit
fi
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,
# not when we're running tests individually.
# not when we're running tests individually. To handle this, we need to
# distinguish between options (testropts), which begin with a '-', and
# arguments (testrargs).
if [ -z "$testrargs" ]; then
if [ $no_pep8 -eq 0 ]; then
run_pep8
fi
fi
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
${wrapper} coverage combine
${wrapper} coverage html --include='cinderclient/*' --omit='cinderclient/openstack/common/*' -d covhtml -i
fi

View File

@@ -1,3 +1,36 @@
[metadata]
name = python-cinderclient
summary = OpenStack Block Storage API Client Library
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Development Status :: 5 - Production/Stable
Environment :: Console
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
[global]
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
cinderclient
[entry_points]
console_scripts =
cinder = cinderclient.shell:main
[build_sphinx]
all_files = 1
source-dir = doc/source

View File

@@ -1,60 +1,22 @@
# Copyright 2011 OpenStack, LLC
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# 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.
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
from cinderclient.openstack.common import setup
requires = setup.parse_requirements()
depend_links = setup.parse_dependency_links()
tests_require = setup.parse_requirements(['tools/test-requires'])
project = 'python-cinderclient'
def read_file(file_name):
return open(os.path.join(os.path.dirname(__file__), file_name)).read()
setuptools.setup(
name=project,
version=setup.get_version(project),
author="OpenStack Contributors",
author_email="openstack-dev@lists.openstack.org",
description="Client library for OpenStack Cinder API.",
long_description=read_file("README.rst"),
license="Apache License, Version 2.0",
url="https://github.com/openstack/python-cinderclient",
packages=setuptools.find_packages(exclude=['tests', 'tests.*']),
cmdclass=setup.get_cmdclass(),
install_requires=requires,
tests_require=tests_require,
setup_requires=['setuptools-git>=0.4'],
include_package_data=True,
dependency_links=depend_links,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Environment :: OpenStack",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python"
],
entry_points={
"console_scripts": ["cinder = cinderclient.shell:main"]
}
)
setup_requires=['pbr>=0.5.20'],
pbr=True)

13
test-requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
# Install bounded pep8/pyflakes first, then let flake8 install
pep8==1.4.5
pyflakes==0.7.2
flake8==2.0
hacking>=0.5.6,<0.7
coverage>=3.6
discover
fixtures>=0.3.12
mock>=0.8.0
python-subunit
sphinx>=1.1.2
testtools>=0.9.32
testrepository>=0.0.17

View File

@@ -1,21 +0,0 @@
from cinderclient import extension
from cinderclient.v1.contrib import list_extensions
from tests import utils
from tests.v1 import fakes
extensions = [
extension.Extension(list_extensions.__name__.split(".")[-1],
list_extensions),
]
cs = fakes.FakeClient(extensions=extensions)
class ListExtensionsTests(utils.TestCase):
def test_list_extensions(self):
all_exts = cs.list_extensions.show_all()
cs.assert_called('GET', '/extensions')
self.assertTrue(len(all_exts) > 0)
for r in all_exts:
self.assertTrue(len(r.summary) > 0)

336
tools/colorizer.py Executable file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013, Nebula, Inc.
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
#
# Colorizer Code is borrowed from Twisted:
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Display a subunit stream through a colorized unittest test runner."""
import heapq
import subunit
import sys
import unittest
import six
import testtools
class _AnsiColorizer(object):
"""
A colorizer is an object that loosely wraps around a stream, allowing
callers to write text to the stream in a particular color.
Colorizer classes must implement C{supported()} and C{write(text, color)}.
"""
_colors = dict(black=30, red=31, green=32, yellow=33,
blue=34, magenta=35, cyan=36, white=37)
def __init__(self, stream):
self.stream = stream
def supported(cls, stream=sys.stdout):
"""
A class method that returns True if the current platform supports
coloring terminal output using this method. Returns False otherwise.
"""
if not stream.isatty():
return False # auto color only on TTYs
try:
import curses
except ImportError:
return False
else:
try:
try:
return curses.tigetnum("colors") > 2
except curses.error:
curses.setupterm()
return curses.tigetnum("colors") > 2
except Exception:
# guess false in case of error
return False
supported = classmethod(supported)
def write(self, text, color):
"""
Write the given text to the stream in the given color.
@param text: Text to be written to the stream.
@param color: A string label for a color. e.g. 'red', 'white'.
"""
color = self._colors[color]
self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
class _Win32Colorizer(object):
"""
See _AnsiColorizer docstring.
"""
def __init__(self, stream):
import win32console
red, green, blue, bold = (win32console.FOREGROUND_RED,
win32console.FOREGROUND_GREEN,
win32console.FOREGROUND_BLUE,
win32console.FOREGROUND_INTENSITY)
self.stream = stream
self.screenBuffer = win32console.GetStdHandle(
win32console.STD_OUT_HANDLE)
self._colors = {
'normal': red | green | blue,
'red': red | bold,
'green': green | bold,
'blue': blue | bold,
'yellow': red | green | bold,
'magenta': red | blue | bold,
'cyan': green | blue | bold,
'white': red | green | blue | bold
}
def supported(cls, stream=sys.stdout):
try:
import win32console
screenBuffer = win32console.GetStdHandle(
win32console.STD_OUT_HANDLE)
except ImportError:
return False
import pywintypes
try:
screenBuffer.SetConsoleTextAttribute(
win32console.FOREGROUND_RED |
win32console.FOREGROUND_GREEN |
win32console.FOREGROUND_BLUE)
except pywintypes.error:
return False
else:
return True
supported = classmethod(supported)
def write(self, text, color):
color = self._colors[color]
self.screenBuffer.SetConsoleTextAttribute(color)
self.stream.write(text)
self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
class _NullColorizer(object):
"""
See _AnsiColorizer docstring.
"""
def __init__(self, stream):
self.stream = stream
def supported(cls, stream=sys.stdout):
return True
supported = classmethod(supported)
def write(self, text, color):
self.stream.write(text)
def get_elapsed_time_color(elapsed_time):
if elapsed_time > 1.0:
return 'red'
elif elapsed_time > 0.25:
return 'yellow'
else:
return 'green'
class NovaTestResult(testtools.TestResult):
def __init__(self, stream, descriptions, verbosity):
super(NovaTestResult, self).__init__()
self.stream = stream
self.showAll = verbosity > 1
self.num_slow_tests = 10
self.slow_tests = [] # this is a fixed-sized heap
self.colorizer = None
# NOTE(vish): reset stdout for the terminal check
stdout = sys.stdout
sys.stdout = sys.__stdout__
for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
if colorizer.supported():
self.colorizer = colorizer(self.stream)
break
sys.stdout = stdout
self.start_time = None
self.last_time = {}
self.results = {}
self.last_written = None
def _writeElapsedTime(self, elapsed):
color = get_elapsed_time_color(elapsed)
self.colorizer.write(" %.2f" % elapsed, color)
def _addResult(self, test, *args):
try:
name = test.id()
except AttributeError:
name = 'Unknown.unknown'
test_class, test_name = name.rsplit('.', 1)
elapsed = (self._now() - self.start_time).total_seconds()
item = (elapsed, test_class, test_name)
if len(self.slow_tests) >= self.num_slow_tests:
heapq.heappushpop(self.slow_tests, item)
else:
heapq.heappush(self.slow_tests, item)
self.results.setdefault(test_class, [])
self.results[test_class].append((test_name, elapsed) + args)
self.last_time[test_class] = self._now()
self.writeTests()
def _writeResult(self, test_name, elapsed, long_result, color,
short_result, success):
if self.showAll:
self.stream.write(' %s' % str(test_name).ljust(66))
self.colorizer.write(long_result, color)
if success:
self._writeElapsedTime(elapsed)
self.stream.writeln()
else:
self.colorizer.write(short_result, color)
def addSuccess(self, test):
super(NovaTestResult, self).addSuccess(test)
self._addResult(test, 'OK', 'green', '.', True)
def addFailure(self, test, err):
if test.id() == 'process-returncode':
return
super(NovaTestResult, self).addFailure(test, err)
self._addResult(test, 'FAIL', 'red', 'F', False)
def addError(self, test, err):
super(NovaTestResult, self).addFailure(test, err)
self._addResult(test, 'ERROR', 'red', 'E', False)
def addSkip(self, test, reason=None, details=None):
super(NovaTestResult, self).addSkip(test, reason, details)
self._addResult(test, 'SKIP', 'blue', 'S', True)
def startTest(self, test):
self.start_time = self._now()
super(NovaTestResult, self).startTest(test)
def writeTestCase(self, cls):
if not self.results.get(cls):
return
if cls != self.last_written:
self.colorizer.write(cls, 'white')
self.stream.writeln()
for result in self.results[cls]:
self._writeResult(*result)
del self.results[cls]
self.stream.flush()
self.last_written = cls
def writeTests(self):
time = self.last_time.get(self.last_written, self._now())
if not self.last_written or (self._now() - time).total_seconds() > 2.0:
diff = 3.0
while diff > 2.0:
classes =list(self.results.keys())
oldest = min(classes, key=lambda x: self.last_time[x])
diff = (self._now() - self.last_time[oldest]).total_seconds()
self.writeTestCase(oldest)
else:
self.writeTestCase(self.last_written)
def done(self):
self.stopTestRun()
def stopTestRun(self):
for cls in list(six.iterkeys(self.results)):
self.writeTestCase(cls)
self.stream.writeln()
self.writeSlowTests()
def writeSlowTests(self):
# Pare out 'fast' tests
slow_tests = [item for item in self.slow_tests
if get_elapsed_time_color(item[0]) != 'green']
if slow_tests:
slow_total_time = sum(item[0] for item in slow_tests)
slow = ("Slowest %i tests took %.2f secs:"
% (len(slow_tests), slow_total_time))
self.colorizer.write(slow, 'yellow')
self.stream.writeln()
last_cls = None
# sort by name
for elapsed, cls, name in sorted(slow_tests,
key=lambda x: x[1] + x[2]):
if cls != last_cls:
self.colorizer.write(cls, 'white')
self.stream.writeln()
last_cls = cls
self.stream.write(' %s' % str(name).ljust(68))
self._writeElapsedTime(elapsed)
self.stream.writeln()
def printErrors(self):
if self.showAll:
self.stream.writeln()
self.printErrorList('ERROR', self.errors)
self.printErrorList('FAIL', self.failures)
def printErrorList(self, flavor, errors):
for test, err in errors:
self.colorizer.write("=" * 70, 'red')
self.stream.writeln()
self.colorizer.write(flavor, 'red')
self.stream.writeln(": %s" % test.id())
self.colorizer.write("-" * 70, 'red')
self.stream.writeln()
self.stream.writeln("%s" % err)
test = subunit.ProtocolTestCase(sys.stdin, passthrough=None)
if sys.version_info[0:2] <= (2, 6):
runner = unittest.TextTestRunner(verbosity=2)
else:
runner = unittest.TextTestRunner(verbosity=2, resultclass=NovaTestResult)
if runner.run(test).wasSuccessful():
exit_code = 0
else:
exit_code = 1
sys.exit(exit_code)

View File

@@ -4,242 +4,74 @@
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2010 OpenStack, LLC
# Copyright 2010 OpenStack Foundation
# Copyright 2013 IBM Corp.
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
# 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
# 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.
# 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.
"""
Installation script for Nova's development virtualenv
"""
import optparse
import ConfigParser
import os
import subprocess
import sys
import platform
import install_venv_common as install_venv # flake8: noqa
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
VENV = os.path.join(ROOT, '.venv')
PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires')
PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
def die(message, *args):
print >> sys.stderr, message % args
sys.exit(1)
def check_python_version():
if sys.version_info < (2, 6):
die("Need Python Version >= 2.6")
def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
"""
Runs a command in an out-of-process shell, returning the
output of that command. Working directory is ROOT.
"""
if redirect_output:
stdout = subprocess.PIPE
else:
stdout = None
proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
output = proc.communicate()[0]
if check_exit_code and proc.returncode != 0:
die('Command "%s" failed.\n%s', ' '.join(cmd), output)
return (output, proc.returncode)
def run_command(cmd, redirect_output=True, check_exit_code=True):
return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
class Distro(object):
def check_cmd(self, cmd):
return bool(run_command(['which', cmd], check_exit_code=False).strip())
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if self.check_cmd('easy_install'):
print 'Installing virtualenv via easy_install...',
if run_command(['easy_install', 'virtualenv']):
print 'Succeeded'
return
else:
print 'Failed'
die('ERROR: virtualenv not found.\n\nDevelopment'
' requires virtualenv, please install it using your'
' favorite package management tool')
def post_process(self):
"""Any distribution-specific post-processing gets done here.
In particular, this is useful for applying patches to code inside
the venv."""
pass
class Debian(Distro):
"""This covers all Debian-based distributions."""
def check_pkg(self, pkg):
return run_command_with_code(['dpkg', '-l', pkg],
check_exit_code=False)[1] == 0
def apt_install(self, pkg, **kwargs):
run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs)
def apply_patch(self, originalfile, patchfile):
run_command(['patch', originalfile, patchfile])
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if not self.check_pkg('python-virtualenv'):
self.apt_install('python-virtualenv', check_exit_code=False)
super(Debian, self).install_virtualenv()
class Fedora(Distro):
"""This covers all Fedora-based distributions.
Includes: Fedora, RHEL, CentOS, Scientific Linux"""
def check_pkg(self, pkg):
return run_command_with_code(['rpm', '-q', pkg],
check_exit_code=False)[1] == 0
def yum_install(self, pkg, **kwargs):
run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
def apply_patch(self, originalfile, patchfile):
run_command(['patch', originalfile, patchfile])
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if not self.check_pkg('python-virtualenv'):
self.yum_install('python-virtualenv', check_exit_code=False)
super(Fedora, self).install_virtualenv()
def get_distro():
if os.path.exists('/etc/fedora-release') or \
os.path.exists('/etc/redhat-release'):
return Fedora()
elif os.path.exists('/etc/debian_version'):
return Debian()
else:
return Distro()
def check_dependencies():
get_distro().install_virtualenv()
def create_virtualenv(venv=VENV, no_site_packages=True):
"""Creates the virtual environment and installs PIP only into the
virtual environment
"""
print 'Creating venv...',
if no_site_packages:
run_command(['virtualenv', '-q', '--no-site-packages', VENV])
else:
run_command(['virtualenv', '-q', VENV])
print 'done.'
print 'Installing pip in virtualenv...',
if not run_command(['tools/with_venv.sh', 'easy_install',
'pip>1.0']).strip():
die("Failed to install pip.")
print 'done.'
def pip_install(*args):
run_command(['tools/with_venv.sh',
'pip', 'install', '--upgrade'] + list(args),
redirect_output=False)
def install_dependencies(venv=VENV):
print 'Installing dependencies with pip (this can take a while)...'
# First things first, make sure our venv has the latest pip and distribute.
pip_install('pip')
pip_install('distribute')
pip_install('-r', PIP_REQUIRES)
pip_install('-r', TEST_REQUIRES)
# Tell the virtual env how to "import cinder"
pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages",
"cinderclient.pth")
f = open(pthfile, 'w')
f.write("%s\n" % ROOT)
def post_process():
get_distro().post_process()
def print_help():
def print_help(project, venv, root):
help = """
python-cinderclient development environment setup is complete.
%(project)s development environment setup is complete.
python-cinderclient development uses virtualenv to track and manage Python
%(project)s development uses virtualenv to track and manage Python
dependencies while in development and testing.
To activate the python-cinderclient virtualenv for the extent of your
current shell session you can run:
To activate the %(project)s virtualenv for the extent of your current
shell session you can run:
$ source .venv/bin/activate
$ source %(venv)s/bin/activate
Or, if you prefer, you can run commands in the virtualenv on a case by case
basis by running:
Or, if you prefer, you can run commands in the virtualenv on a case by
case basis by running:
$ tools/with_venv.sh <your command>
Also, make test will automatically use the virtualenv.
$ %(root)s/tools/with_venv.sh <your command>
"""
print help
def parse_args():
"""Parse command-line arguments"""
parser = optparse.OptionParser()
parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
default=False, action="store_true",
help="Do not inherit packages from global Python install")
return parser.parse_args()
print help % dict(project=project, venv=venv, root=root)
def main(argv):
(options, args) = parse_args()
check_python_version()
check_dependencies()
create_virtualenv(no_site_packages=options.no_site_packages)
install_dependencies()
post_process()
print_help()
root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if os.environ.get('tools_path'):
root = os.environ['tools_path']
venv = os.path.join(root, '.venv')
if os.environ.get('venv'):
venv = os.environ['venv']
pip_requires = os.path.join(root, 'requirements.txt')
test_requires = os.path.join(root, 'test-requirements.txt')
py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
setup_cfg = ConfigParser.ConfigParser()
setup_cfg.read('setup.cfg')
project = setup_cfg.get('metadata', 'name')
install = install_venv.InstallVenv(
root, venv, pip_requires, test_requires, py_version, project)
options = install.parse_args(argv)
install.check_python_version()
install.check_dependencies()
install.create_virtualenv(no_site_packages=options.no_site_packages)
install.install_dependencies()
install.post_process()
print_help(project, venv, root)
if __name__ == '__main__':
main(sys.argv)

View File

@@ -0,0 +1,212 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
#
# 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.
"""Provides methods needed by installation script for OpenStack development
virtual environments.
Since this script is used to bootstrap a virtualenv from the system's Python
environment, it should be kept strictly compatible with Python 2.6.
Synced in from openstack-common
"""
from __future__ import print_function
import optparse
import os
import subprocess
import sys
class InstallVenv(object):
def __init__(self, root, venv, requirements,
test_requirements, py_version,
project):
self.root = root
self.venv = venv
self.requirements = requirements
self.test_requirements = test_requirements
self.py_version = py_version
self.project = project
def die(self, message, *args):
print(message % args, file=sys.stderr)
sys.exit(1)
def check_python_version(self):
if sys.version_info < (2, 6):
self.die("Need Python Version >= 2.6")
def run_command_with_code(self, cmd, redirect_output=True,
check_exit_code=True):
"""Runs a command in an out-of-process shell.
Returns the output of that command. Working directory is self.root.
"""
if redirect_output:
stdout = subprocess.PIPE
else:
stdout = None
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
output = proc.communicate()[0]
if check_exit_code and proc.returncode != 0:
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
return (output, proc.returncode)
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
return self.run_command_with_code(cmd, redirect_output,
check_exit_code)[0]
def get_distro(self):
if (os.path.exists('/etc/fedora-release') or
os.path.exists('/etc/redhat-release')):
return Fedora(
self.root, self.venv, self.requirements,
self.test_requirements, self.py_version, self.project)
else:
return Distro(
self.root, self.venv, self.requirements,
self.test_requirements, self.py_version, self.project)
def check_dependencies(self):
self.get_distro().install_virtualenv()
def create_virtualenv(self, no_site_packages=True):
"""Creates the virtual environment and installs PIP.
Creates the virtual environment and installs PIP only into the
virtual environment.
"""
if not os.path.isdir(self.venv):
print('Creating venv...', end=' ')
if no_site_packages:
self.run_command(['virtualenv', '-q', '--no-site-packages',
self.venv])
else:
self.run_command(['virtualenv', '-q', self.venv])
print('done.')
else:
print("venv already exists...")
pass
def pip_install(self, *args):
self.run_command(['tools/with_venv.sh',
'pip', 'install', '--upgrade'] + list(args),
redirect_output=False)
def install_dependencies(self):
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')
self.pip_install('setuptools')
self.pip_install('-r', self.requirements)
self.pip_install('-r', self.test_requirements)
def post_process(self):
self.get_distro().post_process()
def parse_args(self, argv):
"""Parses command-line arguments."""
parser = optparse.OptionParser()
parser.add_option('-n', '--no-site-packages',
action='store_true',
help="Do not inherit packages from global Python "
"install")
return parser.parse_args(argv[1:])[0]
class Distro(InstallVenv):
def check_cmd(self, cmd):
return bool(self.run_command(['which', cmd],
check_exit_code=False).strip())
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if self.check_cmd('easy_install'):
print('Installing virtualenv via easy_install...', end=' ')
if self.run_command(['easy_install', 'virtualenv']):
print('Succeeded')
return
else:
print('Failed')
self.die('ERROR: virtualenv not found.\n\n%s development'
' requires virtualenv, please install it using your'
' favorite package management tool' % self.project)
def post_process(self):
"""Any distribution-specific post-processing gets done here.
In particular, this is useful for applying patches to code inside
the venv.
"""
pass
class Fedora(Distro):
"""This covers all Fedora-based distributions.
Includes: Fedora, RHEL, CentOS, Scientific Linux
"""
def check_pkg(self, pkg):
return self.run_command_with_code(['rpm', '-q', pkg],
check_exit_code=False)[1] == 0
def apply_patch(self, originalfile, patchfile):
self.run_command(['patch', '-N', originalfile, patchfile],
check_exit_code=False)
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if not self.check_pkg('python-virtualenv'):
self.die("Please install 'python-virtualenv'.")
super(Fedora, self).install_virtualenv()
def post_process(self):
"""Workaround for a bug in eventlet.
This currently affects RHEL6.1, but the fix can safely be
applied to all RHEL and Fedora distributions.
This can be removed when the fix is applied upstream.
Nova: https://bugs.launchpad.net/nova/+bug/884915
Upstream: https://bitbucket.org/eventlet/eventlet/issue/89
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'.")
# 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 +0,0 @@
argparse
prettytable>=0.6,<0.8
requests>=0.8
simplejson>=2.0.9

View File

@@ -1,10 +0,0 @@
distribute>=0.6.24
coverage
discover
fixtures
mock
pep8==1.3.3
sphinx>=1.1.2
testrepository>=0.0.13
testtools>=0.9.22

14
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = py26,py27,pep8
envlist = py26,py27,py33,pep8
[testenv]
setenv = VIRTUAL_ENV={envdir}
@@ -7,13 +7,12 @@ setenv = VIRTUAL_ENV={envdir}
LANGUAGE=en_US:en
LC_ALL=C
deps = -r{toxinidir}/tools/pip-requires
-r{toxinidir}/tools/test-requires
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py testr --testr-args='{posargs}'
[testenv:pep8]
deps = pep8
commands = pep8 --repeat --show-source cinderclient setup.py
commands = flake8
[testenv:venv]
commands = {posargs}
@@ -23,3 +22,8 @@ commands = python setup.py testr --coverage --testr-args='{posargs}'
[tox:jenkins]
downloadcache = ~/cache/pip
[flake8]
show-source = True
ignore = F811,F821,H302,H306,H404
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools