Repository: libcloud
Updated Branches:
  refs/heads/trunk f01509da5 -> 930d05a43


LIBCLOUD-625: Allow for internal GCE authorization with metadata service


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/20d97707
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/20d97707
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/20d97707

Branch: refs/heads/trunk
Commit: 20d977075117f05a0d8cd8ceb91c4dfcd93a7766
Parents: a38ade5
Author: Eric Johnson <[email protected]>
Authored: Sat Oct 18 02:17:00 2014 +0000
Committer: Eric Johnson <[email protected]>
Committed: Tue Oct 21 16:10:49 2014 +0000

----------------------------------------------------------------------
 demos/secrets.py-dist                          |   2 +-
 docs/compute/drivers/gce.rst                   |  21 +++-
 docs/examples/compute/gce/gce_internal_auth.py |   9 ++
 libcloud/common/google.py                      | 108 ++++++++++++++++----
 libcloud/compute/drivers/gce.py                |  24 +++--
 libcloud/test/common/test_google.py            |   8 ++
 libcloud/test/secrets.py-dist                  |   2 +-
 libcloud/utils/connection.py                   |   7 +-
 8 files changed, 148 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/demos/secrets.py-dist
----------------------------------------------------------------------
diff --git a/demos/secrets.py-dist b/demos/secrets.py-dist
index 82c3de1..981286d 100644
--- a/demos/secrets.py-dist
+++ b/demos/secrets.py-dist
@@ -22,7 +22,7 @@ DREAMHOST_PARAMS = ('key',)
 EC2_PARAMS = ('access_id', 'secret')
 ECP_PARAMS = ('user_name', 'password')
 GANDI_PARAMS = ('user',)
-GCE_PARAMS = ('email_address', 'key')  # Service Account Authentication
+GCE_PARAMS = ('[email protected]', 'key')  # Service Account 
Authentication
 #GCE_PARAMS = ('client_id', 'client_secret')  # Installed App Authentication
 GCE_KEYWORD_PARAMS = {'project': 'project_name'}
 HOSTINGCOM_PARAMS = ('user', 'secret')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/docs/compute/drivers/gce.rst
----------------------------------------------------------------------
diff --git a/docs/compute/drivers/gce.rst b/docs/compute/drivers/gce.rst
index 6e91160..9230ca9 100644
--- a/docs/compute/drivers/gce.rst
+++ b/docs/compute/drivers/gce.rst
@@ -20,8 +20,8 @@ Google Compute Engine features:
 
 Connecting to Google Compute Engine
 -----------------------------------
-Libcloud supports two different methods for authenticating to Compute Engine:
-`Service Account`_ and `Installed Application`_
+Libcloud supports three different methods for authenticating:
+`Service Account`_ and `Installed Application`_ and `Internal Authentication_`
 
 Which one should I use?
 
@@ -34,6 +34,11 @@ Which one should I use?
   example, a desktop application for managing VMs that would be used by many
   different people with different Google accounts.
 
+* If you are running your code on an instance inside Google Compute Engine,
+  the GCE driver will consult the internal metadata service to obtain an
+  authorization token. The only parameter required for this type of
+  authorization is your Project ID.
+
 Once you have set up the authentication as described below, you pass the
 authentication information to the driver as described in `Examples`_
 
@@ -69,6 +74,13 @@ To set up Installed Account authentication:
 7. You will also need your "Project ID" which can be found by clicking on the
    "Overview" link on the left sidebar.
 
+Internal Authentication
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To use GCE's internal metadata service to authenticate, simply specify
+your Project ID and let the driver handle the rest.  See the `Examples`_
+below for a sample.
+
 Accessing Google Cloud services from your Libcloud nodes
 --------------------------------------------------------
 In order for nodes created with libcloud to be able to access or manage other
@@ -103,6 +115,11 @@ 
https://github.com/apache/libcloud/blob/trunk/demos/gce_demo.py
 
 .. literalinclude:: /examples/compute/gce/gce_service_account_scopes.py
 
+5. Using GCE Internal Authorization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. literalinclude:: /examples/compute/gce/gce_internal_auth.py
+
 API Docs
 --------
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/docs/examples/compute/gce/gce_internal_auth.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/gce/gce_internal_auth.py 
b/docs/examples/compute/gce/gce_internal_auth.py
new file mode 100644
index 0000000..d3aa6ac
--- /dev/null
+++ b/docs/examples/compute/gce/gce_internal_auth.py
@@ -0,0 +1,9 @@
+from libcloud.compute.types import Provider
+from libcloud.compute.providers import get_driver
+
+# This example assumes you are running on an instance within Google
+# Compute Engine. As such, the only parameter you need to specify is
+# the Project ID. The GCE driver will the consult GCE's internal
+# metadata service for an authorization token.
+ComputeEngine = get_driver(Provider.GCE)
+driver = ComputeEngine(project='your_project_id')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/libcloud/common/google.py
----------------------------------------------------------------------
diff --git a/libcloud/common/google.py b/libcloud/common/google.py
index 52692e6..b805ee1 100644
--- a/libcloud/common/google.py
+++ b/libcloud/common/google.py
@@ -78,6 +78,7 @@ import os
 import socket
 import sys
 
