Merge tag '1.0.5' into debian/unstable
Push python-cinderclient 1.0.5 to PyPi
This commit is contained in:
7
.coveragerc
Normal file
7
.coveragerc
Normal file
@@ -0,0 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = cinderclient
|
||||
omit = cinderclient/openstack/*
|
||||
|
||||
[report]
|
||||
ignore-errors = True
|
||||
@@ -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
115
HACKING
@@ -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
70
HACKING.rst
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
@@ -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 ...
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
@@ -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({
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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 |
|
||||
+---+---+
|
||||
""")
|
||||
@@ -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
|
||||
34
cinderclient/tests/v1/contrib/test_list_extensions.py
Normal file
34
cinderclient/tests/v1/contrib/test_list_extensions.py
Normal 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)
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -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.
|
||||
87
cinderclient/tests/v1/test_availability_zone.py
Normal file
87
cinderclient/tests/v1/test_availability_zone.py
Normal 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])
|
||||
@@ -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):
|
||||
@@ -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()
|
||||
62
cinderclient/tests/v1/test_services.py
Normal file
62
cinderclient/tests/v1/test_services.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
51
cinderclient/tests/v1/test_volume_transfers.py
Normal file
51
cinderclient/tests/v1/test_volume_transfers.py
Normal 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)
|
||||
@@ -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')
|
||||
@@ -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 = [
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -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.
|
||||
87
cinderclient/tests/v2/test_availability_zone.py
Normal file
87
cinderclient/tests/v2/test_availability_zone.py
Normal 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])
|
||||
@@ -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):
|
||||
@@ -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()
|
||||
62
cinderclient/tests/v2/test_services.py
Normal file
62
cinderclient/tests/v2/test_services.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
51
cinderclient/tests/v2/test_volume_transfers.py
Normal file
51
cinderclient/tests/v2/test_volume_transfers.py
Normal 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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
42
cinderclient/v1/availability_zones.py
Normal file
42
cinderclient/v1/availability_zones.py
Normal 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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
56
cinderclient/v1/services.py
Normal file
56
cinderclient/v1/services.py
Normal 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)
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
82
cinderclient/v1/volume_transfers.py
Normal file
82
cinderclient/v1/volume_transfers.py
Normal 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))
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
42
cinderclient/v2/availability_zones.py
Normal file
42
cinderclient/v2/availability_zones.py
Normal 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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
56
cinderclient/v2/services.py
Normal file
56
cinderclient/v2/services.py
Normal 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)
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
82
cinderclient/v2/volume_transfers.py
Normal file
82
cinderclient/v2/volume_transfers.py
Normal 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))
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
58
doc/source/man/cinder.rst
Normal 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/.
|
||||
@@ -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
6
requirements.txt
Normal 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
|
||||
170
run_tests.sh
170
run_tests.sh
@@ -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
|
||||
|
||||
33
setup.cfg
33
setup.cfg
@@ -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
|
||||
|
||||
54
setup.py
54
setup.py
@@ -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
13
test-requirements.txt
Normal 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
|
||||
@@ -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
336
tools/colorizer.py
Executable 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)
|
||||
@@ -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)
|
||||
|
||||
212
tools/install_venv_common.py
Normal file
212
tools/install_venv_common.py
Normal 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')
|
||||
@@ -1,4 +0,0 @@
|
||||
argparse
|
||||
prettytable>=0.6,<0.8
|
||||
requests>=0.8
|
||||
simplejson>=2.0.9
|
||||
@@ -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
14
tox.ini
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user