diff options
| author | haneef ali <haneef.ali@hp.com> | 2014-05-23 16:27:20 -0700 |
|---|---|---|
| committer | Haneef Ali <haneef.ali@hp.com> | 2014-06-17 13:10:32 -0700 |
| commit | e8e06ee2893cc93cb7bc261b77eec8f012e0f783 (patch) | |
| tree | 6f2e15505414cafb514a01e7454072043720bc70 | |
| parent | a87ee752851411bdfce09a4c1ebdfb03fd6a9b80 (diff) | |
| download | python-cinderclient-e8e06ee2893cc93cb7bc261b77eec8f012e0f783.tar.gz | |
Added support for keystone v3client
Change-Id: I7bbc74c9e73f36f942f5800a7af0da717da0bc64
| -rw-r--r-- | cinderclient/client.py | 187 | ||||
| -rw-r--r-- | cinderclient/shell.py | 398 | ||||
| -rw-r--r-- | cinderclient/tests/fixture_data/__init__.py | 0 | ||||
| -rw-r--r-- | cinderclient/tests/fixture_data/keystone_client.py | 218 | ||||
| -rw-r--r-- | cinderclient/tests/v1/test_shell.py | 96 | ||||
| -rw-r--r-- | cinderclient/tests/v2/test_shell.py | 118 | ||||
| -rw-r--r-- | cinderclient/v1/client.py | 17 | ||||
| -rw-r--r-- | cinderclient/v2/client.py | 18 | ||||
| -rw-r--r-- | test-requirements.txt | 1 |
9 files changed, 938 insertions, 115 deletions
diff --git a/cinderclient/client.py b/cinderclient/client.py index f0c31c9..f9f7e91 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -14,7 +14,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ OpenStack Client interface. Handles the REST calls and responses. """ @@ -23,6 +22,14 @@ from __future__ import print_function 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: import urlparse except ImportError: @@ -43,15 +50,117 @@ if not hasattr(urlparse, 'parse_qsl'): import cgi urlparse.parse_qsl = cgi.parse_qsl -import requests -from keystoneclient import access +class CinderClientMixin(object): -from cinderclient import exceptions -from cinderclient import utils + def get_volume_api_version_from_endpoint(self): + magic_tuple = urlparse.urlsplit(self.management_url) + scheme, netloc, path, query, frag = magic_tuple + components = path.split("/") + valid_versions = ['v1', 'v2'] + for version in valid_versions: + if version in components: + return version[1:] + msg = "Invalid client version '%s'. must be one of: %s" % ( + (version, ', '.join(valid_versions))) + raise exceptions.UnsupportedVersion(msg) + + +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 -class HTTPClient(object): + 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' @@ -385,17 +494,61 @@ class HTTPClient(object): return self._extract_service_catalog(url, resp, body) - def get_volume_api_version_from_endpoint(self): - magic_tuple = urlparse.urlsplit(self.management_url) - scheme, netloc, path, query, frag = magic_tuple - components = path.split("/") - valid_versions = ['v1', 'v2'] - for version in valid_versions: - if version in components: - return version[1:] - msg = "Invalid client version '%s'. must be one of: %s" % ( - (version, ', '.join(valid_versions))) - raise exceptions.UnsupportedVersion(msg) + +def _construct_http_client(username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, + region_name=None, endpoint_type='publicURL', + service_type='volume', + service_name=None, volume_service_name=None, + retries=None, + http_log_debug=False, + auth_system='keystone', auth_plugin=None, + 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): diff --git a/cinderclient/shell.py b/cinderclient/shell.py index e8d1e63..9c8d4d6 100644 --- a/cinderclient/shell.py +++ b/cinderclient/shell.py @@ -24,20 +24,27 @@ import argparse import glob import imp import itertools +import logging import os import pkgutil import sys -import logging -import cinderclient.auth_plugin from cinderclient import client from cinderclient import exceptions as exc +from cinderclient import utils +import cinderclient.auth_plugin import cinderclient.extension 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.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_CINDER_ENDPOINT_TYPE = 'publicURL' DEFAULT_CINDER_SERVICE_TYPE = 'volume' @@ -58,7 +65,7 @@ class CinderClientArgumentParser(argparse.ArgumentParser): exits. """ 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' progparts = self.prog.partition(' ') self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" @@ -117,60 +124,6 @@ class OpenStackCinderShell(object): default=False), help="Shows debugging output.") - parser.add_argument('--os-username', - metavar='<auth-user-name>', - default=utils.env('OS_USERNAME', - 'CINDER_USERNAME'), - help='OpenStack user name. ' - 'Default=env[OS_USERNAME].') - parser.add_argument('--os_username', - help=argparse.SUPPRESS) - - parser.add_argument('--os-password', - metavar='<auth-password>', - default=utils.env('OS_PASSWORD', - 'CINDER_PASSWORD'), - help='Password for OpenStack user. ' - 'Default=env[OS_PASSWORD].') - parser.add_argument('--os_password', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-name', - metavar='<auth-tenant-name>', - default=utils.env('OS_TENANT_NAME', - 'CINDER_PROJECT_ID'), - help='Tenant name. ' - 'Default=env[OS_TENANT_NAME].') - parser.add_argument('--os_tenant_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-id', - metavar='<auth-tenant-id>', - default=utils.env('OS_TENANT_ID', - 'CINDER_TENANT_ID'), - help='ID for the tenant. ' - 'Default=env[OS_TENANT_ID].') - parser.add_argument('--os_tenant_id', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-url', - metavar='<auth-url>', - default=utils.env('OS_AUTH_URL', - 'CINDER_URL'), - help='URL for the authentication service. ' - 'Default=env[OS_AUTH_URL].') - parser.add_argument('--os_auth_url', - help=argparse.SUPPRESS) - - parser.add_argument('--os-region-name', - metavar='<region-name>', - default=utils.env('OS_REGION_NAME', - 'CINDER_REGION_NAME'), - help='Region name. ' - 'Default=env[OS_REGION_NAME].') - parser.add_argument('--os_region_name', - help=argparse.SUPPRESS) - parser.add_argument('--os-auth-system', metavar='<auth-system>', default=utils.env('OS_AUTH_SYSTEM'), @@ -204,43 +157,34 @@ class OpenStackCinderShell(object): parser.add_argument('--endpoint-type', metavar='<endpoint-type>', default=utils.env('CINDER_ENDPOINT_TYPE', - default=DEFAULT_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), + 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', - default=utils.env('CINDERCLIENT_INSECURE', - default=False), - action='store_true', - 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 @@ -273,6 +217,188 @@ class OpenStackCinderShell(object): 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', + metavar='<auth-user-name>', + default=utils.env('OS_USERNAME', + 'CINDER_USERNAME'), + help='OpenStack user name. ' + 'Default=env[OS_USERNAME].') + parser.add_argument('--os_username', + help=argparse.SUPPRESS) + + parser.add_argument('--os-password', + metavar='<auth-password>', + default=utils.env('OS_PASSWORD', + 'CINDER_PASSWORD'), + help='Password for OpenStack user. ' + 'Default=env[OS_PASSWORD].') + parser.add_argument('--os_password', + help=argparse.SUPPRESS) + + parser.add_argument('--os-tenant-name', + metavar='<auth-tenant-name>', + default=utils.env('OS_TENANT_NAME', + 'CINDER_PROJECT_ID'), + help='Tenant name. ' + 'Default=env[OS_TENANT_NAME].') + parser.add_argument('--os_tenant_name', + help=argparse.SUPPRESS) + + parser.add_argument('--os-tenant-id', + metavar='<auth-tenant-id>', + default=utils.env('OS_TENANT_ID', + 'CINDER_TENANT_ID'), + help='ID for the tenant. ' + 'Default=env[OS_TENANT_ID].') + parser.add_argument('--os_tenant_id', + help=argparse.SUPPRESS) + + parser.add_argument('--os-auth-url', + metavar='<auth-url>', + default=utils.env('OS_AUTH_URL', + 'CINDER_URL'), + help='URL for the authentication service. ' + 'Default=env[OS_AUTH_URL].') + parser.add_argument('--os_auth_url', + 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', + metavar='<region-name>', + default=utils.env('OS_REGION_NAME', + 'CINDER_REGION_NAME'), + help='Region name. ' + 'Default=env[OS_REGION_NAME].') + parser.add_argument('--os_region_name', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-token', metavar='<token>', + default=utils.env('OS_TOKEN'), + help=_('Defaults to env[OS_TOKEN]')) + parser.add_argument( + '--os_token', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-url', metavar='<url>', + default=utils.env('OS_URL'), + help=_('Defaults to env[OS_URL]')) + parser.add_argument( + '--os_url', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-cacert', + metavar='<ca-certificate>', + default=utils.env('OS_CACERT', default=None), + help=_("Specify a CA bundle file to use in " + "verifying a TLS (https) server certificate. " + "Defaults to env[OS_CACERT]")) + + parser.add_argument('--insecure', + default=utils.env('CINDERCLIENT_INSECURE', + default=False), + action='store_true', + help=argparse.SUPPRESS) + def get_subcommand_parser(self, version): parser = self.get_base_parser() @@ -378,11 +504,13 @@ class OpenStackCinderShell(object): logger.addHandler(streamhandler) def main(self, argv): + # Parse args once to find version and debug settings parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) self.setup_debugging(options.debug) api_version_input = True + self.options = options if not options.os_volume_api_version: # Environment variable OS_VOLUME_API_VERSION was @@ -442,7 +570,7 @@ class OpenStackCinderShell(object): service_type = DEFAULT_CINDER_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. if not utils.isunauthenticated(args.func): @@ -474,6 +602,28 @@ class OpenStackCinderShell(object): else: 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 os_auth_system and os_auth_system != 'keystone': os_auth_url = auth_plugin.get_auth_url() @@ -499,6 +649,8 @@ class OpenStackCinderShell(object): "You must provide an authentication 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, os_password, os_tenant_name, os_auth_url, insecure, region_name=os_region_name, @@ -511,7 +663,8 @@ class OpenStackCinderShell(object): retries=options.retries, http_log_debug=args.debug, cacert=cacert, auth_system=os_auth_system, - auth_plugin=auth_plugin) + auth_plugin=auth_plugin, + session=auth_session) try: if not utils.isunauthenticated(args.func): @@ -588,9 +741,101 @@ class OpenStackCinderShell(object): else: 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. + + class OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) @@ -603,7 +848,7 @@ def main(): OpenStackCinderShell().main(sys.argv[1:]) else: OpenStackCinderShell().main(map(strutils.safe_decode, - sys.argv[1:])) + sys.argv[1:])) except KeyboardInterrupt: print("... terminating cinder client", file=sys.stderr) sys.exit(130) @@ -614,4 +859,5 @@ def main(): if __name__ == "__main__": + main() diff --git a/cinderclient/tests/fixture_data/__init__.py b/cinderclient/tests/fixture_data/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cinderclient/tests/fixture_data/__init__.py diff --git a/cinderclient/tests/fixture_data/keystone_client.py b/cinderclient/tests/fixture_data/keystone_client.py new file mode 100644 index 0000000..a7eb177 --- /dev/null +++ b/cinderclient/tests/fixture_data/keystone_client.py @@ -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) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index ff88c5e..0b9cbe7 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -22,6 +22,8 @@ from cinderclient import shell from cinderclient.v1 import shell as shell_v1 from cinderclient.tests.v1 import fakes from cinderclient.tests import utils +from cinderclient.tests.fixture_data import keystone_client +import httpretty class ShellTest(utils.TestCase): @@ -31,7 +33,7 @@ class ShellTest(utils.TestCase): 'CINDER_PASSWORD': 'password', 'CINDER_PROJECT_ID': 'project_id', 'OS_VOLUME_API_VERSION': '1', - 'CINDER_URL': 'http://no.where', + 'CINDER_URL': keystone_client.BASE_URL, } # Patch os.environ to avoid required auth info. @@ -44,7 +46,7 @@ class ShellTest(utils.TestCase): 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 client.get_client_class = lambda *_: fakes.FakeClient @@ -56,7 +58,7 @@ class ShellTest(utils.TestCase): if hasattr(self.shell, 'cs'): 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 super(ShellTest, self).tearDown() @@ -69,9 +71,14 @@ class ShellTest(utils.TestCase): def assert_called_anytime(self, method, url, body=None): 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): # mimic the result of argparse's parse_args() method class Arguments: + def __init__(self, metadata=[]): self.metadata = metadata @@ -88,63 +95,91 @@ class ShellTest(utils.TestCase): args = Arguments(metadata=input[0]) self.assertEqual(shell_v1._extract_metadata(args), input[1]) + @httpretty.activate def test_list(self): + self.register_keystone_auth_fixture() self.run_command('list') # NOTE(jdg): we default to detail currently self.assert_called('GET', '/volumes/detail') + @httpretty.activate def test_list_filter_status(self): + self.register_keystone_auth_fixture() self.run_command('list --status=available') self.assert_called('GET', '/volumes/detail?status=available') + @httpretty.activate def test_list_filter_display_name(self): + self.register_keystone_auth_fixture() self.run_command('list --display-name=1234') self.assert_called('GET', '/volumes/detail?display_name=1234') + @httpretty.activate def test_list_all_tenants(self): + self.register_keystone_auth_fixture() self.run_command('list --all-tenants=1') self.assert_called('GET', '/volumes/detail?all_tenants=1') + @httpretty.activate def test_list_availability_zone(self): + self.register_keystone_auth_fixture() self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone') + @httpretty.activate def test_show(self): + self.register_keystone_auth_fixture() self.run_command('show 1234') self.assert_called('GET', '/volumes/1234') + @httpretty.activate def test_delete(self): + self.register_keystone_auth_fixture() self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + @httpretty.activate def test_delete_by_name(self): + self.register_keystone_auth_fixture() self.run_command('delete sample-volume') self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1') self.assert_called('DELETE', '/volumes/1234') + @httpretty.activate def test_delete_multiple(self): + self.register_keystone_auth_fixture() self.run_command('delete 1234 5678') self.assert_called_anytime('DELETE', '/volumes/1234') self.assert_called('DELETE', '/volumes/5678') + @httpretty.activate def test_backup(self): + self.register_keystone_auth_fixture() self.run_command('backup-create 1234') self.assert_called('POST', '/backups') + @httpretty.activate def test_restore(self): + self.register_keystone_auth_fixture() self.run_command('backup-restore 1234') self.assert_called('POST', '/backups/1234/restore') + @httpretty.activate def test_snapshot_list_filter_volume_id(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-list --volume-id=1234') self.assert_called('GET', '/snapshots/detail?volume_id=1234') + @httpretty.activate 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.assert_called('GET', '/snapshots/detail?' 'status=available&volume_id=1234') + @httpretty.activate def test_rename(self): + self.register_keystone_auth_fixture() # basic rename with positional arguments self.run_command('rename 1234 new-name') expected = {'volume': {'display_name': 'new-name'}} @@ -165,7 +200,9 @@ class ShellTest(utils.TestCase): # Call rename with no arguments self.assertRaises(SystemExit, self.run_command, 'rename') + @httpretty.activate def test_rename_snapshot(self): + self.register_keystone_auth_fixture() # basic rename with positional arguments self.run_command('snapshot-rename 1234 new-name') expected = {'snapshot': {'display_name': 'new-name'}} @@ -187,32 +224,44 @@ class ShellTest(utils.TestCase): # Call snapshot-rename with no arguments self.assertRaises(SystemExit, self.run_command, 'snapshot-rename') + @httpretty.activate def test_set_metadata_set(self): + self.register_keystone_auth_fixture() self.run_command('metadata 1234 set key1=val1 key2=val2') self.assert_called('POST', '/volumes/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_set_metadata_delete_dict(self): + self.register_keystone_auth_fixture() self.run_command('metadata 1234 unset key1=val1 key2=val2') self.assert_called('DELETE', '/volumes/1234/metadata/key1') self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2) + @httpretty.activate def test_set_metadata_delete_keys(self): + self.register_keystone_auth_fixture() self.run_command('metadata 1234 unset key1 key2') self.assert_called('DELETE', '/volumes/1234/metadata/key1') self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2) + @httpretty.activate def test_reset_state(self): + self.register_keystone_auth_fixture() self.run_command('reset-state 1234') expected = {'os-reset_status': {'status': 'available'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_reset_state_with_flag(self): + self.register_keystone_auth_fixture() self.run_command('reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_reset_state_multiple(self): + self.register_keystone_auth_fixture() self.run_command('reset-state 1234 5678 --state error') expected = {'os-reset_status': {'status': 'error'}} self.assert_called_anytime('POST', '/volumes/1234/action', @@ -220,17 +269,24 @@ class ShellTest(utils.TestCase): self.assert_called_anytime('POST', '/volumes/5678/action', body=expected) + @httpretty.activate def test_snapshot_reset_state(self): + + self.register_keystone_auth_fixture() self.run_command('snapshot-reset-state 1234') expected = {'os-reset_status': {'status': 'available'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + @httpretty.activate def test_snapshot_reset_state_with_flag(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + @httpretty.activate def test_snapshot_reset_state_multiple(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-reset-state 1234 5678') expected = {'os-reset_status': {'status': 'available'}} self.assert_called_anytime('POST', '/snapshots/1234/action', @@ -238,6 +294,7 @@ class ShellTest(utils.TestCase): self.assert_called_anytime('POST', '/snapshots/5678/action', body=expected) + @httpretty.activate def test_encryption_type_list(self): """ Test encryption-type-list shell command. @@ -246,11 +303,13 @@ class ShellTest(utils.TestCase): - one to get the volume type list information - one per volume type to retrieve the encryption type information """ + self.register_keystone_auth_fixture() self.run_command('encryption-type-list') self.assert_called_anytime('GET', '/types') self.assert_called_anytime('GET', '/types/1/encryption') self.assert_called_anytime('GET', '/types/2/encryption') + @httpretty.activate def test_encryption_type_show(self): """ 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 encryption type information """ + self.register_keystone_auth_fixture() self.run_command('encryption-type-show 1') self.assert_called('GET', '/types/1/encryption') self.assert_called_anytime('GET', '/types/1') + @httpretty.activate def test_encryption_type_create(self): """ 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 POST request to create the new encryption type """ + self.register_keystone_auth_fixture() expected = {'encryption': {'cipher': None, 'key_size': None, 'provider': 'TestProvider', 'control_location': 'front-end'}} @@ -289,6 +351,7 @@ class ShellTest(utils.TestCase): """ self.skipTest("Not implemented") + @httpretty.activate def test_encryption_type_delete(self): """ 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 DELETE request to delete the encryption type information """ + self.register_keystone_auth_fixture() self.run_command('encryption-type-delete 1') self.assert_called('DELETE', '/types/1/encryption/provider') self.assert_called_anytime('GET', '/types/1') + @httpretty.activate def test_migrate_volume(self): + self.register_keystone_auth_fixture() self.run_command('migrate 1234 fakehost --force-host-copy=True') expected = {'os-migrate_volume': {'force_host_copy': 'True', 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_snapshot_metadata_set(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2') self.assert_called('POST', '/snapshots/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_snapshot_metadata_unset_dict(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') + @httpretty.activate def test_snapshot_metadata_unset_keys(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-metadata 1234 unset key1 key2') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') + @httpretty.activate def test_volume_metadata_update_all(self): + self.register_keystone_auth_fixture() self.run_command('metadata-update-all 1234 key1=val1 key2=val2') self.assert_called('PUT', '/volumes/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_snapshot_metadata_update_all(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-metadata-update-all\ 1234 key1=val1 key2=val2') self.assert_called('PUT', '/snapshots/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_readonly_mode_update(self): + self.register_keystone_auth_fixture() self.run_command('readonly-mode-update 1234 True') expected = {'os-update_readonly_flag': {'readonly': True}} self.assert_called('POST', '/volumes/1234/action', body=expected) @@ -342,31 +420,43 @@ class ShellTest(utils.TestCase): expected = {'os-update_readonly_flag': {'readonly': False}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_service_disable(self): + self.register_keystone_auth_fixture() self.run_command('service-disable host cinder-volume') self.assert_called('PUT', '/os-services/disable', {"binary": "cinder-volume", "host": "host"}) + @httpretty.activate def test_services_disable_with_reason(self): + self.register_keystone_auth_fixture() cmd = 'service-disable host cinder-volume --reason no_reason' self.run_command(cmd) body = {'host': 'host', 'binary': 'cinder-volume', 'disabled_reason': 'no_reason'} self.assert_called('PUT', '/os-services/disable-log-reason', body) + @httpretty.activate def test_service_enable(self): + self.register_keystone_auth_fixture() self.run_command('service-enable host cinder-volume') self.assert_called('PUT', '/os-services/enable', {"binary": "cinder-volume", "host": "host"}) + @httpretty.activate def test_snapshot_delete(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-delete 1234') self.assert_called('DELETE', '/snapshots/1234') + @httpretty.activate def test_quota_delete(self): + self.register_keystone_auth_fixture() self.run_command('quota-delete 1234') self.assert_called('DELETE', '/os-quota-sets/1234') + @httpretty.activate def test_snapshot_delete_multiple(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-delete 1234 5678') self.assert_called('DELETE', '/snapshots/5678') diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index e33d55e..a52bd10 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -19,6 +19,8 @@ from cinderclient import client from cinderclient import shell from cinderclient.tests import utils from cinderclient.tests.v2 import fakes +from cinderclient.tests.fixture_data import keystone_client +import httpretty class ShellTest(utils.TestCase): @@ -28,7 +30,7 @@ class ShellTest(utils.TestCase): 'CINDER_PASSWORD': 'password', 'CINDER_PROJECT_ID': 'project_id', 'OS_VOLUME_API_VERSION': '2', - 'CINDER_URL': 'http://no.where', + 'CINDER_URL': keystone_client.BASE_URL, } # Patch os.environ to avoid required auth info. @@ -41,7 +43,7 @@ class ShellTest(utils.TestCase): 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 client.get_client_class = lambda *_: fakes.FakeClient @@ -53,10 +55,14 @@ class ShellTest(utils.TestCase): if hasattr(self.shell, 'cs'): 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 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): self.shell.main(cmd.split()) @@ -66,73 +72,105 @@ class ShellTest(utils.TestCase): def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) + @httpretty.activate def test_list(self): + self.register_keystone_auth_fixture() self.run_command('list') # NOTE(jdg): we default to detail currently self.assert_called('GET', '/volumes/detail') + @httpretty.activate def test_list_filter_status(self): + self.register_keystone_auth_fixture() self.run_command('list --status=available') self.assert_called('GET', '/volumes/detail?status=available') + @httpretty.activate def test_list_filter_name(self): + self.register_keystone_auth_fixture() self.run_command('list --name=1234') self.assert_called('GET', '/volumes/detail?name=1234') + @httpretty.activate def test_list_all_tenants(self): + self.register_keystone_auth_fixture() self.run_command('list --all-tenants=1') self.assert_called('GET', '/volumes/detail?all_tenants=1') + @httpretty.activate def test_list_availability_zone(self): + self.register_keystone_auth_fixture() self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone') + @httpretty.activate def test_show(self): + self.register_keystone_auth_fixture() self.run_command('show 1234') self.assert_called('GET', '/volumes/1234') + @httpretty.activate def test_delete(self): + self.register_keystone_auth_fixture() self.run_command('delete 1234') self.assert_called('DELETE', '/volumes/1234') + @httpretty.activate def test_delete_by_name(self): + self.register_keystone_auth_fixture() self.run_command('delete sample-volume') self.assert_called_anytime('GET', '/volumes/detail?all_tenants=1') self.assert_called('DELETE', '/volumes/1234') + @httpretty.activate def test_delete_multiple(self): + self.register_keystone_auth_fixture() self.run_command('delete 1234 5678') self.assert_called_anytime('DELETE', '/volumes/1234') self.assert_called('DELETE', '/volumes/5678') + @httpretty.activate def test_backup(self): + self.register_keystone_auth_fixture() self.run_command('backup-create 1234') self.assert_called('POST', '/backups') + @httpretty.activate def test_restore(self): + self.register_keystone_auth_fixture() self.run_command('backup-restore 1234') self.assert_called('POST', '/backups/1234/restore') + @httpretty.activate def test_record_export(self): + self.register_keystone_auth_fixture() self.run_command('backup-export 1234') self.assert_called('GET', '/backups/1234/export_record') + @httpretty.activate def test_record_import(self): + self.register_keystone_auth_fixture() self.run_command('backup-import fake.driver URL_STRING') expected = {'backup-record': {'backup_service': 'fake.driver', 'backup_url': 'URL_STRING'}} self.assert_called('POST', '/backups/import_record', expected) + @httpretty.activate def test_snapshot_list_filter_volume_id(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-list --volume-id=1234') self.assert_called('GET', '/snapshots/detail?volume_id=1234') + @httpretty.activate 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.assert_called('GET', '/snapshots/detail?' 'status=available&volume_id=1234') + @httpretty.activate def test_rename(self): + self.register_keystone_auth_fixture() # basic rename with positional arguments self.run_command('rename 1234 new-name') expected = {'volume': {'name': 'new-name'}} @@ -153,7 +191,9 @@ class ShellTest(utils.TestCase): # Call rename with no arguments self.assertRaises(SystemExit, self.run_command, 'rename') + @httpretty.activate def test_rename_snapshot(self): + self.register_keystone_auth_fixture() # basic rename with positional arguments self.run_command('snapshot-rename 1234 new-name') expected = {'snapshot': {'name': 'new-name'}} @@ -175,32 +215,44 @@ class ShellTest(utils.TestCase): # Call snapshot-rename with no arguments self.assertRaises(SystemExit, self.run_command, 'snapshot-rename') + @httpretty.activate def test_set_metadata_set(self): + self.register_keystone_auth_fixture() self.run_command('metadata 1234 set key1=val1 key2=val2') self.assert_called('POST', '/volumes/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_set_metadata_delete_dict(self): + self.register_keystone_auth_fixture() self.run_command('metadata 1234 unset key1=val1 key2=val2') self.assert_called('DELETE', '/volumes/1234/metadata/key1') self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2) + @httpretty.activate def test_set_metadata_delete_keys(self): + self.register_keystone_auth_fixture() self.run_command('metadata 1234 unset key1 key2') self.assert_called('DELETE', '/volumes/1234/metadata/key1') self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2) + @httpretty.activate def test_reset_state(self): + self.register_keystone_auth_fixture() self.run_command('reset-state 1234') expected = {'os-reset_status': {'status': 'available'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_reset_state_with_flag(self): + self.register_keystone_auth_fixture() self.run_command('reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_reset_state_multiple(self): + self.register_keystone_auth_fixture() self.run_command('reset-state 1234 5678 --state error') expected = {'os-reset_status': {'status': 'error'}} self.assert_called_anytime('POST', '/volumes/1234/action', @@ -208,17 +260,23 @@ class ShellTest(utils.TestCase): self.assert_called_anytime('POST', '/volumes/5678/action', body=expected) + @httpretty.activate def test_snapshot_reset_state(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-reset-state 1234') expected = {'os-reset_status': {'status': 'available'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + @httpretty.activate def test_snapshot_reset_state_with_flag(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-reset-state --state error 1234') expected = {'os-reset_status': {'status': 'error'}} self.assert_called('POST', '/snapshots/1234/action', body=expected) + @httpretty.activate def test_snapshot_reset_state_multiple(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-reset-state 1234 5678') expected = {'os-reset_status': {'status': 'available'}} self.assert_called_anytime('POST', '/snapshots/1234/action', @@ -226,6 +284,7 @@ class ShellTest(utils.TestCase): self.assert_called_anytime('POST', '/snapshots/5678/action', body=expected) + @httpretty.activate def test_encryption_type_list(self): """ Test encryption-type-list shell command. @@ -234,11 +293,13 @@ class ShellTest(utils.TestCase): - one to get the volume type list information - one per volume type to retrieve the encryption type information """ + self.register_keystone_auth_fixture() self.run_command('encryption-type-list') self.assert_called_anytime('GET', '/types') self.assert_called_anytime('GET', '/types/1/encryption') self.assert_called_anytime('GET', '/types/2/encryption') + @httpretty.activate def test_encryption_type_show(self): """ 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 encryption type information """ + self.register_keystone_auth_fixture() self.run_command('encryption-type-show 1') self.assert_called('GET', '/types/1/encryption') self.assert_called_anytime('GET', '/types/1') + @httpretty.activate def test_encryption_type_create(self): """ 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 POST request to create the new encryption type """ + + self.register_keystone_auth_fixture() expected = {'encryption': {'cipher': None, 'key_size': None, 'provider': 'TestProvider', 'control_location': 'front-end'}} @@ -277,6 +342,7 @@ class ShellTest(utils.TestCase): """ self.skipTest("Not implemented") + @httpretty.activate def test_encryption_type_delete(self): """ 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 DELETE request to delete the encryption type information """ + self.register_keystone_auth_fixture() self.run_command('encryption-type-delete 1') self.assert_called('DELETE', '/types/1/encryption/provider') self.assert_called_anytime('GET', '/types/1') + @httpretty.activate def test_migrate_volume(self): + self.register_keystone_auth_fixture() + self.run_command('migrate 1234 fakehost --force-host-copy=True') expected = {'os-migrate_volume': {'force_host_copy': 'True', 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_snapshot_metadata_set(self): + self.register_keystone_auth_fixture() + self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2') self.assert_called('POST', '/snapshots/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_snapshot_metadata_unset_dict(self): + + self.register_keystone_auth_fixture() + self.run_command('snapshot-metadata 1234 unset key1=val1 key2=val2') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') + @httpretty.activate def test_snapshot_metadata_unset_keys(self): + self.register_keystone_auth_fixture() + self.run_command('snapshot-metadata 1234 unset key1 key2') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key1') self.assert_called_anytime('DELETE', '/snapshots/1234/metadata/key2') + @httpretty.activate def test_volume_metadata_update_all(self): + self.register_keystone_auth_fixture() + self.run_command('metadata-update-all 1234 key1=val1 key2=val2') self.assert_called('PUT', '/volumes/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_snapshot_metadata_update_all(self): + self.register_keystone_auth_fixture() self.run_command('snapshot-metadata-update-all\ 1234 key1=val1 key2=val2') self.assert_called('PUT', '/snapshots/1234/metadata', {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + @httpretty.activate def test_readonly_mode_update(self): + self.register_keystone_auth_fixture() + self.run_command('readonly-mode-update 1234 True') expected = {'os-update_readonly_flag': {'readonly': True}} self.assert_called('POST', '/volumes/1234/action', body=expected) @@ -330,43 +418,67 @@ class ShellTest(utils.TestCase): expected = {'os-update_readonly_flag': {'readonly': False}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_service_disable(self): + self.register_keystone_auth_fixture() + self.run_command('service-disable host cinder-volume') self.assert_called('PUT', '/os-services/disable', {"binary": "cinder-volume", "host": "host"}) + @httpretty.activate def test_services_disable_with_reason(self): + self.register_keystone_auth_fixture() + cmd = 'service-disable host cinder-volume --reason no_reason' self.run_command(cmd) body = {'host': 'host', 'binary': 'cinder-volume', 'disabled_reason': 'no_reason'} self.assert_called('PUT', '/os-services/disable-log-reason', body) + @httpretty.activate def test_service_enable(self): + self.register_keystone_auth_fixture() + self.run_command('service-enable host cinder-volume') self.assert_called('PUT', '/os-services/enable', {"binary": "cinder-volume", "host": "host"}) + @httpretty.activate def test_retype_with_policy(self): + self.register_keystone_auth_fixture() + self.run_command('retype 1234 foo --migration-policy=on-demand') expected = {'os-retype': {'new_type': 'foo', 'migration_policy': 'on-demand'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_retype_default_policy(self): + self.register_keystone_auth_fixture() + self.run_command('retype 1234 foo') expected = {'os-retype': {'new_type': 'foo', 'migration_policy': 'never'}} self.assert_called('POST', '/volumes/1234/action', body=expected) + @httpretty.activate def test_snapshot_delete(self): + self.register_keystone_auth_fixture() + self.run_command('snapshot-delete 1234') self.assert_called('DELETE', '/snapshots/1234') + @httpretty.activate def test_quota_delete(self): + self.register_keystone_auth_fixture() + self.run_command('quota-delete 1234') self.assert_called('DELETE', '/os-quota-sets/1234') + @httpretty.activate def test_snapshot_delete_multiple(self): + self.register_keystone_auth_fixture() + self.run_command('snapshot-delete 5678') self.assert_called('DELETE', '/snapshots/5678') diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index c352927..9b375b8 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -51,7 +51,7 @@ class Client(object): service_type='volume', service_name=None, volume_service_name=None, retries=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 # know it's not being used as keyword argument password = api_key @@ -80,16 +80,16 @@ class Client(object): setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient( - username, - password, - project_id, - auth_url, + self.client = client._construct_http_client( + username=username, + password=password, + project_id=project_id, + auth_url=auth_url, insecure=insecure, timeout=timeout, tenant_id=tenant_id, + proxy_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, @@ -99,7 +99,8 @@ class Client(object): http_log_debug=http_log_debug, cacert=cacert, auth_system=auth_system, - auth_plugin=auth_plugin) + auth_plugin=auth_plugin, + session=session) def authenticate(self): """ diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index ef5b5d1..a4016db 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -49,7 +49,8 @@ class Client(object): service_type='volumev2', service_name=None, volume_service_name=None, retries=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 # know it's not being used as keyword argument password = api_key @@ -78,16 +79,16 @@ class Client(object): setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient( - username, - password, - project_id, - auth_url, + self.client = client._construct_http_client( + username=username, + password=password, + project_id=project_id, + auth_url=auth_url, insecure=insecure, timeout=timeout, tenant_id=tenant_id, + proxy_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, @@ -97,7 +98,8 @@ class Client(object): http_log_debug=http_log_debug, cacert=cacert, auth_system=auth_system, - auth_plugin=auth_plugin) + auth_plugin=auth_plugin, + session=session) def authenticate(self): """Authenticate against the server. diff --git a/test-requirements.txt b/test-requirements.txt index a936aa8..bce44f6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 +httpretty>=0.8.0 python-subunit>=0.0.18 sphinx>=1.2.1,<1.3 testtools>=0.9.34 |