+from libcloud.utils.connection import get_response_object
 from libcloud.utils.py3 import httplib, urlencode, urlparse, PY3
 from libcloud.common.base import (ConnectionUserAndKey, JsonResponse,
                                   PollingConnection)
@@ -99,6 +100,23 @@ except ImportError:
 TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
 
 
+def _is_gce():
+    http_code, http_reason, body = _get_gce_metadata()
+    if http_code == httplib.OK and body:
+        return True
+    return False
+
+
+def _get_gce_metadata(path=''):
+    try:
+        url = "http://metadata/computeMetadata/v1/"; + path.lstrip('/')
+        headers = {'Metadata-Flavor': 'Google'}
+        response = get_response_object(url, headers)
+        return response.status, "", response.body
+    except Exception as e:
+        return -1, str(e), None
+
+
 class GoogleAuthError(LibcloudError):
     """Generic Error class for various authentication errors."""
     def __init__(self, value):
@@ -123,7 +141,16 @@ class JsonParseError(GoogleBaseError):
 
 
 class ResourceNotFoundError(GoogleBaseError):
-    pass
+    def __init__(self, value, http_code, code, driver=None):
+        self.code = code
+        if isinstance(value, dict) and 'message' in value and \
+                value['message'].count('/') == 1 and \
+                value['message'].count('projects/') == 1:
+            value['message'] = value['message'] + ". A missing project " \
+                "error may be an authentication issue. " \
+                "Please  ensure your auth credentials match " \
+                "your project. "
+        super(GoogleBaseError, self).__init__(value, http_code, driver)
 
 
 class QuotaExceededError(GoogleBaseError):
@@ -255,7 +282,7 @@ class GoogleBaseAuthConnection(ConnectionUserAndKey):
     host = 'accounts.google.com'
     auth_path = '/o/oauth2/auth'
 
