Merge "Handle URLs via the session and auth_plugins"

This commit is contained in:
Jenkins
2014-03-25 20:03:40 +00:00
committed by Gerrit Code Review
10 changed files with 401 additions and 16 deletions

View File

@@ -34,3 +34,22 @@ class BaseAuthPlugin(object):
:param session: A session object so the plugin can make HTTP calls.
:return string: A token to use.
"""
def get_endpoint(self, session, **kwargs):
"""Return an endpoint for the client.
There are no required keyword arguments to ``get_endpoint`` as a plugin
implementation should use best effort with the information available to
determine the endpoint. However there are certain standard options that
will be generated by the clients and should be used by plugins:
- ``service_type``: what sort of service is required.
- ``interface``: what visibility the endpoint should have.
- ``region_name``: the region the endpoint exists in.
:param Session session: The session object that the auth_plugin
belongs to.
:returns string: The base URL that will be used to talk to the
required service or None if not available.
"""

View File

@@ -89,3 +89,37 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
self.auth_ref = self.get_auth_ref(session)
return self.auth_ref
def get_endpoint(self, session, service_type=None, interface=None,
region_name=None, **kwargs):
"""Return a valid endpoint for a service.
If a valid token is not present then a new one will be fetched using
the session and kwargs.
:param string service_type: The type of service to lookup the endpoint
for. This plugin will return None (failure)
if service_type is not provided.
:param string interface: The exposure of the endpoint. Should be
`public`, `internal` or `admin`.
Defaults to `public`.
:param string region_name: The region the endpoint should exist in.
(optional)
:raises HTTPError: An error from an invalid HTTP response.
:return string or None: A valid endpoint URL or None if not available.
"""
if not service_type:
LOG.warn('Plugin cannot return an endpoint without knowing the '
'service type that is required. Add service_type to '
'endpoint filtering data.')
return None
if not interface:
interface = 'public'
service_catalog = self.get_access(session).service_catalog
return service_catalog.url_for(service_type=service_type,
endpoint_type=interface,
region_name=region_name)

View File

@@ -252,6 +252,12 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin):
if self.auth_token_from_user:
return self.auth_token_from_user
def get_endpoint(self, session, interface=None, **kwargs):
if interface == 'public':
return self.auth_url
else:
return self.management_url
@auth_token.setter
def auth_token(self, value):
"""Override the auth_token.
@@ -558,25 +564,27 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin):
resp = super(HTTPClient, self).request(url, method, **kwargs)
return resp, self._decode_body(resp)
def _cs_request(self, url, method, **kwargs):
def _cs_request(self, url, method, management=True, **kwargs):
"""Makes an authenticated request to keystone endpoint by
concatenating self.management_url and url and passing in method and
any associated kwargs.
"""
interface = 'admin' if management else 'public'
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('service_type', 'identity')
endpoint_filter.setdefault('interface', interface)
is_management = kwargs.pop('management', True)
if is_management and self.management_url is None:
raise exceptions.AuthorizationFailure(
'Current authorization does not have a known management url')
url_to_use = self.auth_url
if is_management:
url_to_use = self.management_url
if self.region_name:
endpoint_filter.setdefault('region_name', self.region_name)
kwargs.setdefault('authenticated', None)
return self.request(url_to_use + url, method,
**kwargs)
try:
return self.request(url, method, **kwargs)
except exceptions.MissingAuthPlugin:
_logger.info('Cannot get authenticated endpoint without an '
'auth plugin')
raise exceptions.AuthorizationFailure(
'Current authorization does not have a known management url')
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)

View File

