Added support for keystone v3client
Change-Id: I7bbc74c9e73f36f942f5800a7af0da717da0bc64
This commit is contained in:
parent
a87ee75285
commit
e8e06ee289
@ -14,7 +14,6 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
OpenStack Client interface. Handles the REST calls and responses.
|
OpenStack Client interface. Handles the REST calls and responses.
|
||||||
"""
|
"""
|
||||||
@ -23,6 +22,14 @@ from __future__ import print_function
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from cinderclient import exceptions
|
||||||
|
from cinderclient import utils
|
||||||
|
|
||||||
|
from keystoneclient import access
|
||||||
|
from keystoneclient.auth.identity import v3 as v3_auth
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import urlparse
|
import urlparse
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -43,15 +50,117 @@ if not hasattr(urlparse, 'parse_qsl'):
|
|||||||
import cgi
|
import cgi
|
||||||
urlparse.parse_qsl = cgi.parse_qsl
|
urlparse.parse_qsl = cgi.parse_qsl
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from keystoneclient import access
|
class CinderClientMixin(object):
|
||||||
|
|
||||||
from cinderclient import exceptions
|
def get_volume_api_version_from_endpoint(self):
|
||||||
from cinderclient import utils
|
magic_tuple = urlparse.urlsplit(self.management_url)
|
||||||
|
scheme, netloc, path, query, frag = magic_tuple
|
||||||
|
components = path.split("/")
|
||||||
|
valid_versions = ['v1', 'v2']
|
||||||
|
for version in valid_versions:
|
||||||
|
if version in components:
|
||||||
|
return version[1:]
|
||||||
|
msg = "Invalid client version '%s'. must be one of: %s" % (
|
||||||
|
(version, ', '.join(valid_versions)))
|
||||||
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
class SessionClient(CinderClientMixin):
|
||||||
|
|
||||||
|
def __init__(self, session, auth, interface=None,
|
||||||
|
service_type=None, service_name=None,
|
||||||
|
region_name=None, http_log_debug=False):
|
||||||
|
self.session = session
|
||||||
|
self.auth = auth
|
||||||
|
|
||||||
|
self.interface = interface
|
||||||
|
self.service_type = service_type
|
||||||
|
self.service_name = service_name
|
||||||
|
self.region_name = region_name
|
||||||
|
self.auth_token = None
|
||||||
|
self.endpoint_url = None
|
||||||
|
self.management_url = self.endpoint_url
|
||||||
|
self.http_log_debug = http_log_debug
|
||||||
|
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
if self.http_log_debug:
|
||||||
|
# Use keystoneclient's logs instead of writing our own
|
||||||
|
ks_logger = logging.getLogger("keystoneclient")
|
||||||
|
ks_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
def request(self, url, method, **kwargs):
|
||||||
|
kwargs.setdefault('user_agent', 'python-cinderclient')
|
||||||
|
kwargs.setdefault('auth', self.auth)
|
||||||
|
kwargs.setdefault('authenticated', False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs['json'] = kwargs.pop('body')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
|
||||||
|
endpoint_filter.setdefault('interface', self.interface)
|
||||||
|
endpoint_filter.setdefault('service_type', self.service_type)
|
||||||
|
endpoint_filter.setdefault('service_name', self.service_name)
|
||||||
|
endpoint_filter.setdefault('region_name', self.region_name)
|
||||||
|
|
||||||
|
resp = self.session.request(url, method, **kwargs)
|
||||||
|
|
||||||
|
body = None
|
||||||
|
if resp.text:
|
||||||
|
try:
|
||||||
|
body = resp.json()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return resp, body
|
||||||
|
|
||||||
|
def _cs_request(self, url, method, **kwargs):
|
||||||
|
# this function is mostly redundant but makes compatibility easier
|
||||||
|
kwargs.setdefault('authenticated', True)
|
||||||
|
return self.request(url, method, **kwargs)
|
||||||
|
|
||||||
|
def do_request(self, url, method, **kwargs):
|
||||||
|
# this function is mostly redundant but makes compatibility easier
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
if self.auth_token is None:
|
||||||
|
self.authenticate()
|
||||||
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
||||||
|
if self.access_info is not None:
|
||||||
|
kwargs['headers'][
|
||||||
|
'X-Auth-Project-Id'] = self.access_info.project_id
|
||||||
|
|
||||||
|
resp, body = self._cs_request(
|
||||||
|
self.endpoint_url + url, method, **kwargs)
|
||||||
|
return resp, body
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
self.auth_token = self.session.get_token(self.auth)
|
||||||
|
self.access_info = self.session.auth.get_access(self.session)
|
||||||
|
|
||||||
|
self.endpoint_url = self.session.get_endpoint(
|
||||||
|
self.auth,
|
||||||
|
service_type=self.service_type,
|
||||||
|
region_name=self.region_name,
|
||||||
|
interface=self.interface)
|
||||||
|
self.management_url = self.endpoint_url
|
||||||
|
self.service_catalog = self.access_info.service_catalog
|
||||||
|
|
||||||
|
def get(self, url, **kwargs):
|
||||||
|
return self.do_request(url, 'GET', **kwargs)
|
||||||
|
|
||||||
|
def post(self, url, **kwargs):
|
||||||
|
return self.do_request(url, 'POST', **kwargs)
|
||||||
|
|
||||||
|
def put(self, url, **kwargs):
|
||||||
|
return self.do_request(url, 'PUT', **kwargs)
|
||||||
|
|
||||||
|
def delete(self, url, **kwargs):
|
||||||
|
return self.do_request(url, 'DELETE', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClient(CinderClientMixin):
|
||||||
|
|
||||||
USER_AGENT = 'python-cinderclient'
|
USER_AGENT = 'python-cinderclient'
|
||||||
|
|
||||||
@ -385,17 +494,61 @@ class HTTPClient(object):
|
|||||||
|
|
||||||
return self._extract_service_catalog(url, resp, body)
|
return self._extract_service_catalog(url, resp, body)
|
||||||
|
|
||||||
def get_volume_api_version_from_endpoint(self):
|
|
||||||
magic_tuple = urlparse.urlsplit(self.management_url)
|
def _construct_http_client(username=None, password=None, project_id=None,
|
||||||
scheme, netloc, path, query, frag = magic_tuple
|
auth_url=None, insecure=False, timeout=None,
|
||||||
components = path.split("/")
|
proxy_tenant_id=None, proxy_token=None,
|
||||||
valid_versions = ['v1', 'v2']
|
region_name=None, endpoint_type='publicURL',
|
||||||
for version in valid_versions:
|
service_type='volume',
|
||||||
if version in components:
|
service_name=None, volume_service_name=None,
|
||||||
return version[1:]
|
retries=None,
|
||||||
msg = "Invalid client version '%s'. must be one of: %s" % (
|
http_log_debug=False,
|
||||||
(version, ', '.join(valid_versions)))
|
auth_system='keystone', auth_plugin=None,
|
||||||
raise exceptions.UnsupportedVersion(msg)
|
cacert=None, tenant_id=None,
|
||||||
|
session=None,
|
||||||
|
auth=None):
|
||||||
|
if session:
|
||||||
|
|
||||||
|
# If auth pluggin is specified use that pluggin
|
||||||
|
session.auth = auth or session.auth
|
||||||
|
|
||||||
|
if isinstance(session.auth, v3_auth.Password):
|
||||||
|
# In v3 and v2 interace names are different
|
||||||
|
interface_map = {"publicURL": "public",
|
||||||
|
"adminURL": "admin"}
|
||||||
|
|
||||||
|
endpoint_type = interface_map[endpoint_type]
|
||||||
|
|
||||||
|
return SessionClient(session=session,
|
||||||
|
auth=auth,
|
||||||
|
interface=endpoint_type,
|
||||||
|
service_type=service_type,
|
||||||
|
service_name=service_name,
|
||||||
|
region_name=region_name,
|
||||||
|
http_log_debug=http_log_debug)
|
||||||
|
else:
|
||||||
|
# FIXME(jamielennox): username and password are now optional. Need
|
||||||
|
# to test that they were provided in this mode.
|
||||||
|
return HTTPClient(username,
|
||||||
|
password,
|
||||||
|
projectid=project_id,
|
||||||
|
auth_url=auth_url,
|
||||||
|
insecure=insecure,
|
||||||
|
timeout=timeout,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
proxy_token=proxy_token,
|
||||||
|
proxy_tenant_id=proxy_tenant_id,
|
||||||
|
region_name=region_name,
|
||||||
|
endpoint_type=endpoint_type,
|
||||||
|
service_type=service_type,
|
||||||
|
service_name=service_name,
|
||||||
|
volume_service_name=volume_service_name,
|
||||||
|
retries=retries,
|
||||||
|
http_log_debug=http_log_debug,
|
||||||
|
cacert=cacert,
|
||||||
|
auth_system=auth_system,
|
||||||
|
auth_plugin=auth_plugin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_client_class(version):
|
def get_client_class(version):
|
||||||
|
@ -24,20 +24,27 @@ import argparse
|
|||||||
import glob
|
import glob
|
||||||
import imp
|
import imp
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
import logging
|
|
||||||
|
|
||||||
import cinderclient.auth_plugin
|
|
||||||
from cinderclient import client
|
from cinderclient import client
|
||||||
from cinderclient import exceptions as exc
|
from cinderclient import exceptions as exc
|
||||||
|
from cinderclient import utils
|
||||||
|
import cinderclient.auth_plugin
|
||||||
import cinderclient.extension
|
import cinderclient.extension
|
||||||
from cinderclient.openstack.common import strutils
|
from cinderclient.openstack.common import strutils
|
||||||
from cinderclient import utils
|
from cinderclient.openstack.common.gettextutils import _
|
||||||
from cinderclient.v1 import shell as shell_v1
|
from cinderclient.v1 import shell as shell_v1
|
||||||
from cinderclient.v2 import shell as shell_v2
|
from cinderclient.v2 import shell as shell_v2
|
||||||
|
|
||||||
|
from keystoneclient import discover
|
||||||
|
from keystoneclient import session
|
||||||
|
from keystoneclient.auth.identity import v2 as v2_auth
|
||||||
|
from keystoneclient.auth.identity import v3 as v3_auth
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_OS_VOLUME_API_VERSION = "1"
|
DEFAULT_OS_VOLUME_API_VERSION = "1"
|
||||||
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
|
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
|
||||||
DEFAULT_CINDER_SERVICE_TYPE = 'volume'
|
DEFAULT_CINDER_SERVICE_TYPE = 'volume'
|
||||||
@ -58,7 +65,7 @@ class CinderClientArgumentParser(argparse.ArgumentParser):
|
|||||||
exits.
|
exits.
|
||||||
"""
|
"""
|
||||||
self.print_usage(sys.stderr)
|
self.print_usage(sys.stderr)
|
||||||
#FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
# FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
||||||
choose_from = ' (choose from'
|
choose_from = ' (choose from'
|
||||||
progparts = self.prog.partition(' ')
|
progparts = self.prog.partition(' ')
|
||||||
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
||||||
@ -117,6 +124,116 @@ class OpenStackCinderShell(object):
|
|||||||
default=False),
|
default=False),
|
||||||
help="Shows debugging output.")
|
help="Shows debugging output.")
|
||||||
|
|
||||||
|
parser.add_argument('--os-auth-system',
|
||||||
|
metavar='<auth-system>',
|
||||||
|
default=utils.env('OS_AUTH_SYSTEM'),
|
||||||
|
help='Defaults to env[OS_AUTH_SYSTEM].')
|
||||||
|
parser.add_argument('--os_auth_system',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--service-type',
|
||||||
|
metavar='<service-type>',
|
||||||
|
help='Service type. '
|
||||||
|
'For most actions, default is volume.')
|
||||||
|
parser.add_argument('--service_type',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--service-name',
|
||||||
|
metavar='<service-name>',
|
||||||
|
default=utils.env('CINDER_SERVICE_NAME'),
|
||||||
|
help='Service name. '
|
||||||
|
'Default=env[CINDER_SERVICE_NAME].')
|
||||||
|
parser.add_argument('--service_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--volume-service-name',
|
||||||
|
metavar='<volume-service-name>',
|
||||||
|
default=utils.env('CINDER_VOLUME_SERVICE_NAME'),
|
||||||
|
help='Volume service name. '
|
||||||
|
'Default=env[CINDER_VOLUME_SERVICE_NAME].')
|
||||||
|
parser.add_argument('--volume_service_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--endpoint-type',
|
||||||
|
metavar='<endpoint-type>',
|
||||||
|
default=utils.env('CINDER_ENDPOINT_TYPE',
|
||||||
|
default=
|
||||||
|
DEFAULT_CINDER_ENDPOINT_TYPE),
|
||||||
|
help='Endpoint type, which is publicURL or '
|
||||||
|
'internalURL. '
|
||||||
|
'Default=nova env[CINDER_ENDPOINT_TYPE] or '
|
||||||
|
+ DEFAULT_CINDER_ENDPOINT_TYPE + '.')
|
||||||
|
|
||||||
|
parser.add_argument('--endpoint_type',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--os-volume-api-version',
|
||||||
|
metavar='<volume-api-ver>',
|
||||||
|
default=utils.env('OS_VOLUME_API_VERSION',
|
||||||
|
default=None),
|
||||||
|
help='Block Storage API version. '
|
||||||
|
'Valid values are 1 or 2. '
|
||||||
|
'Default=env[OS_VOLUME_API_VERSION].')
|
||||||
|
parser.add_argument('--os_volume_api_version',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--retries',
|
||||||
|
metavar='<retries>',
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help='Number of retries.')
|
||||||
|
|
||||||
|
self._append_global_identity_args(parser)
|
||||||
|
|
||||||
|
# FIXME(dtroyer): The args below are here for diablo compatibility,
|
||||||
|
# remove them in folsum cycle
|
||||||
|
|
||||||
|
# alias for --os-username, left in for backwards compatibility
|
||||||
|
parser.add_argument('--username',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
# alias for --os-region_name, left in for backwards compatibility
|
||||||
|
parser.add_argument('--region_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
# alias for --os-password, left in for backwards compatibility
|
||||||
|
parser.add_argument('--apikey', '--password', dest='apikey',
|
||||||
|
default=utils.env('CINDER_API_KEY'),
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
# alias for --os-tenant-name, left in for backward compatibility
|
||||||
|
parser.add_argument('--projectid', '--tenant_name', dest='projectid',
|
||||||
|
default=utils.env('CINDER_PROJECT_ID'),
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
# alias for --os-auth-url, left in for backward compatibility
|
||||||
|
parser.add_argument('--url', '--auth_url', dest='url',
|
||||||
|
default=utils.env('CINDER_URL'),
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
# The auth-system-plugins might require some extra options
|
||||||
|
cinderclient.auth_plugin.discover_auth_systems()
|
||||||
|
cinderclient.auth_plugin.load_auth_system_opts(parser)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def _append_global_identity_args(self, parser):
|
||||||
|
# FIXME(bklei): these are global identity (Keystone) arguments which
|
||||||
|
# should be consistent and shared by all service clients. Therefore,
|
||||||
|
# they should be provided by python-keystoneclient. We will need to
|
||||||
|
# refactor this code once this functionality is available in
|
||||||
|
# python-keystoneclient.
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-auth-strategy', metavar='<auth-strategy>',
|
||||||
|
default=utils.env('OS_AUTH_STRATEGY', default='keystone'),
|
||||||
|
help=_('Authentication strategy (Env: OS_AUTH_STRATEGY'
|
||||||
|
', default keystone). For now, any other value will'
|
||||||
|
' disable the authentication'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_auth_strategy',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument('--os-username',
|
parser.add_argument('--os-username',
|
||||||
metavar='<auth-user-name>',
|
metavar='<auth-user-name>',
|
||||||
default=utils.env('OS_USERNAME',
|
default=utils.env('OS_USERNAME',
|
||||||
@ -162,6 +279,87 @@ class OpenStackCinderShell(object):
|
|||||||
parser.add_argument('--os_auth_url',
|
parser.add_argument('--os_auth_url',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-user-id', metavar='<auth-user-id>',
|
||||||
|
default=utils.env('OS_USER_ID'),
|
||||||
|
help=_('Authentication user ID (Env: OS_USER_ID)'))
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_user_id',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-user-domain-id',
|
||||||
|
metavar='<auth-user-domain-id>',
|
||||||
|
default=utils.env('OS_USER_DOMAIN_ID'),
|
||||||
|
help='OpenStack user domain ID. '
|
||||||
|
'Defaults to env[OS_USER_DOMAIN_ID].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_user_domain_id',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-user-domain-name',
|
||||||
|
metavar='<auth-user-domain-name>',
|
||||||
|
default=utils.env('OS_USER_DOMAIN_NAME'),
|
||||||
|
help='OpenStack user domain name. '
|
||||||
|
'Defaults to env[OS_USER_DOMAIN_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_user_domain_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-project-id',
|
||||||
|
metavar='<auth-project-id>',
|
||||||
|
default=utils.env('OS_PROJECT_ID'),
|
||||||
|
help='Another way to specify tenant ID. '
|
||||||
|
'This option is mutually exclusive with '
|
||||||
|
' --os-tenant-id. '
|
||||||
|
'Defaults to env[OS_PROJECT_ID].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_project_id',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-project-name',
|
||||||
|
metavar='<auth-project-name>',
|
||||||
|
default=utils.env('OS_PROJECT_NAME'),
|
||||||
|
help='Another way to specify tenant name. '
|
||||||
|
'This option is mutually exclusive with '
|
||||||
|
' --os-tenant-name. '
|
||||||
|
'Defaults to env[OS_PROJECT_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os_project_name',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-project-domain-id',
|
||||||
|
metavar='<auth-project-domain-id>',
|
||||||
|
default=utils.env('OS_PROJECT_DOMAIN_ID'),
|
||||||
|
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-project-domain-name',
|
||||||
|
metavar='<auth-project-domain-name>',
|
||||||
|
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||||
|
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-cert',
|
||||||
|
metavar='<certificate>',
|
||||||
|
default=utils.env('OS_CERT'),
|
||||||
|
help='Defaults to env[OS_CERT].')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-key',
|
||||||
|
metavar='<key>',
|
||||||
|
default=utils.env('OS_KEY'),
|
||||||
|
help='Defaults to env[OS_KEY].')
|
||||||
|
|
||||||
parser.add_argument('--os-region-name',
|
parser.add_argument('--os-region-name',
|
||||||
metavar='<region-name>',
|
metavar='<region-name>',
|
||||||
default=utils.env('OS_REGION_NAME',
|
default=utils.env('OS_REGION_NAME',
|
||||||
@ -171,63 +369,29 @@ class OpenStackCinderShell(object):
|
|||||||
parser.add_argument('--os_region_name',
|
parser.add_argument('--os_region_name',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument('--os-auth-system',
|
parser.add_argument(
|
||||||
metavar='<auth-system>',
|
'--os-token', metavar='<token>',
|
||||||
default=utils.env('OS_AUTH_SYSTEM'),
|
default=utils.env('OS_TOKEN'),
|
||||||
help='Defaults to env[OS_AUTH_SYSTEM].')
|
help=_('Defaults to env[OS_TOKEN]'))
|
||||||
parser.add_argument('--os_auth_system',
|
parser.add_argument(
|
||||||
help=argparse.SUPPRESS)
|
'--os_token',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument('--service-type',
|
parser.add_argument(
|
||||||
metavar='<service-type>',
|
'--os-url', metavar='<url>',
|
||||||
help='Service type. '
|
default=utils.env('OS_URL'),
|
||||||
'For most actions, default is volume.')
|
help=_('Defaults to env[OS_URL]'))
|
||||||
parser.add_argument('--service_type',
|
parser.add_argument(
|
||||||
help=argparse.SUPPRESS)
|
'--os_url',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument('--service-name',
|
parser.add_argument(
|
||||||
metavar='<service-name>',
|
'--os-cacert',
|
||||||
default=utils.env('CINDER_SERVICE_NAME'),
|
metavar='<ca-certificate>',
|
||||||
help='Service name. '
|
default=utils.env('OS_CACERT', default=None),
|
||||||
'Default=env[CINDER_SERVICE_NAME].')
|
help=_("Specify a CA bundle file to use in "
|
||||||
parser.add_argument('--service_name',
|
"verifying a TLS (https) server certificate. "
|
||||||
help=argparse.SUPPRESS)
|
"Defaults to env[OS_CACERT]"))
|
||||||
|
|
||||||
parser.add_argument('--volume-service-name',
|
|
||||||
metavar='<volume-service-name>',
|
|
||||||
default=utils.env('CINDER_VOLUME_SERVICE_NAME'),
|
|
||||||
help='Volume service name. '
|
|
||||||
'Default=env[CINDER_VOLUME_SERVICE_NAME].')
|
|
||||||
parser.add_argument('--volume_service_name',
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
parser.add_argument('--endpoint-type',
|
|
||||||
metavar='<endpoint-type>',
|
|
||||||
default=utils.env('CINDER_ENDPOINT_TYPE',
|
|
||||||
default=DEFAULT_CINDER_ENDPOINT_TYPE),
|
|
||||||
help='Endpoint type, which is publicURL or '
|
|
||||||
'internalURL. '
|
|
||||||
'Default=nova env[CINDER_ENDPOINT_TYPE] or '
|
|
||||||
+ DEFAULT_CINDER_ENDPOINT_TYPE + '.')
|
|
||||||
parser.add_argument('--endpoint_type',
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
parser.add_argument('--os-volume-api-version',
|
|
||||||
metavar='<volume-api-ver>',
|
|
||||||
default=utils.env('OS_VOLUME_API_VERSION',
|
|
||||||
default=None),
|
|
||||||
help='Block Storage API version. '
|
|
||||||
'Valid values are 1 or 2. '
|
|
||||||
'Default=env[OS_VOLUME_API_VERSION].')
|
|
||||||
parser.add_argument('--os_volume_api_version',
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
parser.add_argument('--os-cacert',
|
|
||||||
metavar='<ca-certificate>',
|
|
||||||
default=utils.env('OS_CACERT', default=None),
|
|
||||||
help='A CA bundle file that is used to '
|
|
||||||
'verify a TLS (https) server certificate. '
|
|
||||||
'Default=env[OS_CACERT].')
|
|
||||||
|
|
||||||
parser.add_argument('--insecure',
|
parser.add_argument('--insecure',
|
||||||
default=utils.env('CINDERCLIENT_INSECURE',
|
default=utils.env('CINDERCLIENT_INSECURE',
|
||||||
@ -235,44 +399,6 @@ class OpenStackCinderShell(object):
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument('--retries',
|
|
||||||
metavar='<retries>',
|
|
||||||
type=int,
|
|
||||||
default=0,
|
|
||||||
help='Number of retries.')
|
|
||||||
|
|
||||||
# FIXME(dtroyer): The args below are here for diablo compatibility,
|
|
||||||
# remove them in folsum cycle
|
|
||||||
|
|
||||||
# alias for --os-username, left in for backwards compatibility
|
|
||||||
parser.add_argument('--username',
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
# alias for --os-region_name, left in for backwards compatibility
|
|
||||||
parser.add_argument('--region_name',
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
# alias for --os-password, left in for backwards compatibility
|
|
||||||
parser.add_argument('--apikey', '--password', dest='apikey',
|
|
||||||
default=utils.env('CINDER_API_KEY'),
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
# alias for --os-tenant-name, left in for backward compatibility
|
|
||||||
parser.add_argument('--projectid', '--tenant_name', dest='projectid',
|
|
||||||
default=utils.env('CINDER_PROJECT_ID'),
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
# alias for --os-auth-url, left in for backward compatibility
|
|
||||||
parser.add_argument('--url', '--auth_url', dest='url',
|
|
||||||
default=utils.env('CINDER_URL'),
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
# The auth-system-plugins might require some extra options
|
|
||||||
cinderclient.auth_plugin.discover_auth_systems()
|
|
||||||
cinderclient.auth_plugin.load_auth_system_opts(parser)
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
def get_subcommand_parser(self, version):
|
def get_subcommand_parser(self, version):
|
||||||
parser = self.get_base_parser()
|
parser = self.get_base_parser()
|
||||||
|
|
||||||
@ -378,11 +504,13 @@ class OpenStackCinderShell(object):
|
|||||||
logger.addHandler(streamhandler)
|
logger.addHandler(streamhandler)
|
||||||
|
|
||||||
def main(self, argv):
|
def main(self, argv):
|
||||||
|
|
||||||
# Parse args once to find version and debug settings
|
# Parse args once to find version and debug settings
|
||||||
parser = self.get_base_parser()
|
parser = self.get_base_parser()
|
||||||
(options, args) = parser.parse_known_args(argv)
|
(options, args) = parser.parse_known_args(argv)
|
||||||
self.setup_debugging(options.debug)
|
self.setup_debugging(options.debug)
|
||||||
api_version_input = True
|
api_version_input = True
|
||||||
|
self.options = options
|
||||||
|
|
||||||
if not options.os_volume_api_version:
|
if not options.os_volume_api_version:
|
||||||
# Environment variable OS_VOLUME_API_VERSION was
|
# Environment variable OS_VOLUME_API_VERSION was
|
||||||
@ -442,7 +570,7 @@ class OpenStackCinderShell(object):
|
|||||||
service_type = DEFAULT_CINDER_SERVICE_TYPE
|
service_type = DEFAULT_CINDER_SERVICE_TYPE
|
||||||
service_type = utils.get_service_type(args.func) or service_type
|
service_type = utils.get_service_type(args.func) or service_type
|
||||||
|
|
||||||
#FIXME(usrleon): Here should be restrict for project id same as
|
# FIXME(usrleon): Here should be restrict for project id same as
|
||||||
# for os_username or os_password but for compatibility it is not.
|
# for os_username or os_password but for compatibility it is not.
|
||||||
|
|
||||||
if not utils.isunauthenticated(args.func):
|
if not utils.isunauthenticated(args.func):
|
||||||
@ -474,6 +602,28 @@ class OpenStackCinderShell(object):
|
|||||||
else:
|
else:
|
||||||
os_tenant_name = projectid
|
os_tenant_name = projectid
|
||||||
|
|
||||||
|
# V3 stuff
|
||||||
|
project_info_provided = self.options.os_tenant_name or \
|
||||||
|
self.options.os_tenant_id or \
|
||||||
|
(self.options.os_project_name and
|
||||||
|
(self.options.project_domain_name or
|
||||||
|
self.options.project_domain_id)) or \
|
||||||
|
self.options.os_project_id
|
||||||
|
|
||||||
|
if (not project_info_provided):
|
||||||
|
raise exc.CommandError(
|
||||||
|
_("You must provide a tenant_name, tenant_id, "
|
||||||
|
"project_id or project_name (with "
|
||||||
|
"project_domain_name or project_domain_id) via "
|
||||||
|
" --os-tenant-name (env[OS_TENANT_NAME]),"
|
||||||
|
" --os-tenant-id (env[OS_TENANT_ID]),"
|
||||||
|
" --os-project-id (env[OS_PROJECT_ID])"
|
||||||
|
" --os-project-name (env[OS_PROJECT_NAME]),"
|
||||||
|
" --os-project-domain-id "
|
||||||
|
"(env[OS_PROJECT_DOMAIN_ID])"
|
||||||
|
" --os-project-domain-name "
|
||||||
|
"(env[OS_PROJECT_DOMAIN_NAME])"))
|
||||||
|
|
||||||
if not os_auth_url:
|
if not os_auth_url:
|
||||||
if os_auth_system and os_auth_system != 'keystone':
|
if os_auth_system and os_auth_system != 'keystone':
|
||||||
os_auth_url = auth_plugin.get_auth_url()
|
os_auth_url = auth_plugin.get_auth_url()
|
||||||
@ -499,6 +649,8 @@ class OpenStackCinderShell(object):
|
|||||||
"You must provide an authentication URL "
|
"You must provide an authentication URL "
|
||||||
"through --os-auth-url or env[OS_AUTH_URL].")
|
"through --os-auth-url or env[OS_AUTH_URL].")
|
||||||
|
|
||||||
|
auth_session = self._get_keystone_session()
|
||||||
|
|
||||||
self.cs = client.Client(options.os_volume_api_version, os_username,
|
self.cs = client.Client(options.os_volume_api_version, os_username,
|
||||||
os_password, os_tenant_name, os_auth_url,
|
os_password, os_tenant_name, os_auth_url,
|
||||||
insecure, region_name=os_region_name,
|
insecure, region_name=os_region_name,
|
||||||
@ -511,7 +663,8 @@ class OpenStackCinderShell(object):
|
|||||||
retries=options.retries,
|
retries=options.retries,
|
||||||
http_log_debug=args.debug,
|
http_log_debug=args.debug,
|
||||||
cacert=cacert, auth_system=os_auth_system,
|
cacert=cacert, auth_system=os_auth_system,
|
||||||
auth_plugin=auth_plugin)
|
auth_plugin=auth_plugin,
|
||||||
|
session=auth_session)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not utils.isunauthenticated(args.func):
|
if not utils.isunauthenticated(args.func):
|
||||||
@ -588,9 +741,101 @@ class OpenStackCinderShell(object):
|
|||||||
else:
|
else:
|
||||||
self.parser.print_help()
|
self.parser.print_help()
|
||||||
|
|
||||||
|
def get_v2_auth(self, v2_auth_url):
|
||||||
|
|
||||||
|
username = self.options.os_username
|
||||||
|
password = self.options.os_password
|
||||||
|
tenant_id = self.options.os_tenant_id
|
||||||
|
tenant_name = self.options.os_tenant_name
|
||||||
|
|
||||||
|
return v2_auth.Password(
|
||||||
|
v2_auth_url,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
tenant_name=tenant_name)
|
||||||
|
|
||||||
|
def get_v3_auth(self, v3_auth_url):
|
||||||
|
|
||||||
|
username = self.options.os_username
|
||||||
|
user_id = self.options.os_user_id
|
||||||
|
user_domain_name = self.options.os_user_domain_name
|
||||||
|
user_domain_id = self.options.os_user_domain_id
|
||||||
|
password = self.options.os_password
|
||||||
|
project_id = self.options.os_project_id or self.options.os_tenant_id
|
||||||
|
project_name = (self.options.os_project_name
|
||||||
|
or self.options.os_tenant_name)
|
||||||
|
project_domain_name = self.options.os_project_domain_name
|
||||||
|
project_domain_id = self.options.os_project_domain_id
|
||||||
|
|
||||||
|
return v3_auth.Password(
|
||||||
|
v3_auth_url,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
user_id=user_id,
|
||||||
|
user_domain_name=user_domain_name,
|
||||||
|
user_domain_id=user_domain_id,
|
||||||
|
project_id=project_id,
|
||||||
|
project_name=project_name,
|
||||||
|
project_domain_name=project_domain_name,
|
||||||
|
project_domain_id=project_domain_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_keystone_session(self, **kwargs):
|
||||||
|
# first create a Keystone session
|
||||||
|
cacert = self.options.os_cacert or None
|
||||||
|
cert = self.options.os_cert or None
|
||||||
|
insecure = self.options.insecure or False
|
||||||
|
|
||||||
|
if insecure:
|
||||||
|
verify = False
|
||||||
|
else:
|
||||||
|
verify = cacert or True
|
||||||
|
ks_session = session.Session(verify=verify, cert=cert)
|
||||||
|
# discover the supported keystone versions using the given url
|
||||||
|
ks_discover = discover.Discover(session=ks_session,
|
||||||
|
auth_url=self.options.os_auth_url)
|
||||||
|
|
||||||
|
# Determine which authentication plugin to use. First inspect the
|
||||||
|
# auth_url to see the supported version. If both v3 and v2 are
|
||||||
|
# supported, then use the highest version if possible.
|
||||||
|
v2_auth_url = ks_discover.url_for('v2.0')
|
||||||
|
v3_auth_url = ks_discover.url_for('v3.0')
|
||||||
|
|
||||||
|
username = self.options.os_username or None
|
||||||
|
user_domain_name = self.options.os_user_domain_name or None
|
||||||
|
user_domain_id = self.options.os_user_domain_id or None
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if v3_auth_url and v2_auth_url:
|
||||||
|
# support both v2 and v3 auth. Use v3 if possible.
|
||||||
|
if username:
|
||||||
|
if user_domain_name or user_domain_id:
|
||||||
|
# use v3 auth
|
||||||
|
auth = self.get_v3_auth(v3_auth_url)
|
||||||
|
else:
|
||||||
|
# use v2 auth
|
||||||
|
auth = self.get_v2_auth(v2_auth_url)
|
||||||
|
|
||||||
|
elif v3_auth_url:
|
||||||
|
# support only v3
|
||||||
|
auth = self.get_v3_auth(v3_auth_url)
|
||||||
|
elif v2_auth_url:
|
||||||
|
# support only v2
|
||||||
|
auth = self.get_v2_auth(v2_auth_url)
|
||||||
|
else:
|
||||||
|
raise exc.CommandError('Unable to determine the Keystone version '
|
||||||
|
'to authenticate with using the given '
|
||||||
|
'auth_url.')
|
||||||
|
|
||||||
|
ks_session.auth = auth
|
||||||
|
return ks_session
|
||||||
|
|
||||||
# I'm picky about my shell help.
|
# I'm picky about my shell help.
|
||||||
|
|
||||||
|
|
||||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||||
|
|
||||||
def start_section(self, heading):
|
def start_section(self, heading):
|
||||||
# Title-case the headings
|
# Title-case the headings
|
||||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||||
@ -603,7 +848,7 @@ def main():
|
|||||||
OpenStackCinderShell().main(sys.argv[1:])
|
OpenStackCinderShell().main(sys.argv[1:])
|
||||||
else:
|
else:
|
||||||
OpenStackCinderShell().main(map(strutils.safe_decode,
|
OpenStackCinderShell().main(map(strutils.safe_decode,
|
||||||
sys.argv[1:]))
|
sys.argv[1:]))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("... terminating cinder client", file=sys.stderr)
|
print("... terminating cinder client", file=sys.stderr)
|
||||||
sys.exit(130)
|
sys.exit(130)
|
||||||
@ -614,4 +859,5 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
0
cinderclient/tests/fixture_data/__init__.py
Normal file
0
cinderclient/tests/fixture_data/__init__.py
Normal file
218
cinderclient/tests/fixture_data/keystone_client.py
Normal file
218
cinderclient/tests/fixture_data/keystone_client.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
# these are copied from python-keystoneclient tests
|
||||||
|
BASE_HOST = 'http://keystone.example.com'
|
||||||
|
BASE_URL = "%s:5000/" % BASE_HOST
|
||||||
|
UPDATED = '2013-03-06T00:00:00Z'
|
||||||
|
|
||||||
|
V2_URL = "%sv2.0" % BASE_URL
|
||||||
|
V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/'
|
||||||
|
'openstack-identity-service/2.0/content/',
|
||||||
|
'rel': 'describedby',
|
||||||
|
'type': 'text/html'}
|
||||||
|
|
||||||
|
V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident'
|
||||||
|
'ity-service/2.0/identity-dev-guide-2.0.pdf',
|
||||||
|
'rel': 'describedby',
|
||||||
|
'type': 'application/pdf'}
|
||||||
|
|
||||||
|
V2_VERSION = {'id': 'v2.0',
|
||||||
|
'links': [{'href': V2_URL, 'rel': 'self'},
|
||||||
|
V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF],
|
||||||
|
'status': 'stable',
|
||||||
|
'updated': UPDATED}
|
||||||
|
|
||||||
|
V3_URL = "%sv3" % BASE_URL
|
||||||
|
V3_MEDIA_TYPES = [{'base': 'application/json',
|
||||||
|
'type': 'application/vnd.openstack.identity-v3+json'},
|
||||||
|
{'base': 'application/xml',
|
||||||
|
'type': 'application/vnd.openstack.identity-v3+xml'}]
|
||||||
|
|
||||||
|
V3_VERSION = {'id': 'v3.0',
|
||||||
|
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': 'stable',
|
||||||
|
'updated': UPDATED}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_version_list(versions):
|
||||||
|
return json.dumps({'versions': {'values': versions}})
|
||||||
|
|
||||||
|
|
||||||
|
def _create_single_version(version):
|
||||||
|
return json.dumps({'version': version})
|
||||||
|
|
||||||
|
|
||||||
|
V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
|
||||||
|
V2_VERSION_LIST = _create_version_list([V2_VERSION])
|
||||||
|
|
||||||
|
V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
|
||||||
|
V2_VERSION_ENTRY = _create_single_version(V2_VERSION)
|
||||||
|
|
||||||
|
CINDER_ENDPOINT = 'http://www.cinder.com/v1'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_normalized_token_data(**kwargs):
|
||||||
|
ref = copy.deepcopy(kwargs)
|
||||||
|
# normalized token data
|
||||||
|
ref['user_id'] = ref.get('user_id', uuid.uuid4().hex)
|
||||||
|
ref['username'] = ref.get('username', uuid.uuid4().hex)
|
||||||
|
ref['project_id'] = ref.get('project_id',
|
||||||
|
ref.get('tenant_id', uuid.uuid4().hex))
|
||||||
|
ref['project_name'] = ref.get('tenant_name',
|
||||||
|
ref.get('tenant_name', uuid.uuid4().hex))
|
||||||
|
ref['user_domain_id'] = ref.get('user_domain_id', uuid.uuid4().hex)
|
||||||
|
ref['user_domain_name'] = ref.get('user_domain_name', uuid.uuid4().hex)
|
||||||
|
ref['project_domain_id'] = ref.get('project_domain_id', uuid.uuid4().hex)
|
||||||
|
ref['project_domain_name'] = ref.get('project_domain_name',
|
||||||
|
uuid.uuid4().hex)
|
||||||
|
ref['roles'] = ref.get('roles', [{'name': uuid.uuid4().hex,
|
||||||
|
'id': uuid.uuid4().hex}])
|
||||||
|
ref['roles_link'] = ref.get('roles_link', [])
|
||||||
|
ref['cinder_url'] = ref.get('cinder_url', CINDER_ENDPOINT)
|
||||||
|
|
||||||
|
return ref
|
||||||
|
|
||||||
|
|
||||||
|
def generate_v2_project_scoped_token(**kwargs):
|
||||||
|
"""Generate a Keystone V2 token based on auth request."""
|
||||||
|
ref = _get_normalized_token_data(**kwargs)
|
||||||
|
token = uuid.uuid4().hex
|
||||||
|
|
||||||
|
o = {'access': {'token': {'id': token,
|
||||||
|
'expires': '2099-05-22T00:02:43.941430Z',
|
||||||
|
'issued_at': '2013-05-21T00:02:43.941473Z',
|
||||||
|
'tenant': {'enabled': True,
|
||||||
|
'id': ref.get('project_id'),
|
||||||
|
'name': ref.get('project_id')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'user': {'id': ref.get('user_id'),
|
||||||
|
'name': uuid.uuid4().hex,
|
||||||
|
'username': ref.get('username'),
|
||||||
|
'roles': ref.get('roles'),
|
||||||
|
'roles_links': ref.get('roles_links')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
# we only care about Neutron and Keystone endpoints
|
||||||
|
o['access']['serviceCatalog'] = [
|
||||||
|
{'endpoints': [
|
||||||
|
{'publicURL': 'public_' + ref.get('cinder_url'),
|
||||||
|
'internalURL': 'internal_' + ref.get('cinder_url'),
|
||||||
|
'adminURL': 'admin_' + (ref.get('auth_url') or ""),
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'region': 'RegionOne'
|
||||||
|
}],
|
||||||
|
'endpoints_links': [],
|
||||||
|
'name': 'Neutron',
|
||||||
|
'type': 'network'},
|
||||||
|
{'endpoints': [
|
||||||
|
{'publicURL': ref.get('auth_url'),
|
||||||
|
'adminURL': ref.get('auth_url'),
|
||||||
|
'internalURL': ref.get('auth_url'),
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'region': 'RegionOne'
|
||||||
|
}],
|
||||||
|
'endpoint_links': [],
|
||||||
|
'name': 'keystone',
|
||||||
|
'type': 'identity'}]
|
||||||
|
|
||||||
|
return token, o
|
||||||
|
|
||||||
|
|
||||||
|
def generate_v3_project_scoped_token(**kwargs):
|
||||||
|
"""Generate a Keystone V3 token based on auth request."""
|
||||||
|
ref = _get_normalized_token_data(**kwargs)
|
||||||
|
|
||||||
|
o = {'token': {'expires_at': '2099-05-22T00:02:43.941430Z',
|
||||||
|
'issued_at': '2013-05-21T00:02:43.941473Z',
|
||||||
|
'methods': ['password'],
|
||||||
|
'project': {'id': ref.get('project_id'),
|
||||||
|
'name': ref.get('project_name'),
|
||||||
|
'domain': {'id': ref.get('project_domain_id'),
|
||||||
|
'name': ref.get(
|
||||||
|
'project_domain_name')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'user': {'id': ref.get('user_id'),
|
||||||
|
'name': ref.get('username'),
|
||||||
|
'domain': {'id': ref.get('user_domain_id'),
|
||||||
|
'name': ref.get('user_domain_name')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'roles': ref.get('roles')
|
||||||
|
}}
|
||||||
|
|
||||||
|
# we only care about Neutron and Keystone endpoints
|
||||||
|
o['token']['catalog'] = [
|
||||||
|
{'endpoints': [
|
||||||
|
{
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'interface': 'public',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'url': 'public_' + ref.get('cinder_url')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'interface': 'internal',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'url': 'internal_' + ref.get('cinder_url')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'interface': 'admin',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'url': 'admin_' + ref.get('cinder_url')
|
||||||
|
}],
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'type': 'network'},
|
||||||
|
{'endpoints': [
|
||||||
|
{
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'interface': 'public',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'url': ref.get('auth_url')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'interface': 'admin',
|
||||||
|
'region': 'RegionOne',
|
||||||
|
'url': ref.get('auth_url')
|
||||||
|
}],
|
||||||
|
'id': uuid.uuid4().hex,
|
||||||
|
'type': 'identity'}]
|
||||||
|
|
||||||
|
# token ID is conveyed via the X-Subject-Token header so we are generating
|
||||||
|
# one to stash there
|
||||||
|
token_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
return token_id, o
|
||||||
|
|
||||||
|
|
||||||
|
def keystone_request_callback(request, uri, headers):
|
||||||
|
response_headers = {"content-type": "application/json"}
|
||||||
|
if uri == BASE_URL:
|
||||||
|
return (200, headers, V3_VERSION_LIST)
|
||||||
|
elif uri == BASE_URL + "/v2.0":
|
||||||
|
token_id, token_data = generate_v2_project_scoped_token()
|
||||||
|
return (200, response_headers, token_data)
|
||||||
|
elif uri == BASE_URL + "/v3":
|
||||||
|
token_id, token_data = generate_v3_project_scoped_token()
|
||||||
|
response_headers["X-Subject-Token"] = token_id
|
||||||
|
return (201, response_headers, token_data)
|
@ -22,6 +22,8 @@ from cinderclient import shell
|
|||||||
from cinderclient.v1 import shell as shell_v1
|
from cinderclient.v1 import shell as shell_v1
|
||||||
from cinderclient.tests.v1 import fakes
|
from cinderclient.tests.v1 import fakes
|
||||||
from cinderclient.tests import utils
|
from cinderclient.tests import utils
|
||||||
|
from cinderclient.tests.fixture_data import keystone_client
|
||||||
|
import httpretty
|
||||||
|
|
||||||
|
|
||||||
class ShellTest(utils.TestCase):
|
class ShellTest(utils.TestCase):
|
||||||
@ -31,7 +33,7 @@ class ShellTest(utils.TestCase):
|
|||||||
'CINDER_PASSWORD': 'password',
|
'CINDER_PASSWORD': 'password',
|
||||||
'CINDER_PROJECT_ID': 'project_id',
|
'CINDER_PROJECT_ID': 'project_id',
|
||||||
'OS_VOLUME_API_VERSION': '1',
|
'OS_VOLUME_API_VERSION': '1',
|
||||||
'CINDER_URL': 'http://no.where',
|
'CINDER_URL': keystone_client.BASE_URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Patch os.environ to avoid required auth info.
|
# Patch os.environ to avoid required auth info.
|
||||||
@ -44,7 +46,7 @@ class ShellTest(utils.TestCase):
|
|||||||
|
|
||||||
self.shell = shell.OpenStackCinderShell()
|
self.shell = shell.OpenStackCinderShell()
|
||||||
|
|
||||||
#HACK(bcwaldon): replace this when we start using stubs
|
# HACK(bcwaldon): replace this when we start using stubs
|
||||||
self.old_get_client_class = client.get_client_class
|
self.old_get_client_class = client.get_client_class
|
||||||
client.get_client_class = lambda *_: fakes.FakeClient
|
client.get_client_class = lambda *_: fakes.FakeClient
|
||||||
|
|
||||||
@ -56,7 +58,7 @@ class ShellTest(utils.TestCase):
|
|||||||
if hasattr(self.shell, 'cs'):
|
if hasattr(self.shell, 'cs'):
|
||||||
self.shell.cs.clear_callstack()
|
self.shell.cs.clear_callstack()
|
||||||
|
|
||||||
#HACK(bcwaldon): replace this when we start using stubs
|
# HACK(bcwaldon): replace this when we start using stubs
|
||||||
client.get_client_class = self.old_get_client_class
|
client.get_client_class = self.old_get_client_class
|
||||||
super(ShellTest, self).tearDown()
|
super(ShellTest, self).tearDown()
|
||||||
|
|
||||||
@ -69,9 +71,14 @@ class ShellTest(utils.TestCase):
|
|||||||
def assert_called_anytime(self, method, url, body=None):
|
def assert_called_anytime(self, method, url, body=None):
|
||||||
return self.shell.cs.assert_called_anytime(method, url, body)
|
return self.shell.cs.assert_called_anytime(method, url, body)
|
||||||
|
|
||||||
|
def register_keystone_auth_fixture(self):
|
||||||
|
httpretty.register_uri(httpretty.GET, keystone_client.BASE_URL,
|
||||||
|
body=keystone_client.keystone_request_callback)
|
||||||
|
|
||||||
def test_extract_metadata(self):
|
def test_extract_metadata(self):
|
||||||
# mimic the result of argparse's parse_args() method
|
# mimic the result of argparse's parse_args() method
|
||||||
class Arguments:
|
class Arguments:
|
||||||
|
|
||||||
def __init__(self, metadata=[]):
|
def __init__(self, metadata=[]):
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
|
|
||||||
@ -88,63 +95,91 @@ class ShellTest(utils.TestCase):
|
|||||||
args = Arguments(metadata=input[0])
|
args = Arguments(metadata=input[0])
|
||||||
self.assertEqual(shell_v1._extract_metadata(args), input[1])
|
self.assertEqual(shell_v1._extract_metadata(args), input[1])
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list')
|
self.run_command('list')
|
||||||
# NOTE(jdg): we default to detail currently
|
# NOTE(jdg): we default to detail currently
|
||||||
self.assert_called('GET', '/volumes/detail')
|
self.assert_called('GET', '/volumes/detail')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_filter_status(self):
|
def test_list_filter_status(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list --status=available')
|
self.run_command('list --status=available')
|
||||||
self.assert_called('GET', '/volumes/detail?status=available')
|
self.assert_called('GET', '/volumes/detail?status=available')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_filter_display_name(self):
|
def test_list_filter_display_name(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list --display-name=1234')
|
self.run_command('list --display-name=1234')
|
||||||
self.assert_called('GET', '/volumes/detail?display_name=1234')
|
self.assert_called('GET', '/volumes/detail?display_name=1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_all_tenants(self):
|
def test_list_all_tenants(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list --all-tenants=1')
|
self.run_command('list --all-tenants=1')
|
||||||
self.assert_called('GET', '/volumes/detail?all_tenants=1')
|
self.assert_called('GET', '/volumes/detail?all_tenants=1')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_availability_zone(self):
|
def test_list_availability_zone(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('availability-zone-list')
|
self.run_command('availability-zone-list')
|
||||||
self.assert_called('GET', '/os-availability-zone')
|
self.assert_called('GET', '/os-availability-zone')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_show(self):
|
def test_show(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('show 1234')
|
self.run_command('show 1234')
|
||||||
self.assert_called('GET', '/volumes/1234')
|
self.assert_called('GET', '/volumes/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('delete 1234')
|
self.run_command('delete 1234')
|
||||||
self.assert_called('DELETE', '/volumes/1234')
|
self.assert_called('DELETE', '/volumes/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_delete_by_name(self):
|
def test_delete_by_name(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('delete sample-volume')
|
self.run_command('delete sample-volume')
|
||||||
self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1')
|
self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1')
|
||||||
self.assert_called('DELETE', '/volumes/1234')
|
self.assert_called('DELETE', '/volumes/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_delete_multiple(self):
|
def test_delete_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('delete 1234 5678')
|
self.run_command('delete 1234 5678')
|
||||||
self.assert_called_anytime('DELETE', '/volumes/1234')
|
self.assert_called_anytime('DELETE', '/volumes/1234')
|
||||||
self.assert_called('DELETE', '/volumes/5678')
|
self.assert_called('DELETE', '/volumes/5678')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_backup(self):
|
def test_backup(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('backup-create 1234')
|
self.run_command('backup-create 1234')
|
||||||
self.assert_called('POST', '/backups')
|
self.assert_called('POST', '/backups')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_restore(self):
|
def test_restore(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('backup-restore 1234')
|
self.run_command('backup-restore 1234')
|
||||||
self.assert_called('POST', '/backups/1234/restore')
|
self.assert_called('POST', '/backups/1234/restore')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_list_filter_volume_id(self):
|
def test_snapshot_list_filter_volume_id(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-list --volume-id=1234')
|
self.run_command('snapshot-list --volume-id=1234')
|
||||||
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
|
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_list_filter_status_and_volume_id(self):
|
def test_snapshot_list_filter_status_and_volume_id(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-list --status=available --volume-id=1234')
|
self.run_command('snapshot-list --status=available --volume-id=1234')
|
||||||
self.assert_called('GET', '/snapshots/detail?'
|
self.assert_called('GET', '/snapshots/detail?'
|
||||||
'status=available&volume_id=1234')
|
'status=available&volume_id=1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_rename(self):
|
def test_rename(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
# basic rename with positional arguments
|
# basic rename with positional arguments
|
||||||
self.run_command('rename 1234 new-name')
|
self.run_command('rename 1234 new-name')
|
||||||
expected = {'volume': {'display_name': 'new-name'}}
|
expected = {'volume': {'display_name': 'new-name'}}
|
||||||
@ -165,7 +200,9 @@ class ShellTest(utils.TestCase):
|
|||||||
# Call rename with no arguments
|
# Call rename with no arguments
|
||||||
self.assertRaises(SystemExit, self.run_command, 'rename')
|
self.assertRaises(SystemExit, self.run_command, 'rename')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_rename_snapshot(self):
|
def test_rename_snapshot(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
# basic rename with positional arguments
|
# basic rename with positional arguments
|
||||||
self.run_command('snapshot-rename 1234 new-name')
|
self.run_command('snapshot-rename 1234 new-name')
|
||||||
expected = {'snapshot': {'display_name': 'new-name'}}
|
expected = {'snapshot': {'display_name': 'new-name'}}
|
||||||
@ -187,32 +224,44 @@ class ShellTest(utils.TestCase):
|
|||||||
# Call snapshot-rename with no arguments
|
# Call snapshot-rename with no arguments
|
||||||
self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
|
self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_set_metadata_set(self):
|
def test_set_metadata_set(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata 1234 set key1=val1 key2=val2')
|
self.run_command('metadata 1234 set key1=val1 key2=val2')
|
||||||
self.assert_called('POST', '/volumes/1234/metadata',
|
self.assert_called('POST', '/volumes/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_set_metadata_delete_dict(self):
|
def test_set_metadata_delete_dict(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata 1234 unset key1=val1 key2=val2')
|
self.run_command('metadata 1234 unset key1=val1 key2=val2')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_set_metadata_delete_keys(self):
|
def test_set_metadata_delete_keys(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata 1234 unset key1 key2')
|
self.run_command('metadata 1234 unset key1 key2')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_reset_state(self):
|
def test_reset_state(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('reset-state 1234')
|
self.run_command('reset-state 1234')
|
||||||
expected = {'os-reset_status': {'status': 'available'}}
|
expected = {'os-reset_status': {'status': 'available'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_reset_state_with_flag(self):
|
def test_reset_state_with_flag(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('reset-state --state error 1234')
|
self.run_command('reset-state --state error 1234')
|
||||||
expected = {'os-reset_status': {'status': 'error'}}
|
expected = {'os-reset_status': {'status': 'error'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_reset_state_multiple(self):
|
def test_reset_state_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('reset-state 1234 5678 --state error')
|
self.run_command('reset-state 1234 5678 --state error')
|
||||||
expected = {'os-reset_status': {'status': 'error'}}
|
expected = {'os-reset_status': {'status': 'error'}}
|
||||||
self.assert_called_anytime('POST', '/volumes/1234/action',
|
self.assert_called_anytime('POST', '/volumes/1234/action',
|
||||||
@ -220,17 +269,24 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime('POST', '/volumes/5678/action',
|
self.assert_called_anytime('POST', '/volumes/5678/action',
|
||||||
body=expected)
|
body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_reset_state(self):
|
def test_snapshot_reset_state(self):
|
||||||
|
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-reset-state 1234')
|
self.run_command('snapshot-reset-state 1234')
|
||||||
expected = {'os-reset_status': {'status': 'available'}}
|
expected = {'os-reset_status': {'status': 'available'}}
|
||||||
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_reset_state_with_flag(self):
|
def test_snapshot_reset_state_with_flag(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-reset-state --state error 1234')
|
self.run_command('snapshot-reset-state --state error 1234')
|
||||||
expected = {'os-reset_status': {'status': 'error'}}
|
expected = {'os-reset_status': {'status': 'error'}}
|
||||||
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_reset_state_multiple(self):
|
def test_snapshot_reset_state_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-reset-state 1234 5678')
|
self.run_command('snapshot-reset-state 1234 5678')
|
||||||
expected = {'os-reset_status': {'status': 'available'}}
|
expected = {'os-reset_status': {'status': 'available'}}
|
||||||
self.assert_called_anytime('POST', '/snapshots/1234/action',
|
self.assert_called_anytime('POST', '/snapshots/1234/action',
|
||||||
@ -238,6 +294,7 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime('POST', '/snapshots/5678/action',
|
self.assert_called_anytime('POST', '/snapshots/5678/action',
|
||||||
body=expected)
|
body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_list(self):
|
def test_encryption_type_list(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-list shell command.
|
Test encryption-type-list shell command.
|
||||||
@ -246,11 +303,13 @@ class ShellTest(utils.TestCase):
|
|||||||
- one to get the volume type list information
|
- one to get the volume type list information
|
||||||
- one per volume type to retrieve the encryption type information
|
- one per volume type to retrieve the encryption type information
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('encryption-type-list')
|
self.run_command('encryption-type-list')
|
||||||
self.assert_called_anytime('GET', '/types')
|
self.assert_called_anytime('GET', '/types')
|
||||||
self.assert_called_anytime('GET', '/types/1/encryption')
|
self.assert_called_anytime('GET', '/types/1/encryption')
|
||||||
self.assert_called_anytime('GET', '/types/2/encryption')
|
self.assert_called_anytime('GET', '/types/2/encryption')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_show(self):
|
def test_encryption_type_show(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-show shell command.
|
Test encryption-type-show shell command.
|
||||||
@ -259,10 +318,12 @@ class ShellTest(utils.TestCase):
|
|||||||
- one to get the volume type information
|
- one to get the volume type information
|
||||||
- one to get the encryption type information
|
- one to get the encryption type information
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('encryption-type-show 1')
|
self.run_command('encryption-type-show 1')
|
||||||
self.assert_called('GET', '/types/1/encryption')
|
self.assert_called('GET', '/types/1/encryption')
|
||||||
self.assert_called_anytime('GET', '/types/1')
|
self.assert_called_anytime('GET', '/types/1')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_create(self):
|
def test_encryption_type_create(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-create shell command.
|
Test encryption-type-create shell command.
|
||||||
@ -271,6 +332,7 @@ class ShellTest(utils.TestCase):
|
|||||||
- one GET request to retrieve the relevant volume type information
|
- one GET request to retrieve the relevant volume type information
|
||||||
- one POST request to create the new encryption type
|
- one POST request to create the new encryption type
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
expected = {'encryption': {'cipher': None, 'key_size': None,
|
expected = {'encryption': {'cipher': None, 'key_size': None,
|
||||||
'provider': 'TestProvider',
|
'provider': 'TestProvider',
|
||||||
'control_location': 'front-end'}}
|
'control_location': 'front-end'}}
|
||||||
@ -289,6 +351,7 @@ class ShellTest(utils.TestCase):
|
|||||||
"""
|
"""
|
||||||
self.skipTest("Not implemented")
|
self.skipTest("Not implemented")
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_delete(self):
|
def test_encryption_type_delete(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-delete shell command.
|
Test encryption-type-delete shell command.
|
||||||
@ -297,43 +360,58 @@ class ShellTest(utils.TestCase):
|
|||||||
- one GET request to retrieve the relevant volume type information
|
- one GET request to retrieve the relevant volume type information
|
||||||
- one DELETE request to delete the encryption type information
|
- one DELETE request to delete the encryption type information
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('encryption-type-delete 1')
|
self.run_command('encryption-type-delete 1')
|
||||||
self.assert_called('DELETE', '/types/1/encryption/provider')
|
self.assert_called('DELETE', '/types/1/encryption/provider')
|
||||||
self.assert_called_anytime('GET', '/types/1')
|
self.assert_called_anytime('GET', '/types/1')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_migrate_volume(self):
|
def test_migrate_volume(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('migrate 1234 fakehost --force-host-copy=True')
|
self.run_command('migrate 1234 fakehost --force-host-copy=True')
|
||||||
expected = {'os-migrate_volume': {'force_host_copy': 'True',
|
expected = {'os-migrate_volume': {'force_host_copy': 'True',
|
||||||
'host': 'fakehost'}}
|
'host': 'fakehost'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_set(self):
|
def test_snapshot_metadata_set(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
|
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
|
||||||
self.assert_called('POST', '/snapshots/1234/metadata',
|
self.assert_called('POST', '/snapshots/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_unset_dict(self):
|
def test_snapshot_metadata_unset_dict(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
|
self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_unset_keys(self):
|
def test_snapshot_metadata_unset_keys(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-metadata 1234 unset key1 key2')
|
self.run_command('snapshot-metadata 1234 unset key1 key2')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_volume_metadata_update_all(self):
|
def test_volume_metadata_update_all(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata-update-all 1234 key1=val1 key2=val2')
|
self.run_command('metadata-update-all 1234 key1=val1 key2=val2')
|
||||||
self.assert_called('PUT', '/volumes/1234/metadata',
|
self.assert_called('PUT', '/volumes/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_update_all(self):
|
def test_snapshot_metadata_update_all(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-metadata-update-all\
|
self.run_command('snapshot-metadata-update-all\
|
||||||
1234 key1=val1 key2=val2')
|
1234 key1=val1 key2=val2')
|
||||||
self.assert_called('PUT', '/snapshots/1234/metadata',
|
self.assert_called('PUT', '/snapshots/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_readonly_mode_update(self):
|
def test_readonly_mode_update(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('readonly-mode-update 1234 True')
|
self.run_command('readonly-mode-update 1234 True')
|
||||||
expected = {'os-update_readonly_flag': {'readonly': True}}
|
expected = {'os-update_readonly_flag': {'readonly': True}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
@ -342,31 +420,43 @@ class ShellTest(utils.TestCase):
|
|||||||
expected = {'os-update_readonly_flag': {'readonly': False}}
|
expected = {'os-update_readonly_flag': {'readonly': False}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_service_disable(self):
|
def test_service_disable(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('service-disable host cinder-volume')
|
self.run_command('service-disable host cinder-volume')
|
||||||
self.assert_called('PUT', '/os-services/disable',
|
self.assert_called('PUT', '/os-services/disable',
|
||||||
{"binary": "cinder-volume", "host": "host"})
|
{"binary": "cinder-volume", "host": "host"})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_services_disable_with_reason(self):
|
def test_services_disable_with_reason(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
cmd = 'service-disable host cinder-volume --reason no_reason'
|
cmd = 'service-disable host cinder-volume --reason no_reason'
|
||||||
self.run_command(cmd)
|
self.run_command(cmd)
|
||||||
body = {'host': 'host', 'binary': 'cinder-volume',
|
body = {'host': 'host', 'binary': 'cinder-volume',
|
||||||
'disabled_reason': 'no_reason'}
|
'disabled_reason': 'no_reason'}
|
||||||
self.assert_called('PUT', '/os-services/disable-log-reason', body)
|
self.assert_called('PUT', '/os-services/disable-log-reason', body)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_service_enable(self):
|
def test_service_enable(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('service-enable host cinder-volume')
|
self.run_command('service-enable host cinder-volume')
|
||||||
self.assert_called('PUT', '/os-services/enable',
|
self.assert_called('PUT', '/os-services/enable',
|
||||||
{"binary": "cinder-volume", "host": "host"})
|
{"binary": "cinder-volume", "host": "host"})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_delete(self):
|
def test_snapshot_delete(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-delete 1234')
|
self.run_command('snapshot-delete 1234')
|
||||||
self.assert_called('DELETE', '/snapshots/1234')
|
self.assert_called('DELETE', '/snapshots/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_quota_delete(self):
|
def test_quota_delete(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('quota-delete 1234')
|
self.run_command('quota-delete 1234')
|
||||||
self.assert_called('DELETE', '/os-quota-sets/1234')
|
self.assert_called('DELETE', '/os-quota-sets/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_delete_multiple(self):
|
def test_snapshot_delete_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-delete 1234 5678')
|
self.run_command('snapshot-delete 1234 5678')
|
||||||
self.assert_called('DELETE', '/snapshots/5678')
|
self.assert_called('DELETE', '/snapshots/5678')
|
||||||
|
@ -19,6 +19,8 @@ from cinderclient import client
|
|||||||
from cinderclient import shell
|
from cinderclient import shell
|
||||||
from cinderclient.tests import utils
|
from cinderclient.tests import utils
|
||||||
from cinderclient.tests.v2 import fakes
|
from cinderclient.tests.v2 import fakes
|
||||||
|
from cinderclient.tests.fixture_data import keystone_client
|
||||||
|
import httpretty
|
||||||
|
|
||||||
|
|
||||||
class ShellTest(utils.TestCase):
|
class ShellTest(utils.TestCase):
|
||||||
@ -28,7 +30,7 @@ class ShellTest(utils.TestCase):
|
|||||||
'CINDER_PASSWORD': 'password',
|
'CINDER_PASSWORD': 'password',
|
||||||
'CINDER_PROJECT_ID': 'project_id',
|
'CINDER_PROJECT_ID': 'project_id',
|
||||||
'OS_VOLUME_API_VERSION': '2',
|
'OS_VOLUME_API_VERSION': '2',
|
||||||
'CINDER_URL': 'http://no.where',
|
'CINDER_URL': keystone_client.BASE_URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Patch os.environ to avoid required auth info.
|
# Patch os.environ to avoid required auth info.
|
||||||
@ -41,7 +43,7 @@ class ShellTest(utils.TestCase):
|
|||||||
|
|
||||||
self.shell = shell.OpenStackCinderShell()
|
self.shell = shell.OpenStackCinderShell()
|
||||||
|
|
||||||
#HACK(bcwaldon): replace this when we start using stubs
|
# HACK(bcwaldon): replace this when we start using stubs
|
||||||
self.old_get_client_class = client.get_client_class
|
self.old_get_client_class = client.get_client_class
|
||||||
client.get_client_class = lambda *_: fakes.FakeClient
|
client.get_client_class = lambda *_: fakes.FakeClient
|
||||||
|
|
||||||
@ -53,10 +55,14 @@ class ShellTest(utils.TestCase):
|
|||||||
if hasattr(self.shell, 'cs'):
|
if hasattr(self.shell, 'cs'):
|
||||||
self.shell.cs.clear_callstack()
|
self.shell.cs.clear_callstack()
|
||||||
|
|
||||||
#HACK(bcwaldon): replace this when we start using stubs
|
# HACK(bcwaldon): replace this when we start using stubs
|
||||||
client.get_client_class = self.old_get_client_class
|
client.get_client_class = self.old_get_client_class
|
||||||
super(ShellTest, self).tearDown()
|
super(ShellTest, self).tearDown()
|
||||||
|
|
||||||
|
def register_keystone_auth_fixture(self):
|
||||||
|
httpretty.register_uri(httpretty.GET, keystone_client.BASE_URL,
|
||||||
|
body=keystone_client.keystone_request_callback)
|
||||||
|
|
||||||
def run_command(self, cmd):
|
def run_command(self, cmd):
|
||||||
self.shell.main(cmd.split())
|
self.shell.main(cmd.split())
|
||||||
|
|
||||||
@ -66,73 +72,105 @@ class ShellTest(utils.TestCase):
|
|||||||
def assert_called_anytime(self, method, url, body=None):
|
def assert_called_anytime(self, method, url, body=None):
|
||||||
return self.shell.cs.assert_called_anytime(method, url, body)
|
return self.shell.cs.assert_called_anytime(method, url, body)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list')
|
self.run_command('list')
|
||||||
# NOTE(jdg): we default to detail currently
|
# NOTE(jdg): we default to detail currently
|
||||||
self.assert_called('GET', '/volumes/detail')
|
self.assert_called('GET', '/volumes/detail')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_filter_status(self):
|
def test_list_filter_status(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list --status=available')
|
self.run_command('list --status=available')
|
||||||
self.assert_called('GET', '/volumes/detail?status=available')
|
self.assert_called('GET', '/volumes/detail?status=available')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_filter_name(self):
|
def test_list_filter_name(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list --name=1234')
|
self.run_command('list --name=1234')
|
||||||
self.assert_called('GET', '/volumes/detail?name=1234')
|
self.assert_called('GET', '/volumes/detail?name=1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_all_tenants(self):
|
def test_list_all_tenants(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('list --all-tenants=1')
|
self.run_command('list --all-tenants=1')
|
||||||
self.assert_called('GET', '/volumes/detail?all_tenants=1')
|
self.assert_called('GET', '/volumes/detail?all_tenants=1')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_list_availability_zone(self):
|
def test_list_availability_zone(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('availability-zone-list')
|
self.run_command('availability-zone-list')
|
||||||
self.assert_called('GET', '/os-availability-zone')
|
self.assert_called('GET', '/os-availability-zone')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_show(self):
|
def test_show(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('show 1234')
|
self.run_command('show 1234')
|
||||||
self.assert_called('GET', '/volumes/1234')
|
self.assert_called('GET', '/volumes/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('delete 1234')
|
self.run_command('delete 1234')
|
||||||
self.assert_called('DELETE', '/volumes/1234')
|
self.assert_called('DELETE', '/volumes/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_delete_by_name(self):
|
def test_delete_by_name(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('delete sample-volume')
|
self.run_command('delete sample-volume')
|
||||||
self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1')
|
self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1')
|
||||||
self.assert_called('DELETE', '/volumes/1234')
|
self.assert_called('DELETE', '/volumes/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_delete_multiple(self):
|
def test_delete_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('delete 1234 5678')
|
self.run_command('delete 1234 5678')
|
||||||
self.assert_called_anytime('DELETE', '/volumes/1234')
|
self.assert_called_anytime('DELETE', '/volumes/1234')
|
||||||
self.assert_called('DELETE', '/volumes/5678')
|
self.assert_called('DELETE', '/volumes/5678')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_backup(self):
|
def test_backup(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('backup-create 1234')
|
self.run_command('backup-create 1234')
|
||||||
self.assert_called('POST', '/backups')
|
self.assert_called('POST', '/backups')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_restore(self):
|
def test_restore(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('backup-restore 1234')
|
self.run_command('backup-restore 1234')
|
||||||
self.assert_called('POST', '/backups/1234/restore')
|
self.assert_called('POST', '/backups/1234/restore')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_record_export(self):
|
def test_record_export(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('backup-export 1234')
|
self.run_command('backup-export 1234')
|
||||||
self.assert_called('GET', '/backups/1234/export_record')
|
self.assert_called('GET', '/backups/1234/export_record')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_record_import(self):
|
def test_record_import(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('backup-import fake.driver URL_STRING')
|
self.run_command('backup-import fake.driver URL_STRING')
|
||||||
expected = {'backup-record': {'backup_service': 'fake.driver',
|
expected = {'backup-record': {'backup_service': 'fake.driver',
|
||||||
'backup_url': 'URL_STRING'}}
|
'backup_url': 'URL_STRING'}}
|
||||||
self.assert_called('POST', '/backups/import_record', expected)
|
self.assert_called('POST', '/backups/import_record', expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_list_filter_volume_id(self):
|
def test_snapshot_list_filter_volume_id(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-list --volume-id=1234')
|
self.run_command('snapshot-list --volume-id=1234')
|
||||||
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
|
self.assert_called('GET', '/snapshots/detail?volume_id=1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_list_filter_status_and_volume_id(self):
|
def test_snapshot_list_filter_status_and_volume_id(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-list --status=available --volume-id=1234')
|
self.run_command('snapshot-list --status=available --volume-id=1234')
|
||||||
self.assert_called('GET', '/snapshots/detail?'
|
self.assert_called('GET', '/snapshots/detail?'
|
||||||
'status=available&volume_id=1234')
|
'status=available&volume_id=1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_rename(self):
|
def test_rename(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
# basic rename with positional arguments
|
# basic rename with positional arguments
|
||||||
self.run_command('rename 1234 new-name')
|
self.run_command('rename 1234 new-name')
|
||||||
expected = {'volume': {'name': 'new-name'}}
|
expected = {'volume': {'name': 'new-name'}}
|
||||||
@ -153,7 +191,9 @@ class ShellTest(utils.TestCase):
|
|||||||
# Call rename with no arguments
|
# Call rename with no arguments
|
||||||
self.assertRaises(SystemExit, self.run_command, 'rename')
|
self.assertRaises(SystemExit, self.run_command, 'rename')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_rename_snapshot(self):
|
def test_rename_snapshot(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
# basic rename with positional arguments
|
# basic rename with positional arguments
|
||||||
self.run_command('snapshot-rename 1234 new-name')
|
self.run_command('snapshot-rename 1234 new-name')
|
||||||
expected = {'snapshot': {'name': 'new-name'}}
|
expected = {'snapshot': {'name': 'new-name'}}
|
||||||
@ -175,32 +215,44 @@ class ShellTest(utils.TestCase):
|
|||||||
# Call snapshot-rename with no arguments
|
# Call snapshot-rename with no arguments
|
||||||
self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
|
self.assertRaises(SystemExit, self.run_command, 'snapshot-rename')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_set_metadata_set(self):
|
def test_set_metadata_set(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata 1234 set key1=val1 key2=val2')
|
self.run_command('metadata 1234 set key1=val1 key2=val2')
|
||||||
self.assert_called('POST', '/volumes/1234/metadata',
|
self.assert_called('POST', '/volumes/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_set_metadata_delete_dict(self):
|
def test_set_metadata_delete_dict(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata 1234 unset key1=val1 key2=val2')
|
self.run_command('metadata 1234 unset key1=val1 key2=val2')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_set_metadata_delete_keys(self):
|
def test_set_metadata_delete_keys(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('metadata 1234 unset key1 key2')
|
self.run_command('metadata 1234 unset key1 key2')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
self.assert_called('DELETE', '/volumes/1234/metadata/key1')
|
||||||
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_reset_state(self):
|
def test_reset_state(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('reset-state 1234')
|
self.run_command('reset-state 1234')
|
||||||
expected = {'os-reset_status': {'status': 'available'}}
|
expected = {'os-reset_status': {'status': 'available'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_reset_state_with_flag(self):
|
def test_reset_state_with_flag(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('reset-state --state error 1234')
|
self.run_command('reset-state --state error 1234')
|
||||||
expected = {'os-reset_status': {'status': 'error'}}
|
expected = {'os-reset_status': {'status': 'error'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_reset_state_multiple(self):
|
def test_reset_state_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('reset-state 1234 5678 --state error')
|
self.run_command('reset-state 1234 5678 --state error')
|
||||||
expected = {'os-reset_status': {'status': 'error'}}
|
expected = {'os-reset_status': {'status': 'error'}}
|
||||||
self.assert_called_anytime('POST', '/volumes/1234/action',
|
self.assert_called_anytime('POST', '/volumes/1234/action',
|
||||||
@ -208,17 +260,23 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime('POST', '/volumes/5678/action',
|
self.assert_called_anytime('POST', '/volumes/5678/action',
|
||||||
body=expected)
|
body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_reset_state(self):
|
def test_snapshot_reset_state(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-reset-state 1234')
|
self.run_command('snapshot-reset-state 1234')
|
||||||
expected = {'os-reset_status': {'status': 'available'}}
|
expected = {'os-reset_status': {'status': 'available'}}
|
||||||
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_reset_state_with_flag(self):
|
def test_snapshot_reset_state_with_flag(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-reset-state --state error 1234')
|
self.run_command('snapshot-reset-state --state error 1234')
|
||||||
expected = {'os-reset_status': {'status': 'error'}}
|
expected = {'os-reset_status': {'status': 'error'}}
|
||||||
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
self.assert_called('POST', '/snapshots/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_reset_state_multiple(self):
|
def test_snapshot_reset_state_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-reset-state 1234 5678')
|
self.run_command('snapshot-reset-state 1234 5678')
|
||||||
expected = {'os-reset_status': {'status': 'available'}}
|
expected = {'os-reset_status': {'status': 'available'}}
|
||||||
self.assert_called_anytime('POST', '/snapshots/1234/action',
|
self.assert_called_anytime('POST', '/snapshots/1234/action',
|
||||||
@ -226,6 +284,7 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime('POST', '/snapshots/5678/action',
|
self.assert_called_anytime('POST', '/snapshots/5678/action',
|
||||||
body=expected)
|
body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_list(self):
|
def test_encryption_type_list(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-list shell command.
|
Test encryption-type-list shell command.
|
||||||
@ -234,11 +293,13 @@ class ShellTest(utils.TestCase):
|
|||||||
- one to get the volume type list information
|
- one to get the volume type list information
|
||||||
- one per volume type to retrieve the encryption type information
|
- one per volume type to retrieve the encryption type information
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('encryption-type-list')
|
self.run_command('encryption-type-list')
|
||||||
self.assert_called_anytime('GET', '/types')
|
self.assert_called_anytime('GET', '/types')
|
||||||
self.assert_called_anytime('GET', '/types/1/encryption')
|
self.assert_called_anytime('GET', '/types/1/encryption')
|
||||||
self.assert_called_anytime('GET', '/types/2/encryption')
|
self.assert_called_anytime('GET', '/types/2/encryption')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_show(self):
|
def test_encryption_type_show(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-show shell command.
|
Test encryption-type-show shell command.
|
||||||
@ -247,10 +308,12 @@ class ShellTest(utils.TestCase):
|
|||||||
- one to get the volume type information
|
- one to get the volume type information
|
||||||
- one to get the encryption type information
|
- one to get the encryption type information
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('encryption-type-show 1')
|
self.run_command('encryption-type-show 1')
|
||||||
self.assert_called('GET', '/types/1/encryption')
|
self.assert_called('GET', '/types/1/encryption')
|
||||||
self.assert_called_anytime('GET', '/types/1')
|
self.assert_called_anytime('GET', '/types/1')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_create(self):
|
def test_encryption_type_create(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-create shell command.
|
Test encryption-type-create shell command.
|
||||||
@ -259,6 +322,8 @@ class ShellTest(utils.TestCase):
|
|||||||
- one GET request to retrieve the relevant volume type information
|
- one GET request to retrieve the relevant volume type information
|
||||||
- one POST request to create the new encryption type
|
- one POST request to create the new encryption type
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
expected = {'encryption': {'cipher': None, 'key_size': None,
|
expected = {'encryption': {'cipher': None, 'key_size': None,
|
||||||
'provider': 'TestProvider',
|
'provider': 'TestProvider',
|
||||||
'control_location': 'front-end'}}
|
'control_location': 'front-end'}}
|
||||||
@ -277,6 +342,7 @@ class ShellTest(utils.TestCase):
|
|||||||
"""
|
"""
|
||||||
self.skipTest("Not implemented")
|
self.skipTest("Not implemented")
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_encryption_type_delete(self):
|
def test_encryption_type_delete(self):
|
||||||
"""
|
"""
|
||||||
Test encryption-type-delete shell command.
|
Test encryption-type-delete shell command.
|
||||||
@ -285,43 +351,65 @@ class ShellTest(utils.TestCase):
|
|||||||
- one GET request to retrieve the relevant volume type information
|
- one GET request to retrieve the relevant volume type information
|
||||||
- one DELETE request to delete the encryption type information
|
- one DELETE request to delete the encryption type information
|
||||||
"""
|
"""
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('encryption-type-delete 1')
|
self.run_command('encryption-type-delete 1')
|
||||||
self.assert_called('DELETE', '/types/1/encryption/provider')
|
self.assert_called('DELETE', '/types/1/encryption/provider')
|
||||||
self.assert_called_anytime('GET', '/types/1')
|
self.assert_called_anytime('GET', '/types/1')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_migrate_volume(self):
|
def test_migrate_volume(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('migrate 1234 fakehost --force-host-copy=True')
|
self.run_command('migrate 1234 fakehost --force-host-copy=True')
|
||||||
expected = {'os-migrate_volume': {'force_host_copy': 'True',
|
expected = {'os-migrate_volume': {'force_host_copy': 'True',
|
||||||
'host': 'fakehost'}}
|
'host': 'fakehost'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_set(self):
|
def test_snapshot_metadata_set(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
|
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
|
||||||
self.assert_called('POST', '/snapshots/1234/metadata',
|
self.assert_called('POST', '/snapshots/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_unset_dict(self):
|
def test_snapshot_metadata_unset_dict(self):
|
||||||
|
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
|
self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_unset_keys(self):
|
def test_snapshot_metadata_unset_keys(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('snapshot-metadata 1234 unset key1 key2')
|
self.run_command('snapshot-metadata 1234 unset key1 key2')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1')
|
||||||
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_volume_metadata_update_all(self):
|
def test_volume_metadata_update_all(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('metadata-update-all 1234 key1=val1 key2=val2')
|
self.run_command('metadata-update-all 1234 key1=val1 key2=val2')
|
||||||
self.assert_called('PUT', '/volumes/1234/metadata',
|
self.assert_called('PUT', '/volumes/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_metadata_update_all(self):
|
def test_snapshot_metadata_update_all(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
self.run_command('snapshot-metadata-update-all\
|
self.run_command('snapshot-metadata-update-all\
|
||||||
1234 key1=val1 key2=val2')
|
1234 key1=val1 key2=val2')
|
||||||
self.assert_called('PUT', '/snapshots/1234/metadata',
|
self.assert_called('PUT', '/snapshots/1234/metadata',
|
||||||
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_readonly_mode_update(self):
|
def test_readonly_mode_update(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('readonly-mode-update 1234 True')
|
self.run_command('readonly-mode-update 1234 True')
|
||||||
expected = {'os-update_readonly_flag': {'readonly': True}}
|
expected = {'os-update_readonly_flag': {'readonly': True}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
@ -330,43 +418,67 @@ class ShellTest(utils.TestCase):
|
|||||||
expected = {'os-update_readonly_flag': {'readonly': False}}
|
expected = {'os-update_readonly_flag': {'readonly': False}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_service_disable(self):
|
def test_service_disable(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('service-disable host cinder-volume')
|
self.run_command('service-disable host cinder-volume')
|
||||||
self.assert_called('PUT', '/os-services/disable',
|
self.assert_called('PUT', '/os-services/disable',
|
||||||
{"binary": "cinder-volume", "host": "host"})
|
{"binary": "cinder-volume", "host": "host"})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_services_disable_with_reason(self):
|
def test_services_disable_with_reason(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
cmd = 'service-disable host cinder-volume --reason no_reason'
|
cmd = 'service-disable host cinder-volume --reason no_reason'
|
||||||
self.run_command(cmd)
|
self.run_command(cmd)
|
||||||
body = {'host': 'host', 'binary': 'cinder-volume',
|
body = {'host': 'host', 'binary': 'cinder-volume',
|
||||||
'disabled_reason': 'no_reason'}
|
'disabled_reason': 'no_reason'}
|
||||||
self.assert_called('PUT', '/os-services/disable-log-reason', body)
|
self.assert_called('PUT', '/os-services/disable-log-reason', body)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_service_enable(self):
|
def test_service_enable(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('service-enable host cinder-volume')
|
self.run_command('service-enable host cinder-volume')
|
||||||
self.assert_called('PUT', '/os-services/enable',
|
self.assert_called('PUT', '/os-services/enable',
|
||||||
{"binary": "cinder-volume", "host": "host"})
|
{"binary": "cinder-volume", "host": "host"})
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_retype_with_policy(self):
|
def test_retype_with_policy(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('retype 1234 foo --migration-policy=on-demand')
|
self.run_command('retype 1234 foo --migration-policy=on-demand')
|
||||||
expected = {'os-retype': {'new_type': 'foo',
|
expected = {'os-retype': {'new_type': 'foo',
|
||||||
'migration_policy': 'on-demand'}}
|
'migration_policy': 'on-demand'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_retype_default_policy(self):
|
def test_retype_default_policy(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('retype 1234 foo')
|
self.run_command('retype 1234 foo')
|
||||||
expected = {'os-retype': {'new_type': 'foo',
|
expected = {'os-retype': {'new_type': 'foo',
|
||||||
'migration_policy': 'never'}}
|
'migration_policy': 'never'}}
|
||||||
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
self.assert_called('POST', '/volumes/1234/action', body=expected)
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_delete(self):
|
def test_snapshot_delete(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('snapshot-delete 1234')
|
self.run_command('snapshot-delete 1234')
|
||||||
self.assert_called('DELETE', '/snapshots/1234')
|
self.assert_called('DELETE', '/snapshots/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_quota_delete(self):
|
def test_quota_delete(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('quota-delete 1234')
|
self.run_command('quota-delete 1234')
|
||||||
self.assert_called('DELETE', '/os-quota-sets/1234')
|
self.assert_called('DELETE', '/os-quota-sets/1234')
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
def test_snapshot_delete_multiple(self):
|
def test_snapshot_delete_multiple(self):
|
||||||
|
self.register_keystone_auth_fixture()
|
||||||
|
|
||||||
self.run_command('snapshot-delete 5678')
|
self.run_command('snapshot-delete 5678')
|
||||||
self.assert_called('DELETE', '/snapshots/5678')
|
self.assert_called('DELETE', '/snapshots/5678')
|
||||||
|
@ -51,7 +51,7 @@ class Client(object):
|
|||||||
service_type='volume', service_name=None,
|
service_type='volume', service_name=None,
|
||||||
volume_service_name=None, retries=None,
|
volume_service_name=None, retries=None,
|
||||||
http_log_debug=False, cacert=None,
|
http_log_debug=False, cacert=None,
|
||||||
auth_system='keystone', auth_plugin=None):
|
auth_system='keystone', auth_plugin=None, session=None):
|
||||||
# FIXME(comstud): Rename the api_key argument above when we
|
# FIXME(comstud): Rename the api_key argument above when we
|
||||||
# know it's not being used as keyword argument
|
# know it's not being used as keyword argument
|
||||||
password = api_key
|
password = api_key
|
||||||
@ -80,16 +80,16 @@ class Client(object):
|
|||||||
setattr(self, extension.name,
|
setattr(self, extension.name,
|
||||||
extension.manager_class(self))
|
extension.manager_class(self))
|
||||||
|
|
||||||
self.client = client.HTTPClient(
|
self.client = client._construct_http_client(
|
||||||
username,
|
username=username,
|
||||||
password,
|
password=password,
|
||||||
project_id,
|
project_id=project_id,
|
||||||
auth_url,
|
auth_url=auth_url,
|
||||||
insecure=insecure,
|
insecure=insecure,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
|
proxy_tenant_id=tenant_id,
|
||||||
proxy_token=proxy_token,
|
proxy_token=proxy_token,
|
||||||
proxy_tenant_id=proxy_tenant_id,
|
|
||||||
region_name=region_name,
|
region_name=region_name,
|
||||||
endpoint_type=endpoint_type,
|
endpoint_type=endpoint_type,
|
||||||
service_type=service_type,
|
service_type=service_type,
|
||||||
@ -99,7 +99,8 @@ class Client(object):
|
|||||||
http_log_debug=http_log_debug,
|
http_log_debug=http_log_debug,
|
||||||
cacert=cacert,
|
cacert=cacert,
|
||||||
auth_system=auth_system,
|
auth_system=auth_system,
|
||||||
auth_plugin=auth_plugin)
|
auth_plugin=auth_plugin,
|
||||||
|
session=session)
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
"""
|
"""
|
||||||
|
@ -49,7 +49,8 @@ class Client(object):
|
|||||||
service_type='volumev2', service_name=None,
|
service_type='volumev2', service_name=None,
|
||||||
volume_service_name=None, retries=None,
|
volume_service_name=None, retries=None,
|
||||||
http_log_debug=False, cacert=None,
|
http_log_debug=False, cacert=None,
|
||||||
auth_system='keystone', auth_plugin=None):
|
auth_system='keystone', auth_plugin=None,
|
||||||
|
session=None):
|
||||||
# FIXME(comstud): Rename the api_key argument above when we
|
# FIXME(comstud): Rename the api_key argument above when we
|
||||||
# know it's not being used as keyword argument
|
# know it's not being used as keyword argument
|
||||||
password = api_key
|
password = api_key
|
||||||
@ -78,16 +79,16 @@ class Client(object):
|
|||||||
setattr(self, extension.name,
|
setattr(self, extension.name,
|
||||||
extension.manager_class(self))
|
extension.manager_class(self))
|
||||||
|
|
||||||
self.client = client.HTTPClient(
|
self.client = client._construct_http_client(
|
||||||
username,
|
username=username,
|
||||||
password,
|
password=password,
|
||||||
project_id,
|
project_id=project_id,
|
||||||
auth_url,
|
auth_url=auth_url,
|
||||||
insecure=insecure,
|
insecure=insecure,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
|
proxy_tenant_id=tenant_id,
|
||||||
proxy_token=proxy_token,
|
proxy_token=proxy_token,
|
||||||
proxy_tenant_id=proxy_tenant_id,
|
|
||||||
region_name=region_name,
|
region_name=region_name,
|
||||||
endpoint_type=endpoint_type,
|
endpoint_type=endpoint_type,
|
||||||
service_type=service_type,
|
service_type=service_type,
|
||||||
@ -97,7 +98,8 @@ class Client(object):
|
|||||||
http_log_debug=http_log_debug,
|
http_log_debug=http_log_debug,
|
||||||
cacert=cacert,
|
cacert=cacert,
|
||||||
auth_system=auth_system,
|
auth_system=auth_system,
|
||||||
auth_plugin=auth_plugin)
|
auth_plugin=auth_plugin,
|
||||||
|
session=session)
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
"""Authenticate against the server.
|
"""Authenticate against the server.
|
||||||
|
@ -4,6 +4,7 @@ coverage>=3.6
|
|||||||
discover
|
discover
|
||||||
fixtures>=0.3.14
|
fixtures>=0.3.14
|
||||||
mock>=1.0
|
mock>=1.0
|
||||||
|
httpretty>=0.8.0
|
||||||
python-subunit>=0.0.18
|
python-subunit>=0.0.18
|
||||||
sphinx>=1.2.1,<1.3
|
sphinx>=1.2.1,<1.3
|
||||||
testtools>=0.9.34
|
testtools>=0.9.34
|
||||||
|
Loading…
Reference in New Issue
Block a user