Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 Boudjnah authored August 02, 2012
70  novaclient/client.py
@@ -7,25 +7,26 @@
7 7
 OpenStack Client interface. Handles the REST calls and responses.
8 8
 """
9 9
 
10  
-import httplib2
11  
-
12  
-has_keyring = False
13  
-try:
14  
-    import keyring
15  
-    has_keyring = True
16  
-except ImportError:
17  
-    pass
18  
-
19 10
 import logging
20 11
 import os
21 12
 import time
22 13
 import urlparse
23 14
 
  15
+import httplib2
  16
+import pkg_resources
  17
+
24 18
 try:
25 19
     import json
26 20
 except ImportError:
27 21
     import simplejson as json
28 22
 
  23
+has_keyring = False
  24
+try:
  25
+    import keyring
  26
+    has_keyring = True
  27
+except ImportError:
  28
+    pass
  29
+
29 30
 # Python 2.5 compat fix
30 31
 if not hasattr(urlparse, 'parse_qsl'):
31 32
     import cgi
@@ -36,21 +37,32 @@
36 37
 from novaclient import utils
37 38
 
38 39
 
  40
+def get_auth_system_url(auth_system):
  41
+    """Load plugin-based auth_url"""
  42
+    ep_name = 'openstack.client.auth_url'
  43
+    for ep in pkg_resources.iter_entry_points(ep_name):
  44
+        if ep.name == auth_system:
  45
+            return ep.load()()
  46
+    raise exceptions.AuthSystemNotFound(auth_system)
  47
+
  48
+
39 49
 class HTTPClient(httplib2.Http):
40 50
 
41 51
     USER_AGENT = 'python-novaclient'
42 52
 
43  
-    def __init__(self, user, password, projectid, auth_url, insecure=False,
44  
-                 timeout=None, proxy_tenant_id=None,
  53
+    def __init__(self, user, password, projectid, auth_url=None,
  54
+                 insecure=False, timeout=None, proxy_tenant_id=None,
45 55
                  proxy_token=None, region_name=None,
46 56
                  endpoint_type='publicURL', service_type=None,
47 57
                  service_name=None, volume_service_name=None,
48 58
                  timings=False, bypass_url=None, no_cache=False,
49  
-                 http_log_debug=False):
  59
+                 http_log_debug=False, auth_system='keystone'):
50 60
         super(HTTPClient, self).__init__(timeout=timeout)
51 61
         self.user = user
52 62
         self.password = password
53 63
         self.projectid = projectid
  64
+        if not auth_url and auth_system and auth_system != 'keystone':
  65
+            auth_url = get_auth_system_url(auth_system)
54 66
         self.auth_url = auth_url.rstrip('/')
55 67
         self.version = 'v1.1'
56 68
         self.region_name = region_name
@@ -75,6 +87,8 @@ def __init__(self, user, password, projectid, auth_url, insecure=False,
75 87
         self.force_exception_to_status_code = True
76 88
         self.disable_ssl_certificate_validation = insecure
77 89
 
  90
+        self.auth_system = auth_system
  91
+
78 92
         self._logger = logging.getLogger(__name__)
79 93
         if self.http_log_debug:
80 94
             ch = logging.StreamHandler()
@@ -199,7 +213,6 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True):
199 213
                 self.auth_url = url
200 214
                 self.service_catalog = \
201 215
                     service_catalog.ServiceCatalog(body)
202  
-
203 216
                 if extract_token:
204 217
                     self.auth_token = self.service_catalog.get_token()
205 218
 
@@ -289,13 +302,20 @@ def authenticate(self):
289 302
         admin_url = urlparse.urlunsplit(
290 303
                         (scheme, new_netloc, path, query, frag))
291 304
 
  305
+        # FIXME(chmouel): This is to handle backward compatibiliy when
  306
+        # we didn't have a plugin mechanism for the auth_system. This
  307
+        # should be removed in the future and have people move to
  308
+        # OS_AUTH_SYSTEM=rackspace instead.
  309
+        if "NOVA_RAX_AUTH" in os.environ:
  310
+            self.auth_system = "rackspace"
  311
+
292 312
         auth_url = self.auth_url
293 313
         if self.version == "v2.0":  # FIXME(chris): This should be better.
294 314
             while auth_url:
295  
-                if "NOVA_RAX_AUTH" in os.environ:
296  
-                    auth_url = self._rax_auth(auth_url)
297  
-                else:
  315
+                if not self.auth_system or self.auth_system == 'keystone':
298 316
                     auth_url = self._v2_auth(auth_url)
  317
+                else:
  318
+                    auth_url = self._plugin_auth(auth_url)
299 319
 
300 320
             # Are we acting on behalf of another user via an
301 321
             # existing token? If so, our actual endpoints may
@@ -354,6 +374,14 @@ def _v1_auth(self, url):
354 374
         else:
355 375
             raise exceptions.from_response(resp, body)
356 376
 
  377
+    def _plugin_auth(self, auth_url):
  378
+        """Load plugin-based authentication"""
  379
+        ep_name = 'openstack.client.authenticate'
  380
+        for ep in pkg_resources.iter_entry_points(ep_name):
  381
+            if ep.name == self.auth_system:
  382
+                return ep.load()(self, auth_url)
  383
+        raise exceptions.AuthSystemNotFound(self.auth_system)
  384
+
357 385
     def _v2_auth(self, url):
358 386
         """Authenticate against a v2.0 auth service."""
359 387
         body = {"auth": {
@@ -365,16 +393,6 @@ def _v2_auth(self, url):
365 393
 
366 394
         self._authenticate(url, body)
367 395
 
368  
-    def _rax_auth(self, url):
369  
-        """Authenticate against the Rackspace auth service."""
370  
-        body = {"auth": {
371  
-                "RAX-KSKEY:apiKeyCredentials": {
372  
-                        "username": self.user,
373  
-                        "apiKey": self.password,
374  
-                        "tenantName": self.projectid}}}
375  
-
376  
-        self._authenticate(url, body)
377  
-
378 396
     def _authenticate(self, url, body):
379 397
         """Authenticate and extract the service catalog."""
380 398
         token_url = url + "/tokens"
9  novaclient/exceptions.py
@@ -22,6 +22,15 @@ class NoUniqueMatch(Exception):
22 22
     pass
23 23
 
24 24
 
  25
+class AuthSystemNotFound(Exception):
  26
+    """When the user specify a AuthSystem but not installed."""
  27
+    def __init__(self, auth_system):
  28
+        self.auth_system = auth_system
  29
+
  30
+    def __str__(self):
  31
+        return "AuthSystemNotFound: %s" % repr(self.auth_system)
  32
+
  33
+
25 34
 class NoTokenLookupException(Exception):
26 35
     """This form of authentication does not support looking up
