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
This commit is contained in:
Chmouel Boudjnah 2012-08-02 16:41:47 +02:00
parent f15974b80d
commit 86c713b17a
6 changed files with 238 additions and 38 deletions

@ -7,7 +7,18 @@
OpenStack Client interface. Handles the REST calls and responses.
"""
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:
@ -16,16 +27,6 @@ try:
except ImportError:
pass
import logging
import os
import time
import urlparse
try:
import json
except ImportError:
import simplejson as json
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
@ -36,21 +37,32 @@ from novaclient import service_catalog
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 @@ class HTTPClient(httplib2.Http):
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 @@ class HTTPClient(httplib2.Http):
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 @@ class HTTPClient(httplib2.Http):
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 @@ class HTTPClient(httplib2.Http):
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 @@ class HTTPClient(httplib2.Http):
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"

@ -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."""

@ -116,6 +116,10 @@ class OpenStackComputeShell(object):
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 @@ class OpenStackComputeShell(object):
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 @@ class OpenStackComputeShell(object):
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 @@ class OpenStackComputeShell(object):
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)

@ -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 @@ class Client(object):
auth_url,
insecure=insecure,
timeout=timeout,
auth_system=auth_system,
proxy_token=proxy_token,
proxy_tenant_id=proxy_tenant_id,
region_name=region_name,

159
tests/test_auth_plugins.py Normal file

@ -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()

@ -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,