Support for version and extension discovery
- Supports unauthenticated call to Keystone to discover supported API versions - Added command-line support (usage: keystone discover) - Added client support (keystoneclient.genenric client). Client returns dicts, whereas shell command prints formated output. - Added tests for genenric client - Replicates 'nove discover' in python-novaclient - Starts to address blueprint keystone-client - keystone discover output looks like this: $ keystone discover Keystone found at http://localhost:35357 - supports version v1.0 (DEPRECATED) here http://localhost:35357/v1.0 - supports version v1.1 (CURRENT) here http://localhost:35357/v1.1 - supports version v2.0 (BETA) here http://localhost:35357/v2.0 - and HP-IDM: HP Token Validation Extension - and OS-KSADM: Openstack Keystone Admin - and OS-KSCATALOG: Openstack Keystone Catalog Change-Id: Id16d34dac094c780d36afb3e31c98c318b6071ac
This commit is contained in:
@@ -4,6 +4,12 @@ The :mod:`keystoneclient` Python API
|
|||||||
.. module:: keystoneclient
|
.. module:: keystoneclient
|
||||||
:synopsis: A client for the OpenStack Keystone API.
|
:synopsis: A client for the OpenStack Keystone API.
|
||||||
|
|
||||||
|
.. currentmodule:: keystoneclient.generic.client
|
||||||
|
|
||||||
|
.. autoclass:: Client
|
||||||
|
|
||||||
|
.. automethod:: discover
|
||||||
|
|
||||||
.. currentmodule:: keystoneclient.v2_0.client
|
.. currentmodule:: keystoneclient.v2_0.client
|
||||||
|
|
||||||
.. autoclass:: Client
|
.. autoclass:: Client
|
||||||
|
0
keystoneclient/generic/__init__.py
Normal file
0
keystoneclient/generic/__init__.py
Normal file
205
keystoneclient/generic/client.py
Normal file
205
keystoneclient/generic/client.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from keystoneclient import client
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Client(client.HTTPClient):
|
||||||
|
"""Client for the OpenStack Keystone pre-version calls API.
|
||||||
|
|
||||||
|
:param string endpoint: A user-supplied endpoint URL for the keystone
|
||||||
|
service.
|
||||||
|
:param integer timeout: Allows customization of the timeout for client
|
||||||
|
http requests. (optional)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> from keystoneclient.generic import client
|
||||||
|
>>> root = client.Client(auth_url=KEYSTONE_URL)
|
||||||
|
>>> versions = root.discover()
|
||||||
|
...
|
||||||
|
>>> from keystoneclient.v2_0 import client as v2client
|
||||||
|
>>> keystone = v2client.Client(auth_url=versions['v2.0']['url'])
|
||||||
|
...
|
||||||
|
>>> user = keystone.users.get(USER_ID)
|
||||||
|
>>> user.delete()
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, endpoint=None, **kwargs):
|
||||||
|
""" Initialize a new client for the Keystone v2.0 API. """
|
||||||
|
super(Client, self).__init__(endpoint=endpoint, **kwargs)
|
||||||
|
self.endpoint = endpoint
|
||||||
|
|
||||||
|
def discover(self, url=None):
|
||||||
|
""" Discover Keystone servers and return API versions supported.
|
||||||
|
|
||||||
|
:param url: optional url to test (without version)
|
||||||
|
|
||||||
|
Returns::
|
||||||
|
|
||||||
|
{
|
||||||
|
'message': 'Keystone found at http://127.0.0.1:5000/',
|
||||||
|
'v2.0': {
|
||||||
|
'status': 'beta',
|
||||||
|
'url': 'http://127.0.0.1:5000/v2.0/',
|
||||||
|
'id': 'v2.0'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
if url:
|
||||||
|
return self._check_keystone_versions(url)
|
||||||
|
else:
|
||||||
|
return self._local_keystone_exists()
|
||||||
|
|
||||||
|
def _local_keystone_exists(self):
|
||||||
|
""" Checks if Keystone is available on default local port 35357 """
|
||||||
|
return self._check_keystone_versions("http://localhost:35357")
|
||||||
|
|
||||||
|
def _check_keystone_versions(self, url):
|
||||||
|
""" Calls Keystone URL and detects the available API versions """
|
||||||
|
try:
|
||||||
|
httpclient = client.HTTPClient()
|
||||||
|
resp, body = httpclient.request(url, "GET",
|
||||||
|
headers={'Accept': 'application/json'})
|
||||||
|
if resp.status in (200, 204): # in some cases we get No Content
|
||||||
|
try:
|
||||||
|
results = {}
|
||||||
|
if 'version' in body:
|
||||||
|
results['message'] = "Keystone found at %s" % url
|
||||||
|
version = body['version']
|
||||||
|
# Stable/diablo incorrect format
|
||||||
|
id, status, version_url = self._get_version_info(
|
||||||
|
version, url)
|
||||||
|
results[str(id)] = {"id": id,
|
||||||
|
"status": status,
|
||||||
|
"url": version_url}
|
||||||
|
return results
|
||||||
|
elif 'versions' in body:
|
||||||
|
# Correct format
|
||||||
|
results['message'] = "Keystone found at %s" % url
|
||||||
|
for version in body['versions']['values']:
|
||||||
|
id, status, version_url = self._get_version_info(
|
||||||
|
version, url)
|
||||||
|
results[str(id)] = {"id": id,
|
||||||
|
"status": status,
|
||||||
|
"url": version_url}
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
results['message'] = "Unrecognized response from %s" \
|
||||||
|
% url
|
||||||
|
return results
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.AuthorizationFailure()
|
||||||
|
elif resp.status == 305:
|
||||||
|
return self._check_keystone_versions(resp['location'])
|
||||||
|
else:
|
||||||
|
raise exceptions.from_response(resp, body)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception(e)
|
||||||
|
|
||||||
|
def discover_extensions(self, url=None):
|
||||||
|
""" Discover Keystone extensions supported.
|
||||||
|
|
||||||
|
:param url: optional url to test (should have a version in it)
|
||||||
|
|
||||||
|
Returns::
|
||||||
|
|
||||||
|
{
|
||||||
|
'message': 'Keystone extensions at http://127.0.0.1:35357/v2',
|
||||||
|
'OS-KSEC2': 'OpenStack EC2 Credentials Extension',
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
if url:
|
||||||
|
return self._check_keystone_extensions(url)
|
||||||
|
|
||||||
|
def _check_keystone_extensions(self, url):
|
||||||
|
""" Calls Keystone URL and detects the available extensions """
|
||||||
|
try:
|
||||||
|
httpclient = client.HTTPClient()
|
||||||
|
if not url.endswith("/"):
|
||||||
|
url += '/'
|
||||||
|
resp, body = httpclient.request("%sextensions" % url, "GET",
|
||||||
|
headers={'Accept': 'application/json'})
|
||||||
|
if resp.status in (200, 204): # in some cases we get No Content
|
||||||
|
try:
|
||||||
|
results = {}
|
||||||
|
if 'extensions' in body:
|
||||||
|
if 'values' in body['extensions']:
|
||||||
|
# Parse correct format (per contract)
|
||||||
|
for extension in body['extensions']['values']:
|
||||||
|
alias, name = self._get_extension_info(
|
||||||
|
extension['extension'])
|
||||||
|
results[alias] = name
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
# Support incorrect, but prevalent format
|
||||||
|
for extension in body['extensions']:
|
||||||
|
alias, name = self._get_extension_info(
|
||||||
|
extension)
|
||||||
|
results[alias] = name
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
results['message'] = "Unrecognized extensions" \
|
||||||
|
" response from %s" % url
|
||||||
|
return results
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.AuthorizationFailure()
|
||||||
|
elif resp.status == 305:
|
||||||
|
return self._check_keystone_extensions(resp['location'])
|
||||||
|
else:
|
||||||
|
raise exceptions.from_response(resp, body)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception(e)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_version_info(version, root_url):
|
||||||
|
""" Parses version information
|
||||||
|
|
||||||
|
:param version: a dict of a Keystone version response
|
||||||
|
:param root_url: string url used to construct
|
||||||
|
the version if no URL is provided.
|
||||||
|
:returns: tuple - (verionId, versionStatus, versionUrl)
|
||||||
|
"""
|
||||||
|
id = version['id']
|
||||||
|
status = version['status']
|
||||||
|
ref = urlparse.urljoin(root_url, id)
|
||||||
|
if 'links' in version:
|
||||||
|
for link in version['links']:
|
||||||
|
if link['rel'] == 'self':
|
||||||
|
ref = link['href']
|
||||||
|
break
|
||||||
|
return (id, status, ref)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_extension_info(extension):
|
||||||
|
""" Parses extension information
|
||||||
|
|
||||||
|
:param extension: a dict of a Keystone extension response
|
||||||
|
:returns: tuple - (alias, name)
|
||||||
|
"""
|
||||||
|
alias = extension['alias']
|
||||||
|
name = extension['name']
|
||||||
|
return (alias, name)
|
59
keystoneclient/generic/shell.py
Normal file
59
keystoneclient/generic/shell.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from keystoneclient import utils
|
||||||
|
from keystoneclient.generic import client
|
||||||
|
|
||||||
|
CLIENT_CLASS = client.Client
|
||||||
|
|
||||||
|
|
||||||
|
@utils.unauthenticated
|
||||||
|
def do_discover(cs, args):
|
||||||
|
"""
|
||||||
|
Discover Keystone servers and show authentication protocols and
|
||||||
|
extensions supported.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
$ keystone discover
|
||||||
|
Keystone found at http://localhost:35357
|
||||||
|
- supports version v1.0 (DEPRECATED) here http://localhost:35357/v1.0
|
||||||
|
- supports version v1.1 (CURRENT) here http://localhost:35357/v1.1
|
||||||
|
- supports version v2.0 (BETA) here http://localhost:35357/v2.0
|
||||||
|
- and RAX-KSKEY: Rackspace API Key Authentication Admin Extension
|
||||||
|
- and RAX-KSGRP: Rackspace Keystone Group Extensions
|
||||||
|
"""
|
||||||
|
if cs.endpoint:
|
||||||
|
versions = cs.discover(cs.endpoint)
|
||||||
|
elif cs.auth_url:
|
||||||
|
versions = cs.discover(cs.auth_url)
|
||||||
|
else:
|
||||||
|
versions = cs.discover()
|
||||||
|
if versions:
|
||||||
|
if 'message' in versions:
|
||||||
|
print versions['message']
|
||||||
|
for key, version in versions.iteritems():
|
||||||
|
if key != 'message':
|
||||||
|
print " - supports version %s (%s) here %s" % \
|
||||||
|
(version['id'], version['status'], version['url'])
|
||||||
|
extensions = cs.discover_extensions(version['url'])
|
||||||
|
if extensions:
|
||||||
|
for key, extension in extensions.iteritems():
|
||||||
|
if key != 'message':
|
||||||
|
print " - and %s: %s" % \
|
||||||
|
(key, extension)
|
||||||
|
else:
|
||||||
|
print "No Keystone-compatible endpoint found"
|
@@ -26,6 +26,7 @@ import sys
|
|||||||
from keystoneclient import exceptions as exc
|
from keystoneclient import exceptions as exc
|
||||||
from keystoneclient import utils
|
from keystoneclient import utils
|
||||||
from keystoneclient.v2_0 import shell as shell_v2_0
|
from keystoneclient.v2_0 import shell as shell_v2_0
|
||||||
|
from keystoneclient.generic import shell as shell_generic
|
||||||
|
|
||||||
|
|
||||||
def env(e):
|
def env(e):
|
||||||
@@ -99,6 +100,7 @@ class OpenStackIdentityShell(object):
|
|||||||
actions_module = shell_v2_0
|
actions_module = shell_v2_0
|
||||||
|
|
||||||
self._find_actions(subparsers, actions_module)
|
self._find_actions(subparsers, actions_module)
|
||||||
|
self._find_actions(subparsers, shell_generic)
|
||||||
self._find_actions(subparsers, self)
|
self._find_actions(subparsers, self)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
@@ -151,28 +153,33 @@ class OpenStackIdentityShell(object):
|
|||||||
#FIXME(usrleon): Here should be restrict for project id same as
|
#FIXME(usrleon): Here should be restrict for project id same as
|
||||||
# for username or apikey but for compatibility it is not.
|
# for username or apikey but for compatibility it is not.
|
||||||
|
|
||||||
if not args.os_username:
|
if not utils.isunauthenticated(args.func):
|
||||||
raise exc.CommandError("You must provide a username:"
|
if not args.os_username:
|
||||||
"via --username or env[OS_USERNAME]")
|
raise exc.CommandError("You must provide a username:"
|
||||||
if not args.os_password:
|
"via --username or env[OS_USERNAME]")
|
||||||
raise exc.CommandError("You must provide a password, either"
|
if not args.os_password:
|
||||||
"via --password or env[OS_PASSWORD]")
|
raise exc.CommandError("You must provide a password, either"
|
||||||
|
"via --password or env[OS_PASSWORD]")
|
||||||
|
|
||||||
if not args.os_auth_url:
|
if not args.os_auth_url:
|
||||||
raise exc.CommandError("You must provide a auth url, either"
|
raise exc.CommandError("You must provide a auth url, either"
|
||||||
"via --os-auth_url or via"
|
"via --os-auth_url or via"
|
||||||
"env[OS_AUTH_URL]")
|
"env[OS_AUTH_URL]")
|
||||||
|
|
||||||
self.cs = self.get_api_class(options.os_version)(
|
if utils.isunauthenticated(args.func):
|
||||||
username=args.os_username,
|
self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url)
|
||||||
tenant_name=args.os_tenant_name,
|
else:
|
||||||
tenant_id=args.os_tenant_id,
|
self.cs = self.get_api_class(options.version)(
|
||||||
password=args.os_password,
|
username=args.os_username,
|
||||||
auth_url=args.os_auth_url,
|
tenant_name=args.os_tenant_name,
|
||||||
region_name=args.os_region_name)
|
tenant_id=args.os_tenant_id,
|
||||||
|
password=args.os_password,
|
||||||
|
auth_url=args.os_auth_url,
|
||||||
|
region_name=args.os_region_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.cs.authenticate()
|
if not utils.isunauthenticated(args.func):
|
||||||
|
self.cs.authenticate()
|
||||||
except exc.Unauthorized:
|
except exc.Unauthorized:
|
||||||
raise exc.CommandError("Invalid OpenStack Keystone credentials.")
|
raise exc.CommandError("Invalid OpenStack Keystone credentials.")
|
||||||
except exc.AuthorizationFailure:
|
except exc.AuthorizationFailure:
|
||||||
|
@@ -67,3 +67,24 @@ def find_resource(manager, name_or_id):
|
|||||||
msg = "No %s with a name or ID of '%s' exists." % \
|
msg = "No %s with a name or ID of '%s' exists." % \
|
||||||
(manager.resource_class.__name__.lower(), name_or_id)
|
(manager.resource_class.__name__.lower(), name_or_id)
|
||||||
raise exceptions.CommandError(msg)
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def unauthenticated(f):
|
||||||
|
""" Adds 'unauthenticated' attribute to decorated function.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@unauthenticated
|
||||||
|
def mymethod(f):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
f.unauthenticated = True
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def isunauthenticated(f):
|
||||||
|
"""
|
||||||
|
Checks to see if the function is marked as not requiring authentication
|
||||||
|
with the @unauthenticated decorator. Returns True if decorator is
|
||||||
|
set to True, False otherwise.
|
||||||
|
"""
|
||||||
|
return getattr(f, 'unauthenticated', False)
|
||||||
|
@@ -12,8 +12,10 @@ class TestCase(unittest.TestCase):
|
|||||||
TEST_TENANT_NAME = 'aTenant'
|
TEST_TENANT_NAME = 'aTenant'
|
||||||
TEST_TOKEN = 'aToken'
|
TEST_TOKEN = 'aToken'
|
||||||
TEST_USER = 'test'
|
TEST_USER = 'test'
|
||||||
TEST_URL = 'http://127.0.0.1:5000/v2.0'
|
TEST_ROOT_URL = 'http://127.0.0.1:5000/'
|
||||||
TEST_ADMIN_URL = 'http://127.0.0.1:35357/v2.0'
|
TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0')
|
||||||
|
TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/'
|
||||||
|
TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0')
|
||||||
|
|
||||||
TEST_SERVICE_CATALOG = [{
|
TEST_SERVICE_CATALOG = [{
|
||||||
"endpoints": [{
|
"endpoints": [{
|
||||||
@@ -79,3 +81,24 @@ class TestCase(unittest.TestCase):
|
|||||||
super(TestCase, self).tearDown()
|
super(TestCase, self).tearDown()
|
||||||
self.mox.UnsetStubs()
|
self.mox.UnsetStubs()
|
||||||
self.mox.VerifyAll()
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthenticatedTestCase(unittest.TestCase):
|
||||||
|
""" Class used as base for unauthenticated calls """
|
||||||
|
TEST_ROOT_URL = 'http://127.0.0.1:5000/'
|
||||||
|
TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0')
|
||||||
|
TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/'
|
||||||
|
TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0')
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(UnauthenticatedTestCase, self).setUp()
|
||||||
|
self.mox = mox.Mox()
|
||||||
|
self._original_time = time.time
|
||||||
|
time.time = lambda: 1234
|
||||||
|
httplib2.Http.request = self.mox.CreateMockAnything()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
time.time = self._original_time
|
||||||
|
super(UnauthenticatedTestCase, self).tearDown()
|
||||||
|
self.mox.UnsetStubs()
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
105
tests/v2_0/test_discovery.py
Normal file
105
tests/v2_0/test_discovery.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import httplib2
|
||||||
|
import json
|
||||||
|
|
||||||
|
from keystoneclient.generic import client
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
def to_http_response(resp_dict):
|
||||||
|
"""
|
||||||
|
Utility function to convert a python dictionary
|
||||||
|
(e.g. {'status':status, 'body': body, 'headers':headers}
|
||||||
|
to an httplib2 response.
|
||||||
|
"""
|
||||||
|
resp = httplib2.Response(resp_dict)
|
||||||
|
for k, v in resp_dict['headers'].items():
|
||||||
|
resp[k] = v
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverKeystoneTests(utils.UnauthenticatedTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(DiscoverKeystoneTests, self).setUp()
|
||||||
|
self.TEST_RESPONSE_DICT = {
|
||||||
|
"versions": {
|
||||||
|
"values": [{
|
||||||
|
"id": "v2.0",
|
||||||
|
"status": "beta",
|
||||||
|
"updated": "2011-11-19T00:00:00Z",
|
||||||
|
"links": [{
|
||||||
|
"rel": "self",
|
||||||
|
"href": "http://127.0.0.1:5000/v2.0/"
|
||||||
|
}, {
|
||||||
|
"rel": "describedby",
|
||||||
|
"type": "text/html",
|
||||||
|
"href":
|
||||||
|
"http://docs.openstack.org/api/openstack-identity-service/2.0/content/"
|
||||||
|
}, {
|
||||||
|
"rel": "describedby",
|
||||||
|
"type": "application/pdf",
|
||||||
|
"href":
|
||||||
|
"http://docs.openstack.org/api/openstack-identity-service/2.0/\
|
||||||
|
identity-dev-guide-2.0.pdf"
|
||||||
|
}, {
|
||||||
|
"rel": "describedby",
|
||||||
|
"type": "application/vnd.sun.wadl+xml",
|
||||||
|
"href": "http://127.0.0.1:5000/v2.0/identity.wadl"
|
||||||
|
}],
|
||||||
|
"media-types": [{
|
||||||
|
"base": "application/xml",
|
||||||
|
"type":
|
||||||
|
"application/vnd.openstack.identity-v2.0+xml"
|
||||||
|
}, {
|
||||||
|
"base": "application/json",
|
||||||
|
"type":
|
||||||
|
"application/vnd.openstack.identity-v2.0+json"
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.TEST_REQUEST_HEADERS = {
|
||||||
|
'User-Agent': 'python-keystoneclient',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_versions(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_RESPONSE_DICT),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(self.TEST_ROOT_URL,
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client()
|
||||||
|
versions = cs.discover(self.TEST_ROOT_URL)
|
||||||
|
self.assertIsInstance(versions, dict)
|
||||||
|
self.assertIn('message', versions)
|
||||||
|
self.assertIn('v2.0', versions)
|
||||||
|
self.assertEquals(versions['v2.0']['url'],
|
||||||
|
self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0]
|
||||||
|
['href'])
|
||||||
|
|
||||||
|
def test_get_version_local(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_RESPONSE_DICT),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request("http://localhost:35357",
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client()
|
||||||
|
versions = cs.discover()
|
||||||
|
self.assertIsInstance(versions, dict)
|
||||||
|
self.assertIn('message', versions)
|
||||||
|
self.assertIn('v2.0', versions)
|
||||||
|
self.assertEquals(versions['v2.0']['url'],
|
||||||
|
self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0]
|
||||||
|
['href'])
|
Reference in New Issue
Block a user