@@ -14,6 +14,7 @@ import logging
import requests
import six
from six.moves import urllib
from keystoneclient import exceptions
from keystoneclient.openstack.common import jsonutils
@@ -113,7 +114,7 @@ class Session(object):
@utils.positional(enforcement=utils.positional.WARN)
def request(self, url, method, json=None, original_ip=None,
user_agent=None, redirect=None, authenticated=None,
**kwargs):
endpoint_filter=None, **kwargs):
"""Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
@@ -122,7 +123,11 @@ class Session(object):
Arguments that are not handled are passed through to the requests
library.
:param string url: Fully qualified URL of HTTP request
:param string url: Path or fully qualified URL of HTTP request. If only
a path is provided then endpoint_filter must also be
provided such that the base URL can be determined.
If a fully qualified URL is provided then
endpoint_filter will be ignored.
:param string method: The http method to use. (eg. 'GET', 'POST')
:param string original_ip: Mark this request as forwarded for this ip.
(optional)
@@ -139,6 +144,11 @@ class Session(object):
request, False if not or None for attach if
an auth_plugin is available.
(optional, defaults to None)
:param dict endpoint_filter: Data to be provided to an auth plugin with
which it should be able to determine an
endpoint to use for this request. If not
provided then URL is expected to be a
fully qualified URL. (optional)
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`). Except:
'data' will be overwritten by the data in 'json' param.
@@ -164,6 +174,19 @@ class Session(object):
headers['X-Auth-Token'] = token
# if we are passed a fully qualified URL and a endpoint_filter we
# should ignore the filter. This will make it easier for clients who
# want to overrule the default endpoint_filter data added to all client
# requests. We check fully qualified here by the presence of a host.
url_data = urllib.parse.urlparse(url)
if endpoint_filter and not url_data.netloc:
base_url = self.get_endpoint(**endpoint_filter)
if not base_url:
raise exceptions.EndpointNotFound()
url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/'))
if self.cert:
kwargs.setdefault('cert', self.cert)
@@ -347,3 +370,11 @@ class Session(object):
except exceptions.HTTPError as exc:
raise exceptions.AuthorizationFailure("Authentication failure: "
"%s" % exc)
def get_endpoint(self, **kwargs):
"""Get an endpoint as provided by the auth plugin."""
if not self.auth:
raise exceptions.MissingAuthPlugin('An auth plugin is required to '
'determine the endpoint URL.')
return self.auth.get_endpoint(self, **kwargs)

View File

View File

@@ -13,6 +13,7 @@
# under the License.
import httpretty
from six.moves import urllib
from keystoneclient.auth.identity import v2
from keystoneclient import exceptions
@@ -29,7 +30,52 @@ class V2IdentityPlugin(utils.TestCase):
TEST_PASS = 'password'
TEST_SERVICE_CATALOG = []
TEST_SERVICE_CATALOG = [{
"endpoints": [{
"adminURL": "http://cdn.admin-nets.local:8774/v1.0",
"region": "RegionOne",
"internalURL": "http://127.0.0.1:8774/v1.0",
"publicURL": "http://cdn.admin-nets.local:8774/v1.0/"
}],
"type": "nova_compat",
"name": "nova_compat"
}, {
"endpoints": [{
"adminURL": "http://nova/novapi/admin",
"region": "RegionOne",
"internalURL": "http://nova/novapi/internal",
"publicURL": "http://nova/novapi/public"
}],
"type": "compute",
"name": "nova"
}, {
"endpoints": [{
"adminURL": "http://glance/glanceapi/admin",
"region": "RegionOne",
"internalURL": "http://glance/glanceapi/internal",
"publicURL": "http://glance/glanceapi/public"
}],
"type": "image",
"name": "glance"
}, {
"endpoints": [{
"adminURL": TEST_ADMIN_URL,
"region": "RegionOne",
"internalURL": "http://127.0.0.1:5000/v2.0",
"publicURL": "http://127.0.0.1:5000/v2.0"
}],
"type": "identity",
"name": "keystone"
}, {
"endpoints": [{
"adminURL": "http://swift/swiftapi/admin",
"region": "RegionOne",
"internalURL": "http://swift/swiftapi/internal",
"publicURL": "http://swift/swiftapi/public"
}],
"type": "object-store",
"name": "swift"
}]
def setUp(self):
super(V2IdentityPlugin, self).setUp()
@@ -109,3 +155,55 @@ class V2IdentityPlugin(utils.TestCase):
self.assertRequestBodyIs(json=req)
self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
@httpretty.activate
def _do_service_url_test(self, base_url, endpoint_filter):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
self.stub_url(httpretty.GET, ['path'],
base_url=base_url,
body='SUCCESS', status=200)
a = v2.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
resp = s.get('/path', endpoint_filter=endpoint_filter)
self.assertEqual(resp.status_code, 200)
path = "%s/%s" % (urllib.parse.urlparse(base_url).path, 'path')
self.assertEqual(httpretty.last_request().path, path)
def test_service_url(self):
endpoint_filter = {'service_type': 'compute', 'interface': 'admin'}
self._do_service_url_test('http://nova/novapi/admin', endpoint_filter)
def test_service_url_defaults_to_public(self):
endpoint_filter = {'service_type': 'compute'}
self._do_service_url_test('http://nova/novapi/public', endpoint_filter)
@httpretty.activate
def test_endpoint_filter_without_service_type_fails(self):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
a = v2.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
self.assertRaises(exceptions.EndpointNotFound, s.get, '/path',
endpoint_filter={'interface': 'admin'})
@httpretty.activate
def test_full_url_overrides_endpoint_filter(self):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
self.stub_url(httpretty.GET, [],
base_url='http://testurl/',
body='SUCCESS', status=200)
a = v2.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
resp = s.get('http://testurl/',
endpoint_filter={'service_type': 'compute'})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.text, 'SUCCESS')

View File

@@ -15,6 +15,7 @@
import copy
import httpretty
from six.moves import urllib
from keystoneclient import access
from keystoneclient.auth.identity import v3
@@ -32,7 +33,83 @@ class V3IdentityPlugin(utils.TestCase):
TEST_PASS = 'password'
TEST_SERVICE_CATALOG = []
TEST_SERVICE_CATALOG = [{
"endpoints": [{
"url": "http://cdn.admin-nets.local:8774/v1.0/",
"region": "RegionOne",
"interface": "public"
}, {
"url": "http://127.0.0.1:8774/v1.0",
"region": "RegionOne",
"interface": "internal"
}, {
"url": "http://cdn.admin-nets.local:8774/v1.0",
"region": "RegionOne",
"interface": "admin"
}],
"type": "nova_compat"
}, {
"endpoints": [{
"url": "http://nova/novapi/public",
"region": "RegionOne",
"interface": "public"
}, {
"url": "http://nova/novapi/internal",
"region": "RegionOne",
"interface": "internal"
}, {
"url": "http://nova/novapi/admin",
"region": "RegionOne",
"interface": "admin"
}],
"type": "compute"
}, {
"endpoints": [{
"url": "http://glance/glanceapi/public",
"region": "RegionOne",
"interface": "public"
}, {
"url": "http://glance/glanceapi/internal",
"region": "RegionOne",
"interface": "internal"
}, {
"url": "http://glance/glanceapi/admin",
"region": "RegionOne",
"interface": "admin"
}],
"type": "image",
"name": "glance"
}, {
"endpoints": [{
"url": "http://127.0.0.1:5000/v3",
"region": "RegionOne",
"interface": "public"
}, {
"url": "http://127.0.0.1:5000/v3",
"region": "RegionOne",
"interface": "internal"
}, {
"url": TEST_ADMIN_URL,
"region": "RegionOne",
"interface": "admin"
}],
"type": "identity"
}, {
"endpoints": [{
"url": "http://swift/swiftapi/public",
"region": "RegionOne",
"interface": "public"
}, {
"url": "http://swift/swiftapi/internal",
"region": "RegionOne",
"interface": "internal"
}, {
"url": "http://swift/swiftapi/admin",
"region": "RegionOne",
"interface": "admin"
}],
"type": "object-store"
}]
def setUp(self):
super(V3IdentityPlugin, self).setUp()
@@ -232,3 +309,55 @@ class V3IdentityPlugin(utils.TestCase):
username=self.TEST_USER, password=self.TEST_PASS,
domain_id='x', trust_id='x')
self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s)
@httpretty.activate
def _do_service_url_test(self, base_url, endpoint_filter):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
self.stub_url(httpretty.GET, ['path'],
base_url=base_url,
body='SUCCESS', status=200)
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
resp = s.get('/path', endpoint_filter=endpoint_filter)
self.assertEqual(resp.status_code, 200)
path = "%s/%s" % (urllib.parse.urlparse(base_url).path, 'path')
self.assertEqual(httpretty.last_request().path, path)
def test_service_url(self):
endpoint_filter = {'service_type': 'compute', 'interface': 'admin'}
self._do_service_url_test('http://nova/novapi/admin', endpoint_filter)
def test_service_url_defaults_to_public(self):
endpoint_filter = {'service_type': 'compute'}
self._do_service_url_test('http://nova/novapi/public', endpoint_filter)
@httpretty.activate
def test_endpoint_filter_without_service_type_fails(self):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
self.assertRaises(exceptions.EndpointNotFound, s.get, '/path',
endpoint_filter={'interface': 'admin'})
@httpretty.activate
def test_full_url_overrides_endpoint_filter(self):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
self.stub_url(httpretty.GET, [],
base_url='http://testurl/',
body='SUCCESS', status=200)
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
resp = s.get('http://testurl/',
endpoint_filter={'service_type': 'compute'})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.text, 'SUCCESS')

View File

@@ -281,18 +281,41 @@ class AuthPlugin(base.BaseAuthPlugin):
TEST_TOKEN = 'aToken'
SERVICE_URLS = {
'identity': {'public': 'http://identity-public:1111/v2.0',
'admin': 'http://identity-admin:1111/v2.0'},
'compute': {'public': 'http://compute-public:2222/v1.0',
'admin': 'http://compute-admin:2222/v1.0'},
'image': {'public': 'http://image-public:3333/v2.0',
'admin': 'http://image-admin:3333/v2.0'}
}
def __init__(self, token=TEST_TOKEN):
self.token = token
def get_token(self, session):
return self.token
def get_endpoint(self, session, service_type=None, interface=None,
**kwargs):
try:
return self.SERVICE_URLS[service_type][interface]
except (KeyError, AttributeError):
return None
class SessionAuthTests(utils.TestCase):
TEST_URL = 'http://127.0.0.1:5000/'
TEST_JSON = {'hello': 'world'}
def stub_service_url(self, service_type, interface, path,
method=httpretty.GET, **kwargs):
base_url = AuthPlugin.SERVICE_URLS[service_type][interface]
uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/'))
httpretty.register_uri(method, uri, **kwargs)
@httpretty.activate
def test_auth_plugin_default_with_plugin(self):
self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON)
@@ -315,3 +338,40 @@ class SessionAuthTests(utils.TestCase):
self.assertDictEqual(resp.json(), self.TEST_JSON)
self.assertRequestHeaderEqual('X-Auth-Token', None)
@httpretty.activate
def test_service_type_urls(self):
service_type = 'compute'
interface = 'public'
path = '/instances'
status = 200
body = 'SUCCESS'
self.stub_service_url(service_type=service_type,
interface=interface,
path=path,
status=status,
body=body)
sess = client_session.Session(auth=AuthPlugin())
resp = sess.get(path,
endpoint_filter={'service_type': service_type,
'interface': interface})
self.assertEqual(httpretty.last_request().path, '/v1.0/instances')
self.assertEqual(resp.text, body)
self.assertEqual(resp.status_code, status)
def test_service_url_raises_if_no_auth_plugin(self):
sess = client_session.Session()
self.assertRaises(exceptions.MissingAuthPlugin,
sess.get, '/path',
endpoint_filter={'service_type': 'compute',
'interface': 'public'})
def test_service_url_raises_if_no_url_returned(self):
sess = client_session.Session(auth=AuthPlugin())
self.assertRaises(exceptions.EndpointNotFound,
sess.get, '/path',
endpoint_filter={'service_type': 'unknown',
'interface': 'public'})

View File

@@ -171,6 +171,9 @@ class Client(httpclient.HTTPClient):
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
_logger.debug("Authorization Failed.")
raise
except exceptions.EndpointNotFound:
msg = 'There was no suitable authentication url for this request'
raise exceptions.AuthorizationFailure(msg)
except Exception as e:
raise exceptions.AuthorizationFailure("Authorization Failed: "
"%s" % e)

View File

@@ -168,6 +168,9 @@ class Client(httpclient.HTTPClient):
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
_logger.debug('Authorization failed.')
raise
except exceptions.EndpointNotFound:
msg = 'There was no suitable authentication url for this request'
raise exceptions.AuthorizationFailure(msg)
except Exception as e:
raise exceptions.AuthorizationFailure('Authorization failed: '
'%s' % e)