diff --git a/glanceclient/shell.py b/glanceclient/shell.py index 43919b09..c066f538 100644 --- a/glanceclient/shell.py +++ b/glanceclient/shell.py @@ -27,53 +27,32 @@ import os from os.path import expanduser import sys -from keystoneclient.v2_0 import client as ksclient import netaddr +import six.moves.urllib.parse as urlparse import glanceclient from glanceclient.common import utils from glanceclient import exc from glanceclient.openstack.common import strutils +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import discover +from keystoneclient.openstack.common.apiclient import exceptions as ks_exc +from keystoneclient import session + class OpenStackImagesShell(object): - def get_base_parser(self): - parser = argparse.ArgumentParser( - prog='glance', - description=__doc__.strip(), - epilog='See "glance help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=HelpFormatter, - ) - - # Global arguments - parser.add_argument('-h', '--help', - action='store_true', - help=argparse.SUPPRESS, - ) - - parser.add_argument('--version', - action='version', - version=glanceclient.__version__) - - parser.add_argument('-d', '--debug', - default=bool(utils.env('GLANCECLIENT_DEBUG')), - action='store_true', - help='Defaults to env[GLANCECLIENT_DEBUG].') - - parser.add_argument('-v', '--verbose', - default=False, action="store_true", - help="Print more verbose output") - - parser.add_argument('--get-schema', - default=False, action="store_true", - dest='get_schema', - help='Ignores cached copy and forces retrieval ' - 'of schema that generates portions of the ' - 'help text. Ignored with API version 1.') - + def _append_global_identity_args(self, parser): + # FIXME(bobt): 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 avaible in + # python-keystoneclient. See + # + # https://bugs.launchpad.net/python-keystoneclient/+bug/1332337 + # parser.add_argument('-k', '--insecure', default=False, action='store_true', @@ -83,16 +62,24 @@ class OpenStackImagesShell(object): 'certificate authorities. This option should ' 'be used with caution.') - parser.add_argument('--cert-file', + parser.add_argument('--os-cert', help='Path of certificate file to use in SSL ' 'connection. This file can optionally be ' 'prepended with the private key.') - parser.add_argument('--key-file', + parser.add_argument('--cert-file', + dest='os_cert', + help='DEPRECATED! Use --os-cert.') + + parser.add_argument('--os-key', help='Path of client key to use in SSL ' 'connection. This option is not necessary ' 'if your key is prepended to your cert file.') + parser.add_argument('--key-file', + dest='os_key', + help='DEPRECATED! Use --os-key.') + parser.add_argument('--os-cacert', metavar='', dest='os_cacert', @@ -106,51 +93,6 @@ class OpenStackImagesShell(object): dest='os_cacert', help='DEPRECATED! Use --os-cacert.') - parser.add_argument('--timeout', - default=600, - help='Number of seconds to wait for a response') - - parser.add_argument('--no-ssl-compression', - dest='ssl_compression', - default=True, action='store_false', - help='Disable SSL compression when using https.') - - parser.add_argument('-f', '--force', - dest='force', - default=False, action='store_true', - help='Prevent select actions from requesting ' - 'user confirmation.') - - #NOTE(bcwaldon): DEPRECATED - parser.add_argument('--dry-run', - default=False, - action='store_true', - help='DEPRECATED! Only used for deprecated ' - 'legacy commands.') - - #NOTE(bcwaldon): DEPRECATED - parser.add_argument('--ssl', - dest='use_ssl', - default=False, - action='store_true', - help='DEPRECATED! Send a fully-formed endpoint ' - 'using --os-image-url instead.') - - #NOTE(bcwaldon): DEPRECATED - parser.add_argument('-H', '--host', - metavar='ADDRESS', - help='DEPRECATED! Send a fully-formed endpoint ' - 'using --os-image-url instead.') - - #NOTE(bcwaldon): DEPRECATED - parser.add_argument('-p', '--port', - dest='port', - metavar='PORT', - type=int, - default=9292, - help='DEPRECATED! Send a fully-formed endpoint ' - 'using --os-image-url instead.') - parser.add_argument('--os-username', default=utils.env('OS_USERNAME'), help='Defaults to env[OS_USERNAME].') @@ -158,6 +100,40 @@ class OpenStackImagesShell(object): parser.add_argument('--os_username', help=argparse.SUPPRESS) + parser.add_argument('--os-user-id', + default=utils.env('OS_USER_ID'), + help='Defaults to env[OS_USER_ID].') + + parser.add_argument('--os-user-domain-id', + default=utils.env('OS_USER_DOMAIN_ID'), + help='Defaults to env[OS_USER_DOMAIN_ID].') + + parser.add_argument('--os-user-domain-name', + default=utils.env('OS_USER_DOMAIN_NAME'), + help='Defaults to env[OS_USER_DOMAIN_NAME].') + + parser.add_argument('--os-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-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-domain-id', + default=utils.env('OS_PROJECT_DOMAIN_ID'), + help='Defaults to env[OS_PROJECT_DOMAIN_ID].') + + parser.add_argument('--os-project-domain-name', + default=utils.env('OS_PROJECT_DOMAIN_NAME'), + help='Defaults to env[OS_PROJECT_DOMAIN_NAME].') + #NOTE(bcwaldon): DEPRECATED parser.add_argument('-I', dest='os_username', @@ -230,6 +206,101 @@ class OpenStackImagesShell(object): dest='os_auth_token', help='DEPRECATED! Use --os-auth-token.') + parser.add_argument('--os-service-type', + default=utils.env('OS_SERVICE_TYPE'), + help='Defaults to env[OS_SERVICE_TYPE].') + + parser.add_argument('--os_service_type', + help=argparse.SUPPRESS) + + parser.add_argument('--os-endpoint-type', + default=utils.env('OS_ENDPOINT_TYPE'), + help='Defaults to env[OS_ENDPOINT_TYPE].') + + parser.add_argument('--os_endpoint_type', + help=argparse.SUPPRESS) + + def get_base_parser(self): + parser = argparse.ArgumentParser( + prog='glance', + description=__doc__.strip(), + epilog='See "glance help COMMAND" ' + 'for help on a specific command.', + add_help=False, + formatter_class=HelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', '--help', + action='store_true', + help=argparse.SUPPRESS, + ) + + parser.add_argument('--version', + action='version', + version=glanceclient.__version__) + + parser.add_argument('-d', '--debug', + default=bool(utils.env('GLANCECLIENT_DEBUG')), + action='store_true', + help='Defaults to env[GLANCECLIENT_DEBUG].') + + parser.add_argument('-v', '--verbose', + default=False, action="store_true", + help="Print more verbose output") + + parser.add_argument('--get-schema', + default=False, action="store_true", + dest='get_schema', + help='Ignores cached copy and forces retrieval ' + 'of schema that generates portions of the ' + 'help text. Ignored with API version 1.') + + parser.add_argument('--timeout', + default=600, + help='Number of seconds to wait for a response') + + parser.add_argument('--no-ssl-compression', + dest='ssl_compression', + default=True, action='store_false', + help='Disable SSL compression when using https.') + + parser.add_argument('-f', '--force', + dest='force', + default=False, action='store_true', + help='Prevent select actions from requesting ' + 'user confirmation.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('--dry-run', + default=False, + action='store_true', + help='DEPRECATED! Only used for deprecated ' + 'legacy commands.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('--ssl', + dest='use_ssl', + default=False, + action='store_true', + help='DEPRECATED! Send a fully-formed endpoint ' + 'using --os-image-url instead.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-H', '--host', + metavar='ADDRESS', + help='DEPRECATED! Send a fully-formed endpoint ' + 'using --os-image-url instead.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-p', '--port', + dest='port', + metavar='PORT', + type=int, + default=9292, + help='DEPRECATED! Send a fully-formed endpoint ' + 'using --os-image-url instead.') + parser.add_argument('--os-image-url', default=utils.env('OS_IMAGE_URL'), help='Defaults to env[OS_IMAGE_URL].') @@ -250,25 +321,14 @@ class OpenStackImagesShell(object): parser.add_argument('--os_image_api_version', help=argparse.SUPPRESS) - parser.add_argument('--os-service-type', - default=utils.env('OS_SERVICE_TYPE'), - help='Defaults to env[OS_SERVICE_TYPE].') - - parser.add_argument('--os_service_type', - help=argparse.SUPPRESS) - - parser.add_argument('--os-endpoint-type', - default=utils.env('OS_ENDPOINT_TYPE'), - help='Defaults to env[OS_ENDPOINT_TYPE].') - - parser.add_argument('--os_endpoint_type', - help=argparse.SUPPRESS) - #NOTE(bcwaldon): DEPRECATED parser.add_argument('-S', '--os_auth_strategy', help='DEPRECATED! This option is ' 'completely ignored.') + # FIXME(bobt): this method should come from python-keystoneclient + self._append_global_identity_args(parser) + return parser def get_subcommand_parser(self, version): @@ -306,36 +366,6 @@ class OpenStackImagesShell(object): subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) - def _get_ksclient(self, **kwargs): - """Get an endpoint and auth token from Keystone. - - :param username: name of user - :param password: user's password - :param tenant_id: unique identifier of tenant - :param tenant_name: name of tenant - :param auth_url: endpoint to authenticate against - """ - return ksclient.Client(username=kwargs.get('username'), - password=kwargs.get('password'), - tenant_id=kwargs.get('tenant_id'), - tenant_name=kwargs.get('tenant_name'), - auth_url=kwargs.get('auth_url'), - cacert=kwargs.get('cacert'), - insecure=kwargs.get('insecure')) - - def _get_endpoint(self, client, **kwargs): - """Get an endpoint using the provided keystone client.""" - endpoint_kwargs = { - 'service_type': kwargs.get('service_type') or 'image', - 'endpoint_type': kwargs.get('endpoint_type') or 'publicURL', - } - - if kwargs.get('region_name'): - endpoint_kwargs['attr'] = 'region' - endpoint_kwargs['filter_value'] = kwargs.get('region_name') - - return client.service_catalog.url_for(**endpoint_kwargs) - def _get_image_url(self, args): """Translate the available url-related options into a single string. @@ -353,6 +383,101 @@ class OpenStackImagesShell(object): else: return None + def _discover_auth_versions(self, session, auth_url): + # discover the API versions the server is supporting base on the + # given URL + v2_auth_url = None + v3_auth_url = None + try: + ks_discover = discover.Discover(session=session, auth_url=auth_url) + v2_auth_url = ks_discover.url_for('2.0') + v3_auth_url = ks_discover.url_for('3.0') + except ks_exc.ClientException as e: + # Identity service may not support discover API version. + # Lets trying to figure out the API version from the original URL. + url_parts = urlparse.urlparse(auth_url) + (scheme, netloc, path, params, query, fragment) = url_parts + path = path.lower() + if path.startswith('/v3'): + v3_auth_url = auth_url + elif path.startswith('/v2'): + v2_auth_url = auth_url + else: + # not enough information to determine the auth version + msg = ('Unable to determine the Keystone version ' + 'to authenticate with using the given ' + 'auth_url. Identity service may not support API ' + 'version discovery. Please provide a versioned ' + 'auth_url instead. error=%s') % (e) + raise exc.CommandError(msg) + + return (v2_auth_url, v3_auth_url) + + def _get_keystone_session(self, **kwargs): + ks_session = session.Session.construct(kwargs) + + # discover the supported keystone versions using the given auth url + auth_url = kwargs.pop('auth_url', None) + (v2_auth_url, v3_auth_url) = self._discover_auth_versions( + session=ks_session, + auth_url=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. + user_id = kwargs.pop('user_id', None) + username = kwargs.pop('username', None) + password = kwargs.pop('password', None) + user_domain_name = kwargs.pop('user_domain_name', None) + user_domain_id = kwargs.pop('user_domain_id', None) + # project and tenant can be used interchangeably + project_id = (kwargs.pop('project_id', None) or + kwargs.pop('tenant_id', None)) + project_name = (kwargs.pop('project_name', None) or + kwargs.pop('tenant_name', None)) + project_domain_id = kwargs.pop('project_domain_id', None) + project_domain_name = kwargs.pop('project_domain_name', None) + auth = None + + use_domain = (user_domain_id or + user_domain_name or + project_domain_id or + project_domain_name) + use_v3 = v3_auth_url and (use_domain or (not v2_auth_url)) + use_v2 = v2_auth_url and not use_domain + + if use_v3: + auth = v3_auth.Password( + v3_auth_url, + user_id=user_id, + username=username, + password=password, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + elif use_v2: + auth = v2_auth.Password( + v2_auth_url, + username, + password, + tenant_id=project_id, + tenant_name=project_name) + else: + # if we get here it means domain information is provided + # (caller meant to use Keystone V3) but the auth url is + # actually Keystone V2. Obviously we can't authenticate a V3 + # user using V2. + exc.CommandError("Credential and auth_url mismatch. The given " + "auth_url is using Keystone V2 endpoint, which " + "may not able to handle Keystone V3 credentials. " + "Please provide a correct Keystone V3 auth_url.") + + ks_session.auth = auth + return ks_session + def _get_endpoint_and_token(self, args, force_auth=False): image_url = self._get_image_url(args) auth_token = args.os_auth_token @@ -364,42 +489,74 @@ class OpenStackImagesShell(object): endpoint = image_url token = args.os_auth_token else: + if not args.os_username: - raise exc.CommandError("You must provide a username via" - " either --os-username or " - "env[OS_USERNAME]") + raise exc.CommandError( + _("You must provide a username via" + " either --os-username or " + "env[OS_USERNAME]")) if not args.os_password: - raise exc.CommandError("You must provide a password via" - " either --os-password or " - "env[OS_PASSWORD]") + raise exc.CommandError( + _("You must provide a password via" + " either --os-password or " + "env[OS_PASSWORD]")) - if not (args.os_tenant_id or args.os_tenant_name): - raise exc.CommandError("You must provide a tenant_id via" - " either --os-tenant-id or " - "via env[OS_TENANT_ID]") + # Validate password flow auth + project_info = (args.os_tenant_name or + args.os_tenant_id or + (args.os_project_name and + (args.project_domain_name or + args.project_domain_id)) or + args.os_project_id) + + if (not project_info): + # tenent is deprecated in Keystone v3. Use the latest + # terminology instead. + raise exc.CommandError( + _("You must provide a project_id or project_name (" + "with project_domain_name or project_domain_id) " + "via " + " --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 args.os_auth_url: - raise exc.CommandError("You must provide an auth url via" - " either --os-auth-url or " - "via env[OS_AUTH_URL]") - kwargs = { - 'username': args.os_username, - 'password': args.os_password, - 'tenant_id': args.os_tenant_id, - 'tenant_name': args.os_tenant_name, - 'auth_url': args.os_auth_url, - 'service_type': args.os_service_type, - 'endpoint_type': args.os_endpoint_type, - 'cacert': args.os_cacert, - 'insecure': args.insecure, - 'region_name': args.os_region_name, - } - _ksclient = self._get_ksclient(**kwargs) - token = args.os_auth_token or _ksclient.auth_token + raise exc.CommandError( + _("You must provide an auth url via" + " either --os-auth-url or " + "via env[OS_AUTH_URL]")) - endpoint = args.os_image_url or self._get_endpoint(_ksclient, - **kwargs) + kwargs = { + 'auth_url': args.os_auth_url, + 'username': args.os_username, + 'user_id': args.os_user_id, + 'user_domain_id': args.os_user_domain_id, + 'user_domain_name': args.os_user_domain_name, + 'password': args.os_password, + 'tenant_name': args.os_tenant_name, + 'tenant_id': args.os_tenant_id, + 'project_name': args.os_project_name, + 'project_id': args.os_project_id, + 'project_domain_name': args.os_project_domain_name, + 'project_domain_id': args.os_project_domain_id, + 'insecure': args.insecure, + 'cacert': args.os_cacert, + 'cert': args.os_cert, + 'key': args.os_key + } + ks_session = self._get_keystone_session(**kwargs) + token = args.os_auth_token or ks_session.get_token() + + endpoint_type = args.os_endpoint_type or 'public' + service_type = args.os_service_type or 'image' + endpoint = args.os_image_url or ks_session.get_endpoint( + service_type=service_type, + endpoint_type=endpoint_type, + region_name=args.os_region_name) return endpoint, token @@ -412,8 +569,8 @@ class OpenStackImagesShell(object): 'insecure': args.insecure, 'timeout': args.timeout, 'cacert': args.os_cacert, - 'cert_file': args.cert_file, - 'key_file': args.key_file, + 'cert': args.os_cert, + 'key': args.os_key, 'ssl_compression': args.ssl_compression } client = glanceclient.Client(api_version, endpoint, **kwargs) diff --git a/tests/keystone_client_fixtures.py b/tests/keystone_client_fixtures.py new file mode 100644 index 00000000..b28b5e21 --- /dev/null +++ b/tests/keystone_client_fixtures.py @@ -0,0 +1,188 @@ +# 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) + +GLANCE_ENDPOINT = 'http://glance.example.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['glance_url'] = ref.get('glance_url', GLANCE_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) + + o = {'access': {'token': {'id': uuid.uuid4().hex, + '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 Glance and Keystone endpoints + o['access']['serviceCatalog'] = [ + {'endpoints': [ + {'publicURL': ref.get('glance_url'), + 'id': uuid.uuid4().hex, + 'region': 'RegionOne' + }], + 'endpoints_links': [], + 'name': 'Glance', + 'type': 'keystore'}, + {'endpoints': [ + {'publicURL': ref.get('auth_url'), + 'adminURL': ref.get('auth_url'), + 'id': uuid.uuid4().hex, + 'region': 'RegionOne' + }], + 'endpoint_links': [], + 'name': 'keystone', + 'type': 'identity'}] + + return 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 Glance and Keystone endpoints + o['token']['catalog'] = [ + {'endpoints': [ + { + 'id': uuid.uuid4().hex, + 'interface': 'public', + 'region': 'RegionTwo', + 'url': ref.get('glance_url') + }], + 'id': uuid.uuid4().hex, + 'type': 'keystore'}, + {'endpoints': [ + { + 'id': uuid.uuid4().hex, + 'interface': 'public', + 'region': 'RegionTwo', + 'url': ref.get('auth_url') + }, + { + 'id': uuid.uuid4().hex, + 'interface': 'admin', + 'region': 'RegionTwo', + '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 diff --git a/tests/test_shell.py b/tests/test_shell.py index 28f6b132..29445fa7 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -25,31 +25,50 @@ from glanceclient import shell as openstack_shell #NOTE (esheffield) Used for the schema caching tests from glanceclient.v2 import schemas as schemas import json - +from tests import keystone_client_fixtures from tests import utils +import keystoneclient +from keystoneclient.openstack.common.apiclient import exceptions as ks_exc + + DEFAULT_IMAGE_URL = 'http://127.0.0.1:5000/' DEFAULT_USERNAME = 'username' DEFAULT_PASSWORD = 'password' DEFAULT_TENANT_ID = 'tenant_id' DEFAULT_TENANT_NAME = 'tenant_name' -DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' +DEFAULT_PROJECT_ID = '0123456789' +DEFAULT_USER_DOMAIN_NAME = 'user_domain_name' +DEFAULT_UNVERSIONED_AUTH_URL = 'http://127.0.0.1:5000/' +DEFAULT_V2_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' +DEFAULT_V3_AUTH_URL = 'http://127.0.0.1:5000/v3/' DEFAULT_AUTH_TOKEN = ' 3bcc3d3a03f44e3d8377f9247b0ad155' TEST_SERVICE_URL = 'http://127.0.0.1:5000/' +FAKE_V2_ENV = {'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, + 'OS_AUTH_URL': DEFAULT_V2_AUTH_URL, + 'OS_IMAGE_URL': DEFAULT_IMAGE_URL} + +FAKE_V3_ENV = {'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_PROJECT_ID': DEFAULT_PROJECT_ID, + 'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME, + 'OS_AUTH_URL': DEFAULT_V3_AUTH_URL, + 'OS_IMAGE_URL': DEFAULT_IMAGE_URL} + class ShellTest(utils.TestCase): + # auth environment to use + auth_env = FAKE_V2_ENV.copy() + # expected auth plugin to invoke + auth_plugin = 'keystoneclient.auth.identity.v2.Password' + def setUp(self): super(ShellTest, self).setUp() global _old_env - fake_env = { - 'OS_USERNAME': DEFAULT_USERNAME, - 'OS_PASSWORD': DEFAULT_PASSWORD, - 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, - 'OS_AUTH_URL': DEFAULT_AUTH_URL, - 'OS_IMAGE_URL': DEFAULT_IMAGE_URL, - 'OS_AUTH_TOKEN': DEFAULT_AUTH_TOKEN} - _old_env, os.environ = os.environ, fake_env.copy() + _old_env, os.environ = os.environ, self.auth_env global shell, _shell, assert_called, assert_called_anytime _shell = openstack_shell.OpenStackImagesShell() @@ -99,6 +118,184 @@ class ShellTest(utils.TestCase): targeted_image_url = test_shell._get_image_url(fake_args) self.assertEqual(expected_image_url, targeted_image_url) + @mock.patch.object(openstack_shell.OpenStackImagesShell, + '_get_versioned_client') + def test_cert_and_key_args_interchangeable(self, + mock_versioned_client): + # make sure --os-cert and --os-key are passed correctly + args = '--os-cert mycert --os-key mykey image-list' + shell(args) + assert mock_versioned_client.called + ((api_version, args), kwargs) = mock_versioned_client.call_args + self.assertEqual(args.os_cert, 'mycert') + self.assertEqual(args.os_key, 'mykey') + + # make sure we get the same thing with --cert-file and --key-file + args = '--cert-file mycertfile --key-file mykeyfile image-list' + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + assert mock_versioned_client.called + ((api_version, args), kwargs) = mock_versioned_client.call_args + self.assertEqual(args.os_cert, 'mycertfile') + self.assertEqual(args.os_key, 'mykeyfile') + + @mock.patch('glanceclient.v1.client.Client') + def test_no_auth_with_token_and_image_url_with_v1(self, v1_client): + # test no authentication is required if both token and endpoint url + # are specified + args = ('--os-auth-token mytoken --os-image-url https://image:1234/v1 ' + 'image-list') + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + assert v1_client.called + (args, kwargs) = v1_client.call_args + self.assertEqual(kwargs['token'], 'mytoken') + self.assertEqual(args[0], 'https://image:1234/v1') + + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schema') + def test_no_auth_with_token_and_image_url_with_v2(self, + cache_schema): + with mock.patch('glanceclient.v2.client.Client') as v2_client: + # test no authentication is required if both token and endpoint url + # are specified + args = ('--os-auth-token mytoken ' + '--os-image-url https://image:1234/v2 ' + '--os-image-api-version 2 image-list') + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + ((args), kwargs) = v2_client.call_args + self.assertEqual(args[0], 'https://image:1234/v2') + self.assertEqual(kwargs['token'], 'mytoken') + + def _assert_auth_plugin_args(self, mock_auth_plugin): + # make sure our auth plugin is invoked with the correct args + mock_auth_plugin.assert_called_once_with( + keystone_client_fixtures.V2_URL, + self.auth_env['OS_USERNAME'], + self.auth_env['OS_PASSWORD'], + tenant_name=self.auth_env['OS_TENANT_NAME'], + tenant_id='') + + @mock.patch('glanceclient.v1.client.Client') + @mock.patch('keystoneclient.session.Session') + @mock.patch.object(keystoneclient.discover.Discover, 'url_for', + side_effect=[keystone_client_fixtures.V2_URL, None]) + def test_auth_plugin_invocation_with_v1(self, + v1_client, + ks_session, + url_for): + with mock.patch(self.auth_plugin) as mock_auth_plugin: + args = 'image-list' + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + self._assert_auth_plugin_args(mock_auth_plugin) + + @mock.patch('glanceclient.v2.client.Client') + @mock.patch('keystoneclient.session.Session') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schema') + @mock.patch.object(keystoneclient.discover.Discover, 'url_for', + side_effect=[keystone_client_fixtures.V2_URL, None]) + def test_auth_plugin_invocation_with_v2(self, + v2_client, + ks_session, + url_for, + cache_schema): + with mock.patch(self.auth_plugin) as mock_auth_plugin: + args = '--os-image-api-version 2 image-list' + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + self._assert_auth_plugin_args(mock_auth_plugin) + + @mock.patch('glanceclient.v1.client.Client') + @mock.patch('keystoneclient.session.Session') + @mock.patch.object(keystoneclient.discover.Discover, 'url_for', + side_effect=[keystone_client_fixtures.V2_URL, + keystone_client_fixtures.V3_URL]) + def test_auth_plugin_invocation_with_unversioned_auth_url_with_v1( + self, v1_client, ks_session, url_for): + with mock.patch(self.auth_plugin) as mock_auth_plugin: + args = '--os-auth-url %s image-list' % ( + keystone_client_fixtures.BASE_URL) + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + self._assert_auth_plugin_args(mock_auth_plugin) + + @mock.patch('glanceclient.v2.client.Client') + @mock.patch('keystoneclient.session.Session') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schema') + @mock.patch.object(keystoneclient.discover.Discover, 'url_for', + side_effect=[keystone_client_fixtures.V2_URL, + keystone_client_fixtures.V3_URL]) + def test_auth_plugin_invocation_with_unversioned_auth_url_with_v2( + self, v2_client, ks_session, cache_schema, url_for): + with mock.patch(self.auth_plugin) as mock_auth_plugin: + args = ('--os-auth-url %s --os-image-api-version 2 ' + 'image-list') % (keystone_client_fixtures.BASE_URL) + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + self._assert_auth_plugin_args(mock_auth_plugin) + + +class ShellTestWithKeystoneV3Auth(ShellTest): + # auth environment to use + auth_env = FAKE_V3_ENV.copy() + # expected auth plugin to invoke + auth_plugin = 'keystoneclient.auth.identity.v3.Password' + + def _assert_auth_plugin_args(self, mock_auth_plugin): + mock_auth_plugin.assert_called_once_with( + keystone_client_fixtures.V3_URL, + user_id='', + username=self.auth_env['OS_USERNAME'], + password=self.auth_env['OS_PASSWORD'], + user_domain_id='', + user_domain_name=self.auth_env['OS_USER_DOMAIN_NAME'], + project_id=self.auth_env['OS_PROJECT_ID'], + project_name='', + project_domain_id='', + project_domain_name='') + + @mock.patch('glanceclient.v1.client.Client') + @mock.patch('keystoneclient.session.Session') + @mock.patch.object(keystoneclient.discover.Discover, 'url_for', + side_effect=[None, keystone_client_fixtures.V3_URL]) + def test_auth_plugin_invocation_with_v1(self, + v1_client, + ks_session, + url_for): + with mock.patch(self.auth_plugin) as mock_auth_plugin: + args = 'image-list' + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + self._assert_auth_plugin_args(mock_auth_plugin) + + @mock.patch('glanceclient.v2.client.Client') + @mock.patch('keystoneclient.session.Session') + @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schema') + @mock.patch.object(keystoneclient.discover.Discover, 'url_for', + side_effect=[None, keystone_client_fixtures.V3_URL]) + def test_auth_plugin_invocation_with_v2(self, + v2_client, + ks_session, + url_for, + cache_schema): + with mock.patch(self.auth_plugin) as mock_auth_plugin: + args = '--os-image-api-version 2 image-list' + glance_shell = openstack_shell.OpenStackImagesShell() + glance_shell.main(args.split()) + self._assert_auth_plugin_args(mock_auth_plugin) + + @mock.patch('keystoneclient.session.Session') + @mock.patch('keystoneclient.discover.Discover', + side_effect=ks_exc.ClientException()) + def test_api_discovery_failed_with_unversioned_auth_url(self, + ks_session, + discover): + args = '--os-auth-url %s image-list' % ( + keystone_client_fixtures.BASE_URL) + glance_shell = openstack_shell.OpenStackImagesShell() + self.assertRaises(exc.CommandError, glance_shell.main, args.split()) + class ShellCacheSchemaTest(utils.TestCase): def setUp(self):