Handle Ambiguous Endpoints Correctly

- Added --service_name argument to allow selecting
  endpoints by service name
- Renamed endpoint_name argument to endpoint_type (this breaks
  compatibility)
- Return AmbiguousEndpoints error if more than one endpoint
  matches filter
- Also addresses bug 924052

Use case:
  $ nova --projectid xxx --version 1.1 --password xxx --username xxx --url https://identity.openstackcloud.com/ image-list
  Found more than one valid endpoint. Use a more restrictive filter
  AmbiguousEndpoints: [
    {'serviceName': 'New Cloud', 'region': 'Test', 'publicURL': 'https://test.openstackcloud.com/v1.1/tttt', 'tenantId': 'tttt'},
    {'serviceName': 'Old Cloud', 'publicURL': 'https://servers.openstackcloud.com/v1.0/tttt', 'tenantId': 'tttt'}]

  $ nova --projectid tttt --version 1.1 --password xxx --username xxx --url https://identity.openstackcloud.com/ --service_name 'New Cloud' image-list
  +--------------------------------------+-----------------------------+--------+--------+
  |                  ID                  |             Name            | Status | Server |
  +--------------------------------------+-----------------------------+--------+--------+
  | 346f4039-a81e-4444-9223-4a3d13592a07 | Debian Squeeze (6.0)        | ACTIVE |        |
  | ac8985ea-c09e-4544-82af-eb459a02a6b2 | Fedora 15                   | ACTIVE |        |
  | ddddc02e-92fa-4f44-b36f-55b39bf66a67 | CentOS 5.6                  | ACTIVE |        |
  +--------------------------------------+-----------------------------+--------+--------+

Change-Id: I9a10b9ad5e5b9cf6e762659013496a93a79774db
This commit is contained in:
Ziad Sawalha 2012-01-31 18:08:22 -06:00
parent c3a0c702ee
commit 38bc7ea570
7 changed files with 109 additions and 23 deletions

View File

@ -36,7 +36,7 @@ class HTTPClient(httplib2.Http):
def __init__(self, user, password, projectid, auth_url, insecure=False,
timeout=None, token=None, region_name=None,
endpoint_name='publicURL'):
endpoint_type='publicURL', service_name=None):
super(HTTPClient, self).__init__(timeout=timeout)
self.user = user
self.password = password
@ -44,7 +44,8 @@ class HTTPClient(httplib2.Http):
self.auth_url = auth_url
self.version = 'v1.1'
self.region_name = region_name
self.endpoint_name = endpoint_name
self.endpoint_type = endpoint_type
self.service_name = service_name
self.management_url = None
self.auth_token = None
@ -154,8 +155,13 @@ class HTTPClient(httplib2.Http):
self.management_url = self.service_catalog.url_for(
attr='region',
filter_value=self.region_name,
endpoint_type=self.endpoint_name)
endpoint_type=self.endpoint_type,
service_name=self.service_name)
return None
except exceptions.AmbiguousEndpoints, exc:
print "Found more than one valid endpoint. Use a more " \
"restrictive filter"
raise
except KeyError:
raise exceptions.AuthorizationFailure()
except exceptions.EndpointNotFound:

View File

