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:
parent
f15974b80d
commit
86c713b17a
@ -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
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user