27 36
        endpoints from an existing token."""
28  novaclient/shell.py
@@ -116,6 +116,10 @@ def get_base_parser(self):
116 116
             default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'),
117 117
             help='Defaults to env[OS_REGION_NAME].')
118 118
 
  119
+        parser.add_argument('--os_auth_system',
  120
+            default=utils.env('OS_AUTH_SYSTEM'),
  121
+            help='Defaults to env[OS_AUTH_SYSTEM].')
  122
+
119 123
         parser.add_argument('--service_type',
120 124
             help='Defaults to compute for most actions')
121 125
 
@@ -312,16 +316,16 @@ def main(self, argv):
312 316
             return 0
313 317
 
314 318
         (os_username, os_password, os_tenant_name, os_auth_url,
315  
-                os_region_name, endpoint_type, insecure,
  319
+                os_region_name, os_auth_system, endpoint_type, insecure,
316 320
                 service_type, service_name, volume_service_name,
317 321
                 username, apikey, projectid, url, region_name,
318 322
                 bypass_url, no_cache) = (
319 323
                         args.os_username, args.os_password,
320 324
                         args.os_tenant_name, args.os_auth_url,
321  
-                        args.os_region_name, args.endpoint_type,
322  
-                        args.insecure, args.service_type, args.service_name,
323  
-                        args.volume_service_name, args.username,
324  
-                        args.apikey, args.projectid,
  325
+                        args.os_region_name, args.os_auth_system,
  326
+                        args.endpoint_type, args.insecure, args.service_type,
  327
+                        args.service_name, args.volume_service_name,
  328
+                        args.username, args.apikey, args.projectid,
325 329
                         args.url, args.region_name,
326 330
                         args.bypass_url, args.no_cache)
327 331
 
@@ -361,11 +365,19 @@ def main(self, argv):
361 365
 
362 366
             if not os_auth_url:
363 367
                 if not url:
364  
-                    raise exc.CommandError("You must provide an auth url "
365  
-                            "via either --os_auth_url or env[OS_AUTH_URL]")
  368
+                    if os_auth_system and os_auth_system != 'keystone':
  369
+                        os_auth_url = \
  370
+                            client.get_auth_system_url(os_auth_system)
366 371
                 else:
367 372
                     os_auth_url = url
368 373
 
  374
+            if not os_auth_url:
  375
+                    raise exc.CommandError("You must provide an auth url "
  376
+                            "via either --os_auth_url or env[OS_AUTH_URL] "
  377
+                            "or specify an auth_system which defines a "
  378
+                            "default url with --os_auth_system "
  379
+                            "or env[OS_AUTH_SYSTEM")
  380
+
369 381
             if not os_region_name and region_name:
370 382
                 os_region_name = region_name
371 383
 
@@ -383,7 +395,7 @@ def main(self, argv):
383 395
                 os_password, os_tenant_name, os_auth_url, insecure,
384 396
                 region_name=os_region_name, endpoint_type=endpoint_type,
385 397
                 extensions=self.extensions, service_type=service_type,
386  
-                service_name=service_name,
  398
+                service_name=service_name, auth_system=os_auth_system,
387 399
                 volume_service_name=volume_service_name,
388 400
                 timings=args.timings, bypass_url=bypass_url,
389 401
                 no_cache=no_cache, http_log_debug=options.debug)
6  novaclient/v1_1/client.py
@@ -41,13 +41,14 @@ class Client(object):
41 41
     """
