Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Allow different auth providers via plugin system.

- Remove the NOVA_RAX_AUTH hack and provide (temporary) compatibility
  with the new system.
- Example plugin for RAX and HP provided here :
    RAX - https://github.com/emonty/rackspace-auth-openstack
    HP - https://github.com/emonty/hpcloud-auth-openstack
- Plugin are allowed to specify their own auth_url directly.
- Thanks to mtaylor for helping on this.

Change-Id: Ie96835be617c6a20d9c3fc3bd1536083aecfdc0b
  • Loading branch information...
commit 86c713b17ac8984b54ff767d83ab41037e7a7833 1 parent f15974b
@chmouel chmouel authored
View
70 novaclient/client.py
@@ -7,25 +7,26 @@
OpenStack Client interface. Handles the REST calls and responses.
"""
-import httplib2
-
-has_keyring = False
-try:
- import keyring
- has_keyring = True
-except ImportError:
- pass
-
import logging
import os
import time
import urlparse
+import httplib2
+import pkg_resources
+
try:
import json
except ImportError:
import simplejson as json
+has_keyring = False
+try:
+ import keyring
+ has_keyring = True
+except ImportError:
+ pass
+
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
@@ -36,21 +37,32 @@
from novaclient import utils
+def get_auth_system_url(auth_system):
+ """Load plugin-based auth_url"""
+ ep_name = 'openstack.client.auth_url'
+ for ep in pkg_resources.iter_entry_points(ep_name):
+ if ep.name == auth_system:
+ return ep.load()()
+ raise exceptions.AuthSystemNotFound(auth_system)
+
+
class HTTPClient(httplib2.Http):
USER_AGENT = 'python-novaclient'
- def __init__(self, user, password, projectid, auth_url, insecure=False,
- timeout=None, proxy_tenant_id=None,
+ def __init__(self, user, password, projectid, auth_url=None,
+ insecure=False, timeout=None, proxy_tenant_id=None,
proxy_token=None, region_name=None,
endpoint_type='publicURL', service_type=None,
service_name=None, volume_service_name=None,
timings=False, bypass_url=None, no_cache=False,
- http_log_debug=False):
+ http_log_debug=False, auth_system='keystone'):
super(HTTPClient, self).__init__(timeout=timeout)
self.user = user
self.password = password
self.projectid = projectid
+ if not auth_url and auth_system and auth_system != 'keystone':
+ auth_url = get_auth_system_url(auth_system)
self.auth_url = auth_url.rstrip('/')
self.version = 'v1.1'
self.region_name = region_name
@@ -75,6 +87,8 @@ def __init__(self, user, password, projectid, auth_url, insecure=False,
self.force_exception_to_status_code = True
self.disable_ssl_certificate_validation = insecure
+ self.auth_system = auth_system
+
self._logger = logging.getLogger(__name__)
if self.http_log_debug:
ch = logging.StreamHandler()
@@ -199,7 +213,6 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True):
self.auth_url = url
self.service_catalog = \
service_catalog.ServiceCatalog(body)
-
if extract_token:
self.auth_token = self.service_catalog.get_token()
@@ -289,13 +302,20 @@ def authenticate(self):
admin_url = urlparse.urlunsplit(
(scheme, new_netloc, path, query, frag))
+ # FIXME(chmouel): This is to handle backward compatibiliy when
+ # we didn't have a plugin mechanism for the auth_system. This
+ # should be removed in the future and have people move to
+ # OS_AUTH_SYSTEM=rackspace instead.
+ if "NOVA_RAX_AUTH" in os.environ:
+ self.auth_system = "rackspace"
+
auth_url = self.auth_url
if self.version == "v2.0": # FIXME(chris): This should be better.
while auth_url:
- if "NOVA_RAX_AUTH" in os.environ:
- auth_url = self._rax_auth(auth_url)
- else:
+ if not self.auth_system or self.auth_system == 'keystone':
auth_url = self._v2_auth(auth_url)
+ else:
+ auth_url = self._plugin_auth(auth_url)
# Are we acting on behalf of another user via an
# existing token? If so, our actual endpoints may
@@ -354,6 +374,14 @@ def _v1_auth(self, url):
else:
raise exceptions.from_response(resp, body)
+ def _plugin_auth(self, auth_url):
+ """Load plugin-based authentication"""
+ ep_name = 'openstack.client.authenticate'
+ for ep in pkg_resources.iter_entry_points(ep_name):
+ if ep.name == self.auth_system:
+ return ep.load()(self, auth_url)
+ raise exceptions.AuthSystemNotFound(self.auth_system)
+
def _v2_auth(self, url):
"""Authenticate against a v2.0 auth service."""
body = {"auth": {
@@ -365,16 +393,6 @@ def _v2_auth(self, url):
self._authenticate(url, body)
- def _rax_auth(self, url):
- """Authenticate against the Rackspace auth service."""
- body = {"auth": {
- "RAX-KSKEY:apiKeyCredentials": {
- "username": self.user,
- "apiKey": self.password,
- "tenantName": self.projectid}}}
-
- self._authenticate(url, body)
-
def _authenticate(self, url, body):
"""Authenticate and extract the service catalog."""
token_url = url + "/tokens"
View
9 novaclient/exceptions.py
@@ -22,6 +22,15 @@ class NoUniqueMatch(Exception):
pass
+class AuthSystemNotFound(Exception):
+ """When the user specify a AuthSystem but not installed."""
+ def __init__(self, auth_system):
+ self.auth_system = auth_system
+
+ def __str__(self):
+ return "AuthSystemNotFound: %s" % repr(self.auth_system)
+
+
class NoTokenLookupException(Exception):
"""This form of authentication does not support looking up
endpoints from an existing token."""
View
28 novaclient/shell.py
@@ -116,6 +116,10 @@ def get_base_parser(self):
default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
+ parser.add_argument('--os_auth_system',
+ default=utils.env('OS_AUTH_SYSTEM'),
+ help='Defaults to env[OS_AUTH_SYSTEM].')
+
parser.add_argument('--service_type',
help='Defaults to compute for most actions')
@@ -312,16 +316,16 @@ def main(self, argv):
return 0
(os_username, os_password, os_tenant_name, os_auth_url,
- os_region_name, endpoint_type, insecure,
+ os_region_name, os_auth_system, endpoint_type, insecure,
service_type, service_name, volume_service_name,
username, apikey, projectid, url, region_name,
bypass_url, no_cache) = (
args.os_username, args.os_password,
args.os_tenant_name, args.os_auth_url,
- args.os_region_name, args.endpoint_type,
- args.insecure, args.service_type, args.service_name,
- args.volume_service_name, args.username,
- args.apikey, args.projectid,
+ args.os_region_name, args.os_auth_system,
+ args.endpoint_type, args.insecure, args.service_type,
+ args.service_name, args.volume_service_name,
+ args.username, args.apikey, args.projectid,
args.url, args.region_name,
args.bypass_url, args.no_cache)
@@ -361,11 +365,19 @@ def main(self, argv):
if not os_auth_url:
if not url:
- raise exc.CommandError("You must provide an auth url "
- "via either --os_auth_url or env[OS_AUTH_URL]")
+ if os_auth_system and os_auth_system != 'keystone':
+ os_auth_url = \
+ client.get_auth_system_url(os_auth_system)
else:
os_auth_url = url
+ if not os_auth_url:
+ raise exc.CommandError("You must provide an auth url "
+ "via either --os_auth_url or env[OS_AUTH_URL] "
+ "or specify an auth_system which defines a "
+ "default url with --os_auth_system "
+ "or env[OS_AUTH_SYSTEM")
+
if not os_region_name and region_name:
os_region_name = region_name
@@ -383,7 +395,7 @@ def main(self, argv):
os_password, os_tenant_name, os_auth_url, insecure,
region_name=os_region_name, endpoint_type=endpoint_type,
extensions=self.extensions, service_type=service_type,
- service_name=service_name,
+ service_name=service_name, auth_system=os_auth_system,
volume_service_name=volume_service_name,
timings=args.timings, bypass_url=bypass_url,
no_cache=no_cache, http_log_debug=options.debug)
View
6 novaclient/v1_1/client.py
@@ -41,13 +41,14 @@ class Client(object):
"""
# FIXME(jesse): project_id isn't required to authenticate
- def __init__(self, username, api_key, project_id, auth_url,
+ def __init__(self, username, api_key, project_id, auth_url=None,
insecure=False, timeout=None, proxy_tenant_id=None,
proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='compute', service_name=None,
volume_service_name=None, timings=False,
- bypass_url=None, no_cache=False, http_log_debug=False):
+ bypass_url=None, no_cache=False, http_log_debug=False,
+ auth_system='keystone'):
# FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument
password = api_key
@@ -92,6 +93,7 @@ def __init__(self, username, api_key, project_id, auth_url,
auth_url,
insecure=insecure,
timeout=timeout,
+ auth_system=auth_system,
proxy_token=proxy_token,
proxy_tenant_id=proxy_tenant_id,
region_name=region_name,
View
159 tests/test_auth_plugins.py
@@ -0,0 +1,159 @@
+# Copyright 2012 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 httplib2
+import mock
+import pkg_resources
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+from novaclient import exceptions
+from novaclient.v1_1 import client
+from tests import utils
+
+
+def mock_http_request(resp=None):
+ """Mock an HTTP Request."""
+ if not resp:
+ resp = {
+ "access": {
+ "token": {
+ "expires": "12345",
+ "id": "FAKE_ID",
+ },
+ "serviceCatalog": [
+ {
+ "type": "compute",
+ "endpoints": [
+ {
+ "region": "RegionOne",
+ "adminURL": "http://localhost:8774/v1.1",
+ "internalURL":"http://localhost:8774/v1.1",
+ "publicURL": "http://localhost:8774/v1.1/",
+ },
+ ],
+ },
+ ],
+ },
+ }
+
+ auth_response = httplib2.Response({
+ "status": 200,
+ "body": json.dumps(resp),
+ })
+ return mock.Mock(return_value=(auth_response,
+ json.dumps(resp)))
+
+
+def requested_headers(cs):
+ """Return requested passed headers."""
+ return {
+ 'User-Agent': cs.client.USER_AGENT,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+
+
+class AuthPluginTest(utils.TestCase):
+ def test_auth_system_success(self):
+ class MockEntrypoint(pkg_resources.EntryPoint):
+ def load(self):
+ return self.authenticate
+
+ def authenticate(self, cls, auth_url):
+ cls._authenticate(auth_url, {"fake": "me"})
+
+ def mock_iter_entry_points(_type):
+ if _type == 'openstack.client.authenticate':
+ return [MockEntrypoint("fake", "fake", ["fake"])]
+
+ mock_request = mock_http_request()
+
+ @mock.patch.object(pkg_resources, "iter_entry_points",
+ mock_iter_entry_points)
+ @mock.patch.object(httplib2.Http, "request", mock_request)
+ def test_auth_call():
+ cs = client.Client("username", "password", "project_id",
+ "auth_url/v2.0", auth_system="fake",
+ no_cache=True)
+ cs.client.authenticate()
+
+ headers = requested_headers(cs)
+ token_url = cs.client.auth_url + "/tokens"
+
+ mock_request.assert_called_with(token_url, "POST",
+ headers=headers,
+ body='{"fake": "me"}')
+
+ test_auth_call()
+
+ def test_auth_system_not_exists(self):
+ def mock_iter_entry_points(_t):
+ return [pkg_resources.EntryPoint("fake", "fake", ["fake"])]
+
+ mock_request = mock_http_request()
+
+ @mock.patch.object(pkg_resources, "iter_entry_points",
+ mock_iter_entry_points)
+ @mock.patch.object(httplib2.Http, "request", mock_request)
+ def test_auth_call():
+ cs = client.Client("username", "password", "project_id",
+ "auth_url/v2.0", auth_system="notexists",
+ no_cache=True)
+ self.assertRaises(exceptions.AuthSystemNotFound,
+ cs.client.authenticate)
+
+ test_auth_call()
+
+ def test_auth_system_defining_auth_url(self):
+ class MockAuthUrlEntrypoint(pkg_resources.EntryPoint):
+ def load(self):
+ return self.auth_url
+
+ def auth_url(self):
+ return "http://faked/v2.0"
+
+ class MockAuthenticateEntrypoint(pkg_resources.EntryPoint):
+ def load(self):
+ return self.authenticate
+
+ def authenticate(self, cls, auth_url):
+ cls._authenticate(auth_url, {"fake": "me"})
+
+ def mock_iter_entry_points(_type):
+ if _type == 'openstack.client.auth_url':
+ return [MockAuthUrlEntrypoint("fakewithauthurl",
+ "fakewithauthurl.plugin",
+ ["auth_url"])]
+ elif _type == 'openstack.client.authenticate':
+ return [MockAuthenticateEntrypoint("fakewithauthurl",
+ "fakewithauthurl.plugin",
+ ["auth_url"])]
+ mock_request = mock_http_request()
+
+ @mock.patch.object(pkg_resources, "iter_entry_points",
+ mock_iter_entry_points)
+ @mock.patch.object(httplib2.Http, "request", mock_request)
+ def test_auth_call():
+ cs = client.Client("username", "password", "project_id",
+ auth_system="fakewithauthurl",
+ no_cache=True)
+ cs.client.authenticate()
+ self.assertEquals(cs.client.auth_url, "http://faked/v2.0")
+
+ test_auth_call()
View
2  tests/v1_1/test_auth.py
@@ -237,7 +237,7 @@ class AuthenticationTests(utils.TestCase):
def test_authenticate_success(self):
cs = client.Client("username", "password", "project_id", "auth_url",
no_cache=True)
- management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
+ management_url = 'https://localhost/v1.1/443470'
auth_response = httplib2.Response({
'status': 204,
'x-server-management-url': management_url,
Please sign in to comment.
Something went wrong with that request. Please try again.