@ -29,6 +29,15 @@ class EndpointNotFound(Exception):
pass
class AmbiguousEndpoints(Exception):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
self.endpoints = endpoints
def __str__(self):
return "AmbiguousEndpoints: %s" % repr(self.endpoints)
class ClientException(Exception):
"""
The base exception class for all exceptions this library raises.

View File

@ -29,16 +29,19 @@ class ServiceCatalog(object):
return self.catalog['access']['token']['id']
def url_for(self, attr=None, filter_value=None,
service_type='compute', endpoint_type='publicURL'):
service_type='compute', endpoint_type='publicURL',
service_name=None):
"""Fetch the public URL from the Compute service for
a particular endpoint attribute. If none given, return
the first. See tests for sample service catalog."""
matching_endpoints = []
if 'endpoints' in self.catalog:
# We have a bastardized service catalog. Treat it special. :/
for endpoint in self.catalog['endpoints']:
if not filter_value or endpoint[attr] == filter_value:
return endpoint[endpoint_type]
raise novaclient.exceptions.EndpointNotFound()
matching_endpoints.append(endpoint)
if not matching_endpoints:
raise novaclient.exceptions.EndpointNotFound()
# We don't always get a service catalog back ...
if not 'serviceCatalog' in self.catalog['access']:
@ -51,9 +54,19 @@ class ServiceCatalog(object):
if service['type'] != service_type:
continue
if service_name and service.get('name') != service_name:
continue
endpoints = service['endpoints']
for endpoint in endpoints:
if not filter_value or endpoint[attr] == filter_value:
return endpoint[endpoint_type]
endpoint["serviceName"] = service.get("name")
matching_endpoints.append(endpoint)
raise novaclient.exceptions.EndpointNotFound()
if not matching_endpoints:
raise novaclient.exceptions.EndpointNotFound()
elif len(matching_endpoints) > 1:
raise novaclient.exceptions.AmbiguousEndpoints(
endpoints=matching_endpoints)
else:
return matching_endpoints[0][endpoint_type]

View File

@ -111,9 +111,13 @@ class OpenStackComputeShell(object):
default=env('NOVA_REGION_NAME'),
help='Defaults to env[NOVA_REGION_NAME].')
parser.add_argument('--endpoint_name',
default=env('NOVA_ENDPOINT_NAME'),
help='Defaults to env[NOVA_ENDPOINT_NAME] or "publicURL".')
parser.add_argument('--service_name',
default=env('NOVA_SERVICE_NAME'),
help='Defaults to env[NOVA_SERVICE_NAME]')
parser.add_argument('--endpoint_type',
default=env('NOVA_ENDPOINT_TYPE'),
help='Defaults to env[NOVA_ENDPOINT_TYPE] or "publicURL".')
parser.add_argument('--version',
default=env('NOVA_VERSION'),
@ -247,12 +251,13 @@ class OpenStackComputeShell(object):
return 0
(user, apikey, password, projectid, url, region_name,
endpoint_name, insecure) = (args.username, args.apikey,
args.password, args.projectid, args.url,
args.region_name, args.endpoint_name, args.insecure)
endpoint_type, insecure, service_name) = (args.username,
args.apikey, args.password, args.projectid, args.url,
args.region_name, args.endpoint_type, args.insecure,
args.service_name)
if not endpoint_name:
endpoint_name = 'publicURL'
if not endpoint_type:
endpoint_type = 'publicURL'
#FIXME(usrleon): Here should be restrict for project id same as
# for username or password but for compatibility it is not.
@ -294,8 +299,9 @@ class OpenStackComputeShell(object):
self.cs = client.Client(options.version, user, password,
projectid, url, insecure,
region_name=region_name,
endpoint_name=endpoint_name,
extensions=self.extensions)
endpoint_type=endpoint_type,
extensions=self.extensions,
service_name=service_name)
try:
if not utils.isunauthenticated(args.func):

View File

@ -39,7 +39,8 @@ class Client(object):
# FIXME(jesse): project_id isn't required to authenticate
def __init__(self, username, api_key, project_id, auth_url,
insecure=False, timeout=None, token=None, region_name=None,
endpoint_name='publicURL', extensions=None):
endpoint_type='publicURL', extensions=None,
service_name=None):
# FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument
password = api_key
@ -82,7 +83,8 @@ class Client(object):
timeout=timeout,
token=token,
region_name=region_name,
endpoint_name=endpoint_name)
endpoint_type=endpoint_type,
service_name=service_name)
def authenticate(self):
"""

View File

@ -103,8 +103,7 @@ class ServiceCatalogTest(utils.TestCase):
def test_building_a_service_catalog(self):
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
self.assertEquals(sc.url_for(),
"https://compute1.host/v1/1234")
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for)
self.assertEquals(sc.url_for('tenantId', '1'),
"https://compute1.host/v1/1234")
self.assertEquals(sc.url_for('tenantId', '2'),

View File

@ -28,11 +28,11 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
},
"serviceCatalog": [
{
"adminURL": "http://localhost:8774/v1.1",
"type": "compute",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://localhost:8774/v1.1",
"internalURL": "http://localhost:8774/v1.1",
"publicURL": "http://localhost:8774/v1.1/",
},
@ -177,6 +177,57 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
test_auth_call()
def test_ambiguous_endpoints(self):
cs = client.Client("username", "password", "project_id",
"auth_url/v2.0")
resp = {
"access": {
"token": {
"expires": "12345",
"id": "FAKE_ID",
},
"serviceCatalog": [
{
"adminURL": "http://localhost:8774/v1.1",
"type": "compute",
"name": "Compute CLoud",
"endpoints": [
{
"region": "RegionOne",
"internalURL": "http://localhost:8774/v1.1",
"publicURL": "http://localhost:8774/v1.1/",
},
],
},
{
"adminURL": "http://localhost:8774/v1.1",
"type": "compute",
"name": "Hyper-compute Cloud",
"endpoints": [
{
"internalURL": "http://localhost:8774/v1.1",
"publicURL": "http://localhost:8774/v1.1/",
},
],
},
],
},
}
auth_response = httplib2.Response({
"status": 200,
"body": json.dumps(resp),
})
mock_request = mock.Mock(return_value=(auth_response,
json.dumps(resp)))
@mock.patch.object(httplib2.Http, "request", mock_request)
def test_auth_call():
self.assertRaises(exceptions.AmbiguousEndpoints,
cs.client.authenticate)
test_auth_call()
class AuthenticationTests(utils.TestCase):
def test_authenticate_success(self):