42 42
 
43 43
     # FIXME(jesse): project_id isn't required to authenticate
44  
-    def __init__(self, username, api_key, project_id, auth_url,
  44
+    def __init__(self, username, api_key, project_id, auth_url=None,
45 45
                   insecure=False, timeout=None, proxy_tenant_id=None,
46 46
                   proxy_token=None, region_name=None,
47 47
                   endpoint_type='publicURL', extensions=None,
48 48
                   service_type='compute', service_name=None,
49 49
                   volume_service_name=None, timings=False,
50  
-                  bypass_url=None, no_cache=False, http_log_debug=False):
  50
+                  bypass_url=None, no_cache=False, http_log_debug=False,
  51
+                  auth_system='keystone'):
51 52
         # FIXME(comstud): Rename the api_key argument above when we
52 53
         # know it's not being used as keyword argument
53 54
         password = api_key
@@ -92,6 +93,7 @@ def __init__(self, username, api_key, project_id, auth_url,
92 93
                                     auth_url,
93 94
                                     insecure=insecure,
94 95
                                     timeout=timeout,
  96
+                                    auth_system=auth_system,
95 97
                                     proxy_token=proxy_token,
96 98
                                     proxy_tenant_id=proxy_tenant_id,
97 99
                                     region_name=region_name,
159  tests/test_auth_plugins.py
... ...
@@ -0,0 +1,159 @@
  1
+# Copyright 2012 OpenStack LLC.
  2
+# All Rights Reserved.
  3
+#
  4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
  5
+#    not use this file except in compliance with the License. You may obtain
  6
+#    a copy of the License at
  7
+#
  8
+#         http://www.apache.org/licenses/LICENSE-2.0
  9
+#
  10
+#    Unless required by applicable law or agreed to in writing, software
  11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13
+#    License for the specific language governing permissions and limitations
  14
+#    under the License.
  15
+
  16
+import httplib2
  17
+import mock
  18
+import pkg_resources
  19
+
  20
+try:
  21
+    import json
  22
+except ImportError:
  23
+    import simplejson as json
  24
+
  25
+from novaclient import exceptions
  26
+from novaclient.v1_1 import client
  27
+from tests import utils
  28
+
  29
+
  30
+def mock_http_request(resp=None):
  31
+    """Mock an HTTP Request."""
  32
+    if not resp:
  33
+        resp = {
  34
+            "access": {
  35
+                "token": {
  36
+                    "expires": "12345",
  37
+                    "id": "FAKE_ID",
  38
+                },
  39
+                "serviceCatalog": [
  40
+                    {
  41
+                        "type": "compute",
  42
+                        "endpoints": [
  43
+                            {
  44
+                                "region": "RegionOne",
  45
+                                "adminURL": "http://localhost:8774/v1.1",
  46
+                                "internalURL":"http://localhost:8774/v1.1",
  47
+                                "publicURL": "http://localhost:8774/v1.1/",
  48
+                            },
  49
+                        ],
  50
+                    },
  51
+                ],
  52
+            },
  53
+        }
  54
+
  55
+    auth_response = httplib2.Response({
  56
+        "status": 200,
  57
+        "body": json.dumps(resp),
  58
+    })
  59
+    return mock.Mock(return_value=(auth_response,
  60
+                                   json.dumps(resp)))
  61
+
  62
+
  63
+def requested_headers(cs):
  64
+    """Return requested passed headers."""
  65
+    return {
  66
+        'User-Agent': cs.client.USER_AGENT,
  67
+        'Content-Type': 'application/json',
  68
+        'Accept': 'application/json',
  69
+    }
  70
+
  71
+
  72
+class AuthPluginTest(utils.TestCase):
  73
+    def test_auth_system_success(self):
  74
+        class MockEntrypoint(pkg_resources.EntryPoint):
  75
+            def load(self):
  76
+                return self.authenticate
  77
+
  78
+            def authenticate(self, cls, auth_url):
  79
+                cls._authenticate(auth_url, {"fake": "me"})
  80
+
  81
+        def mock_iter_entry_points(_type):
  82
+            if _type == 'openstack.client.authenticate':
  83
+                return [MockEntrypoint("fake", "fake", ["fake"])]
  84
+
  85
+        mock_request = mock_http_request()
  86
+
  87
+        @mock.patch.object(pkg_resources, "iter_entry_points",
  88
+                           mock_iter_entry_points)
  89
+        @mock.patch.object(httplib2.Http, "request", mock_request)
  90
+        def test_auth_call():
  91
+            cs = client.Client("username", "password", "project_id",
  92
+                               "auth_url/v2.0", auth_system="fake",
  93
+                               no_cache=True)
  94
+            cs.client.authenticate()
  95
+
  96
+            headers = requested_headers(cs)
  97
+            token_url = cs.client.auth_url + "/tokens"
  98
+
  99
+            mock_request.assert_called_with(token_url, "POST",
  100
+                                            headers=headers,
  101
+                                            body='{"fake": "me"}')
  102
+
  103
+        test_auth_call()
  104
+
  105
+    def test_auth_system_not_exists(self):
  106
+        def mock_iter_entry_points(_t):
  107
+            return [pkg_resources.EntryPoint("fake", "fake", ["fake"])]
  108
+
  109
+        mock_request = mock_http_request()
  110
+
  111
+        @mock.patch.object(pkg_resources, "iter_entry_points",
  112
+                           mock_iter_entry_points)
  113
+        @mock.patch.object(httplib2.Http, "request", mock_request)
  114
+        def test_auth_call():
  115
+            cs = client.Client("username", "password", "project_id",
  116
+                               "auth_url/v2.0", auth_system="notexists",
  117
+                               no_cache=True)
  118
+            self.assertRaises(exceptions.AuthSystemNotFound,
  119
+                              cs.client.authenticate)
  120
+
  121
+        test_auth_call()
  122
+
  123
+    def test_auth_system_defining_auth_url(self):
  124
+        class MockAuthUrlEntrypoint(pkg_resources.EntryPoint):
  125
+            def load(self):
  126
+                return self.auth_url
  127
+
  128
+            def auth_url(self):
  129
+                return "http://faked/v2.0"
  130
+
  131
+        class MockAuthenticateEntrypoint(pkg_resources.EntryPoint):
  132
+            def load(self):
  133
+                return self.authenticate
  134
+
  135
+            def authenticate(self, cls, auth_url):
  136
+                cls._authenticate(auth_url, {"fake": "me"})
  137
+
  138
+        def mock_iter_entry_points(_type):
  139
+            if _type == 'openstack.client.auth_url':
  140
+                return [MockAuthUrlEntrypoint("fakewithauthurl",
  141
+                                           "fakewithauthurl.plugin",
  142
+                                           ["auth_url"])]
  143
+            elif _type == 'openstack.client.authenticate':
  144
+                return [MockAuthenticateEntrypoint("fakewithauthurl",
  145
+                                                   "fakewithauthurl.plugin",
  146
+                                                   ["auth_url"])]
  147
+        mock_request = mock_http_request()
  148
+
  149
+        @mock.patch.object(pkg_resources, "iter_entry_points",
  150
+                           mock_iter_entry_points)
  151
+        @mock.patch.object(httplib2.Http, "request", mock_request)
  152
+        def test_auth_call():
  153
+            cs = client.Client("username", "password", "project_id",
  154
+                               auth_system="fakewithauthurl",
  155
+                               no_cache=True)
  156
+            cs.client.authenticate()
  157
+            self.assertEquals(cs.client.auth_url, "http://faked/v2.0")
  158
+
  159
+        test_auth_call()
2  tests/v1_1/test_auth.py
@@ -237,7 +237,7 @@ class AuthenticationTests(utils.TestCase):
237 237
     def test_authenticate_success(self):
238 238
         cs = client.Client("username", "password", "project_id", "auth_url",
239 239
                            no_cache=True)
240  
-        management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
  240
+        management_url = 'https://localhost/v1.1/443470'
241 241
         auth_response = httplib2.Response({
242 242
             'status': 204,
243 243
             'x-server-management-url': management_url,

Git Notes

review

Verified+2: Jenkins
Approved+1: Josh Kearney <josh@jk0.org>
Code-Review+2: Josh Kearney <josh@jk0.org>
Code-Review+2: Kevin L. Mitchell <kevin.mitchell@rackspace.com>
Submitted-by: Jenkins
Submitted-at: Wed, 15 Aug 2012 17:58:54 +0000
Reviewed-on: https://review.openstack.org/10716
Project: openstack/python-novaclient
Branch: refs/heads/master

0 notes on commit 86c713b

Please sign in to comment.
Something went wrong with that request. Please try again.