summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBob Thyne <bob.thyne@hp.com>2014-03-21 15:26:09 +0000
committerBob Thyne <bob.thyne@hp.com>2014-08-05 11:05:09 -0700
commitf15dc6b82f1972e8995ad695bd8b3b52bf5c3897 (patch)
tree852d5467435ef9fec1cec0f95d95505d8a126e82
parent938031ad526866e72a27d3c566c9cbc2c0d7dd68 (diff)
downloadpython-glanceclient-f15dc6b82f1972e8995ad695bd8b3b52bf5c3897.tar.gz
Add support for Keystone v3
This enables glanceclient to authenticate using Keystone v3 API and includes the addition of several new CLI arguments. DocImpact Change-Id: I863ba08d312363dc1ce4fc7822fb21ef53df1a4f
-rw-r--r--glanceclient/shell.py461
-rw-r--r--tests/keystone_client_fixtures.py188
-rw-r--r--tests/test_shell.py217
3 files changed, 704 insertions, 162 deletions
diff --git a/glanceclient/shell.py b/glanceclient/shell.py
index 4bef684..2381c2d 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.')
+class OpenStackImagesShell(object):
+ 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='<ca-certificate-file>',
dest='os_cacert',
@@ -106,57 +93,46 @@ 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('--os-username',
+ default=utils.env('OS_USERNAME'),
+ help='Defaults to env[OS_USERNAME].')
- parser.add_argument('--no-ssl-compression',
- dest='ssl_compression',
- default=True, action='store_false',
- help='Disable SSL compression when using https.')
+ parser.add_argument('--os_username',
+ help=argparse.SUPPRESS)
- parser.add_argument('-f', '--force',
- dest='force',
- default=False, action='store_true',
- help='Prevent select actions from requesting '
- 'user confirmation.')
+ parser.add_argument('--os-user-id',
+ default=utils.env('OS_USER_ID'),
+ help='Defaults to env[OS_USER_ID].')
- #NOTE(bcwaldon): DEPRECATED
- parser.add_argument('--dry-run',
- default=False,
- action='store_true',
- help='DEPRECATED! Only used for deprecated '
- 'legacy commands.')
+ parser.add_argument('--os-user-domain-id',
+ default=utils.env('OS_USER_DOMAIN_ID'),
+ help='Defaults to env[OS_USER_DOMAIN_ID].')
- #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.')
+ parser.add_argument('--os-user-domain-name',
+ default=utils.env('OS_USER_DOMAIN_NAME'),
+ help='Defaults to env[OS_USER_DOMAIN_NAME].')
- #NOTE(bcwaldon): DEPRECATED
- parser.add_argument('-H', '--host',
- metavar='ADDRESS',
- help='DEPRECATED! Send a fully-formed endpoint '
- 'using --os-image-url instead.')
+ 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].')
- #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-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-username',
- default=utils.env('OS_USERNAME'),
- help='Defaults to env[OS_USERNAME].')
+ 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_username',
- help=argparse.SUPPRESS)
+ 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',
@@ -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]")
-
- 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]")
+ raise exc.CommandError(
+ _("You must provide a password via"
+ " either --os-password or "
+ "env[OS_PASSWORD]"))
+
+ # 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]")
+ raise exc.CommandError(
+ _("You must provide an auth url via"
+ " either --os-auth-url or "
+ "via env[OS_AUTH_URL]"))
+
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_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,
+ '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,
- 'region_name': args.os_region_name,
+ 'cacert': args.os_cacert,
+ 'cert': args.os_cert,
+ 'key': args.os_key
}
- _ksclient = self._get_ksclient(**kwargs)
- token = args.os_auth_token or _ksclient.auth_token
+ ks_session = self._get_keystone_session(**kwargs)
+ token = args.os_auth_token or ks_session.get_token()
- endpoint = args.os_image_url or self._get_endpoint(_ksclient,
- **kwargs)
+ 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 0000000..b28b5e2
--- /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 ebe77e0..34eb391 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):