Allow the HTTPClient consumer to pass endpoint_type.

Changes the default behavior of the client.  It will now choose
publicURL by default rather than adminURL.

Now allows the consumer to pass adminURL, internalURL or some other
endpoint type when constructing a Client to override this default
behavior.

Adds --endpoint-type option to the shell client.  Defaults to the
environment variable OS_ENDPOINT_TYPE or publicURL.  This was
patterned after the same option in the Nova client.

Adds a new exception type to handle the case where a suitable endpoint
type is not found in the catalog.  Without this, the exception
encountered is a KeyError that was not clearly reported to the caller
of the quantum command line.

Change-Id: Iaffcaff291d433a605d8379dc89c1308096d36c2
Fixes: Bug #1176197
This commit is contained in:
Carl Baldwin 2013-04-30 15:20:10 -06:00
parent d477346627
commit 06375f089b
7 changed files with 216 additions and 6 deletions

@ -60,10 +60,10 @@ class ServiceCatalog(object):
return token
def url_for(self, attr=None, filter_value=None,
service_type='network', endpoint_type='adminURL'):
"""Fetch the admin URL from the Quantum service for
a particular endpoint attribute. If none given, return
the first. See tests for sample service catalog.
service_type='network', endpoint_type='publicURL'):
"""Fetch the URL from the Quantum service for
a particular endpoint type. If none given, return
publicURL.
"""
catalog = self.catalog['access'].get('serviceCatalog', [])
@ -82,6 +82,9 @@ class ServiceCatalog(object):
elif len(matching_endpoints) > 1:
raise exceptions.AmbiguousEndpoints(message=matching_endpoints)
else:
if endpoint_type not in matching_endpoints[0]:
raise exceptions.EndpointTypeNotFound(message=endpoint_type)
return matching_endpoints[0][endpoint_type]
@ -94,12 +97,14 @@ class HTTPClient(httplib2.Http):
password=None, auth_url=None,
token=None, region_name=None, timeout=None,
endpoint_url=None, insecure=False,
endpoint_type='publicURL',
auth_strategy='keystone', **kwargs):
super(HTTPClient, self).__init__(timeout=timeout)
self.username = username
self.tenant_name = tenant_name
self.password = password
self.auth_url = auth_url.rstrip('/') if auth_url else None
self.endpoint_type = endpoint_type
self.region_name = region_name
self.auth_token = token
self.content_type = 'application/json'
@ -170,7 +175,7 @@ class HTTPClient(httplib2.Http):
raise exceptions.Unauthorized()
self.endpoint_url = self.service_catalog.url_for(
attr='region', filter_value=self.region_name,
endpoint_type='adminURL')
endpoint_type=self.endpoint_type)
def authenticate(self):
if self.auth_strategy != 'keystone':
@ -217,7 +222,10 @@ class HTTPClient(httplib2.Http):
for endpoint in body.get('endpoints', []):
if (endpoint['type'] == 'network' and
endpoint.get('region') == self.region_name):
return endpoint['adminURL']
if self.endpoint_type not in endpoint:
raise exceptions.EndpointTypeNotFound(
message=self.endpoint_type)
return endpoint[self.endpoint_type]
raise exceptions.EndpointNotFound()

@ -49,6 +49,7 @@ class ClientManager(object):
def __init__(self, token=None, url=None,
auth_url=None,
endpoint_type=None,
tenant_name=None, tenant_id=None,
username=None, password=None,
region_name=None,
@ -59,6 +60,7 @@ class ClientManager(object):
self._token = token
self._url = url
self._auth_url = auth_url
self._endpoint_type = endpoint_type
self._tenant_name = tenant_name
self._tenant_id = tenant_id
self._username = username
@ -77,6 +79,7 @@ class ClientManager(object):
password=self._password,
region_name=self._region_name,
auth_url=self._auth_url,
endpoint_type=self._endpoint_type,
insecure=self._insecure)
httpclient.authenticate()
# Populate other password flow attributes

@ -111,6 +111,14 @@ class EndpointNotFound(QuantumClientException):
message = _("Could not find Service or Region in Service Catalog.")
class EndpointTypeNotFound(QuantumClientException):
"""Could not find endpoint type in Service Catalog."""
def __str__(self):
msg = "Could not find endpoint type %s in Service Catalog."
return msg % repr(self.message)
class AmbiguousEndpoints(QuantumClientException):
"""Found more than one matching endpoint in Service Catalog."""

@ -405,6 +405,11 @@ class QuantumShell(App):
'--os_token',
help=argparse.SUPPRESS)
parser.add_argument(
'--endpoint-type', metavar='<endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help='Defaults to env[OS_ENDPOINT_TYPE] or publicURL.')
parser.add_argument(
'--os-url', metavar='<url>',
default=env('OS_URL'),
@ -583,6 +588,7 @@ class QuantumShell(App):
region_name=self.options.os_region_name,
api_version=self.api_version,
auth_strategy=self.options.os_auth_strategy,
endpoint_type=self.options.endpoint_type,
insecure=self.options.insecure, )
return

@ -119,6 +119,9 @@ class Client(object):
:param string token: Token for authentication. (optional)
:param string tenant_name: Tenant name. (optional)
:param string auth_url: Keystone service endpoint for authorization.
:param string endpoint_type: Network service endpoint type to pull from the
keystone catalog (e.g. 'publicURL',
'internalURL', or 'adminURL') (optional)
:param string region_name: Name of a region to select when choosing an
endpoint from the service catalog.
:param string endpoint_url: A user-supplied endpoint URL for the quantum

@ -15,6 +15,7 @@
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
import copy
import httplib2
import json
import uuid
@ -23,7 +24,9 @@ import mox
from mox import ContainsKeyValue, IsA, StrContains
import testtools
from quantumclient.client import exceptions
from quantumclient.client import HTTPClient
from quantumclient.client import ServiceCatalog
USERNAME = 'testuser'
@ -136,6 +139,27 @@ class CLITestAuthKeystone(testtools.TestCase):
self.mox.ReplayAll()
self.client.do_request('/resource', 'GET')
def test_get_endpoint_url_other(self):
self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME,
password=PASSWORD, auth_url=AUTH_URL,
region_name=REGION, endpoint_type='otherURL')
self.mox.StubOutWithMock(self.client, "request")
self.client.auth_token = TOKEN
res200 = self.mox.CreateMock(httplib2.Response)
res200.status = 200
self.client.request(StrContains(AUTH_URL +
'/tokens/%s/endpoints' % TOKEN), 'GET',
headers=IsA(dict)). \
AndReturn((res200, json.dumps(ENDPOINTS_RESULT)))
self.mox.ReplayAll()
self.assertRaises(exceptions.EndpointTypeNotFound,
self.client.do_request,
'/resource',
'GET')
def test_get_endpoint_url_failed(self):
self.mox.StubOutWithMock(self.client, "request")
@ -158,3 +182,133 @@ class CLITestAuthKeystone(testtools.TestCase):
AndReturn((res200, ''))
self.mox.ReplayAll()
self.client.do_request('/resource', 'GET')
def test_url_for(self):
resources = copy.deepcopy(KS_TOKEN_RESULT)
endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0]
endpoints['publicURL'] = 'public'
endpoints['internalURL'] = 'internal'
endpoints['adminURL'] = 'admin'
catalog = ServiceCatalog(resources)
# endpoint_type not specified
url = catalog.url_for(attr='region',
filter_value=REGION)
self.assertEqual('public', url)
# endpoint type specified (3 cases)
url = catalog.url_for(attr='region',
filter_value=REGION,
endpoint_type='adminURL')
self.assertEqual('admin', url)
url = catalog.url_for(attr='region',
filter_value=REGION,
endpoint_type='publicURL')
self.assertEqual('public', url)
url = catalog.url_for(attr='region',
filter_value=REGION,
endpoint_type='internalURL')
self.assertEqual('internal', url)
# endpoint_type requested does not exist.
self.assertRaises(exceptions.EndpointTypeNotFound,
catalog.url_for,
attr='region',
filter_value=REGION,
endpoint_type='privateURL')
# Test scenario with url_for when the service catalog only has publicURL.
def test_url_for_only_public_url(self):
resources = copy.deepcopy(KS_TOKEN_RESULT)
catalog = ServiceCatalog(resources)
# Remove endpoints from the catalog.
endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0]
del endpoints['internalURL']
del endpoints['adminURL']
endpoints['publicURL'] = 'public'
# Use publicURL when specified explicitly.
url = catalog.url_for(attr='region',
filter_value=REGION,
endpoint_type='publicURL')
self.assertEqual('public', url)
# Use publicURL when specified explicitly.
url = catalog.url_for(attr='region',
filter_value=REGION)
self.assertEqual('public', url)
# Test scenario with url_for when the service catalog only has adminURL.
def test_url_for_only_admin_url(self):
resources = copy.deepcopy(KS_TOKEN_RESULT)
catalog = ServiceCatalog(resources)
endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0]
del endpoints['internalURL']
del endpoints['publicURL']
endpoints['adminURL'] = 'admin'
# Use publicURL when specified explicitly.
url = catalog.url_for(attr='region',
filter_value=REGION,
endpoint_type='adminURL')
self.assertEqual('admin', url)
# But not when nothing is specified.
self.assertRaises(exceptions.EndpointTypeNotFound,
catalog.url_for,
attr='region',
filter_value=REGION)
def test_endpoint_type(self):
resources = copy.deepcopy(KS_TOKEN_RESULT)
endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0]
endpoints['internalURL'] = 'internal'
endpoints['adminURL'] = 'admin'
endpoints['publicURL'] = 'public'
# Test default behavior is to choose public.
self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME,
password=PASSWORD, auth_url=AUTH_URL,
region_name=REGION)
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'public')
# Test admin url
self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME,
password=PASSWORD, auth_url=AUTH_URL,
region_name=REGION, endpoint_type='adminURL')
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'admin')
# Test public url
self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME,
password=PASSWORD, auth_url=AUTH_URL,
region_name=REGION, endpoint_type='publicURL')
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'public')
# Test internal url
self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME,
password=PASSWORD, auth_url=AUTH_URL,
region_name=REGION,
endpoint_type='internalURL')
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'internal')
# Test url that isn't found in the service catalog
self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME,
password=PASSWORD, auth_url=AUTH_URL,
region_name=REGION,
endpoint_type='privateURL')
self.assertRaises(exceptions.EndpointTypeNotFound,
self.client._extract_service_catalog,
resources)

@ -143,3 +143,31 @@ class ShellTest(testtools.TestCase):
self.mox.VerifyAll()
self.mox.UnsetStubs()
self.assertEqual(ret, 0)
def test_endpoint_option(self):
shell = openstack_shell.QuantumShell('2.0')
parser = shell.build_option_parser('descr', '2.0')
# Neither $OS_ENDPOINT_TYPE nor --endpoint-type
namespace = parser.parse_args([])
self.assertEqual('publicURL', namespace.endpoint_type)
# --endpoint-type but not $OS_ENDPOINT_TYPE
namespace = parser.parse_args(['--endpoint-type=admin'])
self.assertEqual('admin', namespace.endpoint_type)
def test_endpoint_environment_variable(self):
fixture = fixtures.EnvironmentVariable("OS_ENDPOINT_TYPE",
"public")
self.useFixture(fixture)
shell = openstack_shell.QuantumShell('2.0')
parser = shell.build_option_parser('descr', '2.0')
# $OS_ENDPOINT_TYPE but not --endpoint-type
namespace = parser.parse_args([])
self.assertEqual("public", namespace.endpoint_type)
# --endpoint-type and $OS_ENDPOINT_TYPE
namespace = parser.parse_args(['--endpoint-type=admin'])
self.assertEqual('admin', namespace.endpoint_type)