-    def __init__(self, user_id, key, scopes=None,
+    def __init__(self, user_id, key=None, scopes=None,
                  redirect_uri='urn:ietf:wg:oauth:2.0:oob',
                  login_hint=None, **kwargs):
         """
@@ -318,6 +345,21 @@ class GoogleBaseAuthConnection(ConnectionUserAndKey):
             token_info['expire_time'] = expire_time.strftime(TIMESTAMP_FORMAT)
         return token_info
 
+    def refresh_token(self, token_info):
+        """
+        Refresh the current token.
+
+        Fetch an updated refresh token from internal metadata service.
+
+        :param  token_info: Dictionary containing token information.
+                            (Not used, but here for compatibility)
+        :type   token_info: ``dict``
+
+        :return:  A dictionary containing updated token information.
+        :rtype:   ``dict``
+        """
+        return self.get_new_token()
+
 
 class GoogleInstalledAppAuthConnection(GoogleBaseAuthConnection):
     """Authentication connection for "Installed Application" authentication."""
@@ -450,21 +492,32 @@ class 
GoogleServiceAcctAuthConnection(GoogleBaseAuthConnection):
 
         return self._token_request(request)
 
-    def refresh_token(self, token_info):
-        """
-        Refresh the current token.
-
-        Service Account authentication doesn't supply a "refresh token" so
-        this simply gets a new token using the email address/key.
 
-        :param  token_info: Dictionary containing token information.
-                            (Not used, but here for compatibility)
-        :type   token_info: ``dict``
+class GoogleGCEServiceAcctAuthConnection(GoogleBaseAuthConnection):
+    """Authentication class for self-authentication when used with a GCE
+    istance that supports serviceAccounts.
+    """
+    def get_new_token(self):
+        """
+        Get a new token from the internal metadata service.
 
-        :return:  A dictionary containing updated token information.
+        :return:  Dictionary containing token information
         :rtype:   ``dict``
         """
-        return self.get_new_token()
+        path = '/instance/service-accounts/default/token'
+        http_code, http_reason, token_info = _get_gce_metadata(path)
+        if http_code == httplib.NOT_FOUND:
+            raise ValueError("Service Accounts are not enabled for this "
+                             "GCE instance.")
+        if http_code != httplib.OK:
+            raise ValueError("Internal GCE Authorization failed: "
+                             "'%s'" % str(http_reason))
+        token_info = json.loads(token_info)
+        if 'expires_in' in token_info:
+            expire_time = self._now() + datetime.timedelta(
+                seconds=token_info['expires_in'])
+            token_info['expire_time'] = expire_time.strftime(TIMESTAMP_FORMAT)
+        return token_info
 
 
 class GoogleBaseConnection(ConnectionUserAndKey, PollingConnection):
@@ -475,7 +528,7 @@ class GoogleBaseConnection(ConnectionUserAndKey, 
PollingConnection):
     poll_interval = 2.0
     timeout = 180
 
-    def __init__(self, user_id, key, auth_type=None,
+    def __init__(self, user_id, key=None, auth_type=None,
                  credential_file=None, scopes=None, **kwargs):
         """
         Determine authentication type, set up appropriate authentication
@@ -490,10 +543,16 @@ class GoogleBaseConnection(ConnectionUserAndKey, 
PollingConnection):
                      authentication.
         :type   key: ``str``
 
-        :keyword  auth_type: Accepted values are "SA" or "IA"
-                             ("Service Account" or "Installed Application").
+        :keyword  auth_type: Accepted values are "SA" or "IA" or "GCE"
+                             ("Service Account" or "Installed Application" or
+                             "GCE" if libcloud is being used on a GCE instance
+                             with service account enabled).
+                             If not supplied, auth_type will be guessed based
+                             on value of user_id or if the code is being
+                             executed in a GCE instance.).
                              If not supplied, auth_type will be guessed based
-                             on value of user_id.
+                             on value of user_id or if the code is running
+                             on a GCE instance.
         :type     auth_type: ``str``
 
         :keyword  credential_file: Path to file for caching authentication
@@ -507,10 +566,11 @@ class GoogleBaseConnection(ConnectionUserAndKey, 
PollingConnection):
         self.credential_file = credential_file or '~/.gce_libcloud_auth'
 
         if auth_type is None:
-            # Try to guess.  Service accounts use an email address
-            # as the user id.
+            # Try to guess.
             if '@' in user_id:
                 auth_type = 'SA'
+            elif _is_gce():
+                auth_type = 'GCE'
             else:
                 auth_type = 'IA'
 
@@ -525,14 +585,20 @@ class GoogleBaseConnection(ConnectionUserAndKey, 
PollingConnection):
             ]
         self.token_info = self._get_token_info_from_file()
 
-        if auth_type == 'SA':
+        if auth_type == 'GCE':
+            self.auth_conn = GoogleGCEServiceAcctAuthConnection(
+                user_id, self.scopes, **kwargs)
+        elif auth_type == 'SA':
+            if '@' not in user_id:
+                raise GoogleAuthError('Service Account auth requires a '
+                                      'valid email address')
             self.auth_conn = GoogleServiceAcctAuthConnection(
                 user_id, key, self.scopes, **kwargs)
         elif auth_type == 'IA':
             self.auth_conn = GoogleInstalledAppAuthConnection(
                 user_id, key, self.scopes, **kwargs)
         else:
-            raise GoogleAuthError('auth_type should be \'SA\' or \'IA\'')
+            raise GoogleAuthError('Invalid auth_type: %s' % str(auth_type))
 
         if self.token_info is None:
             self.token_info = self.auth_conn.get_new_token()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/libcloud/compute/drivers/gce.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/gce.py b/libcloud/compute/drivers/gce.py
index 856c334..648ae3e 100644
--- a/libcloud/compute/drivers/gce.py
+++ b/libcloud/compute/drivers/gce.py
@@ -550,8 +550,8 @@ class GCENodeDriver(NodeDriver):
         "userinfo-email": "userinfo.email"
     }
 
-    def __init__(self, user_id, key, datacenter=None, project=None,
-                 auth_type=None, scopes=None, **kwargs):
+    def __init__(self, user_id, key=None, datacenter=None, project=None,
+                 auth_type=None, scopes=None, credential_file=None, **kwargs):
         """
         :param  user_id: The email address (for service accounts) or Client ID
                          (for installed apps) to be used for authentication.
@@ -569,19 +569,30 @@ class GCENodeDriver(NodeDriver):
         :keyword  project: Your GCE project name. (required)
         :type     project: ``str``
 
-        :keyword  auth_type: Accepted values are "SA" or "IA"
-                             ("Service Account" or "Installed Application").
+        :keyword  auth_type: Accepted values are "SA" or "IA" or "GCE"
+                             ("Service Account" or "Installed Application" or
+                             "GCE" if libcloud is being used on a GCE instance
+                             with service account enabled).
                              If not supplied, auth_type will be guessed based
-                             on value of user_id.
+                             on value of user_id or if the code is being
+                             executed in a GCE instance.
         :type     auth_type: ``str``
 
         :keyword  scopes: List of authorization URLs. Default is empty and
                           grants read/write to Compute, Storage, DNS.
         :type     scopes: ``list``
+
+        :keyword  credential_file: Path to file for caching authentication
+                                   information used by GCEConnection.
+        :type     credential_file: ``str``
         """
+
         self.auth_type = auth_type
         self.project = project
         self.scopes = scopes
+        self.credential_file = credential_file or \
+            '~/.gce_libcloud_auth' + '.' + self.project
+
         if not self.project:
             raise ValueError('Project name must be specified using '
                              '"project" keyword.')
@@ -2749,7 +2760,8 @@ class GCENodeDriver(NodeDriver):
     def _ex_connection_class_kwargs(self):
         return {'auth_type': self.auth_type,
                 'project': self.project,
-                'scopes': self.scopes}
+                'scopes': self.scopes,
+                'credential_file': self.credential_file}
 
     def _catch_error(self, ignore_errors=False):
         """

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/libcloud/test/common/test_google.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_google.py 
b/libcloud/test/common/test_google.py
index 2e9c701..d851b06 100644
--- a/libcloud/test/common/test_google.py
+++ b/libcloud/test/common/test_google.py
@@ -31,6 +31,7 @@ from libcloud.common.google import (GoogleAuthError,
                                     GoogleBaseAuthConnection,
                                     GoogleInstalledAppAuthConnection,
                                     GoogleServiceAcctAuthConnection,
+                                    GoogleGCEServiceAcctAuthConnection,
                                     GoogleBaseConnection)
 from libcloud.test.secrets import GCE_PARAMS
 
@@ -125,6 +126,8 @@ class GoogleBaseConnectionTest(LibcloudTestCase):
     GoogleInstalledAppAuthConnection.get_code = lambda x: '1234'
     GoogleServiceAcctAuthConnection.get_new_token = \
         lambda x: x._token_request({})
+    GoogleGCEServiceAcctAuthConnection.get_new_token = \
+        lambda x: x._token_request({})
     GoogleBaseConnection._now = lambda x: datetime.datetime(2013, 6, 26,
                                                             19, 0, 0)
 
@@ -152,6 +155,11 @@ class GoogleBaseConnectionTest(LibcloudTestCase):
         self.assertTrue(isinstance(conn2.auth_conn,
                                    GoogleInstalledAppAuthConnection))
 
+        kwargs['auth_type'] = 'GCE'
+        conn3 = GoogleBaseConnection(*GCE_PARAMS, **kwargs)
+        self.assertTrue(isinstance(conn3.auth_conn,
+                                   GoogleGCEServiceAcctAuthConnection))
+
     def test_add_default_headers(self):
         old_headers = {}
         new_expected_headers = {'Content-Type': 'application/json',

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/libcloud/test/secrets.py-dist
----------------------------------------------------------------------
diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist
index b3525e8..940f455 100644
--- a/libcloud/test/secrets.py-dist
+++ b/libcloud/test/secrets.py-dist
@@ -22,7 +22,7 @@ DREAMHOST_PARAMS = ('key',)
 EC2_PARAMS = ('access_id', 'secret')
 ECP_PARAMS = ('user_name', 'password')
 GANDI_PARAMS = ('user',)
-GCE_PARAMS = ('email_address', 'key')  # Service Account Authentication
+GCE_PARAMS = ('[email protected]', 'key')  # Service Account 
Authentication
 # GCE_PARAMS = ('client_id', 'client_secret')  # Installed App Authentication
 GCE_KEYWORD_PARAMS = {'project': 'project_name'}
 HOSTINGCOM_PARAMS = ('user', 'secret')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/20d97707/libcloud/utils/connection.py
----------------------------------------------------------------------
diff --git a/libcloud/utils/connection.py b/libcloud/utils/connection.py
index db381a3..f507ad3 100644
--- a/libcloud/utils/connection.py
+++ b/libcloud/utils/connection.py
@@ -21,7 +21,7 @@ __all__ = [
 ]
 
 
-def get_response_object(url):
+def get_response_object(url, headers=None):
     """
     Utility function which uses libcloud's connection class to issue an HTTP
     request.
@@ -29,6 +29,9 @@ def get_response_object(url):
     :param url: URL to send the request to.
     :type url: ``str``
 
+    :param headers: Custom request headers.
+    :type headers: ``dict``
+
     :return: Response object.
     :rtype: :class:`Response`.
     """
@@ -38,5 +41,5 @@ def get_response_object(url):
 
     con = Connection(secure=secure, host=parsed_url.netloc)
     response = con.request(method='GET', action=parsed_url.path,
-                           params=parsed_qs)
+                           params=parsed_qs, headers=headers)
     return response

Reply via email to