libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From to...@apache.org
Subject [10/10] git commit: Add new driver for Google Compute Engine.
Date Fri, 12 Jul 2013 23:45:25 GMT
Add new driver for Google Compute Engine.

Signed-off-by: Tomaz Muraus <tomaz@apache.org>


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

Branch: refs/heads/0.13.x
Commit: c1980ca76a00dbc5c92b90eda915a4390ff4d65c
Parents: b3a7467
Author: Rick Wright <rickw@google.com>
Authored: Fri Jul 12 12:57:20 2013 -0700
Committer: Tomaz Muraus <tomaz@apache.org>
Committed: Sat Jul 13 01:44:58 2013 +0200

----------------------------------------------------------------------
 demos/gce_demo.py                               |  282 +++
 demos/secrets.py-dist                           |    3 +
 libcloud/common/google.py                       |  520 +++++
 libcloud/compute/drivers/__init__.py            |    1 +
 libcloud/compute/drivers/gce.py                 | 1794 ++++++++++++++++++
 libcloud/compute/providers.py                   |    2 +
 libcloud/compute/types.py                       |    2 +
 libcloud/test/common/test_google.py             |  241 +++
 .../fixtures/gce/aggregated_addresses.json      |   71 +
 .../compute/fixtures/gce/aggregated_disks.json  |   81 +
 .../fixtures/gce/aggregated_instances.json      |  414 ++++
 .../fixtures/gce/aggregated_machineTypes.json   | 1683 ++++++++++++++++
 .../compute/fixtures/gce/global_firewalls.json  |   88 +
 .../gce/global_firewalls_lcfirewall.json        |   19 +
 .../gce/global_firewalls_lcfirewall_delete.json |   14 +
 .../gce/global_firewalls_lcfirewall_put.json    |   14 +
 .../fixtures/gce/global_firewalls_post.json     |   13 +
 .../compute/fixtures/gce/global_images.json     |   22 +
 .../fixtures/gce/global_images.json.save        |   22 +
 .../compute/fixtures/gce/global_networks.json   |   34 +
 .../fixtures/gce/global_networks_default.json   |    9 +
 .../fixtures/gce/global_networks_lcnetwork.json |    9 +
 .../gce/global_networks_lcnetwork_delete.json   |   14 +
 ...l_networks_libcloud-demo-europe-network.json |    9 +
 .../global_networks_libcloud-demo-network.json  |    9 +
 .../fixtures/gce/global_networks_post.json      |   13 +
 ...tion_global_firewalls_lcfirewall_delete.json |   15 +
 ...eration_global_firewalls_lcfirewall_put.json |   15 +
 ...rations_operation_global_firewalls_post.json |   15 +
 ...ration_global_networks_lcnetwork_delete.json |   15 +
 ...erations_operation_global_networks_post.json |   15 +
 ..._us-central1_addresses_lcaddress_delete.json |   15 +
 ...tion_regions_us-central1_addresses_post.json |   15 +
 ...ion_zones_europe-west1-a_instances_post.json |   25 +
 ...zones_us-central1-a_disks_lcdisk_delete.json |   15 +
 ...peration_zones_us-central1-a_disks_post.json |   16 +
 ...-central1-a_instances_lcnode-000_delete.json |   16 +
 ...-central1-a_instances_lcnode-001_delete.json |   16 +
 ...1-a_instances_node-name_attachDisk_post.json |   16 +
 ...s-central1-a_instances_node-name_delete.json |   16 +
 ...1-a_instances_node-name_detachDisk_post.json |   16 +
 ...ntral1-a_instances_node-name_reset_post.json |   15 +
 ...ral1-a_instances_node-name_setTags_post.json |   16 +
 ...tion_zones_us-central1-a_instances_post.json |   16 +
 libcloud/test/compute/fixtures/gce/project.json |   74 +
 .../projects_debian-cloud_global_images.json    |  157 ++
 .../gce/regions_us-central1_addresses.json      |   29 +
 ...regions_us-central1_addresses_lcaddress.json |   11 +
 ..._us-central1_addresses_lcaddress_delete.json |   15 +
 .../gce/regions_us-central1_addresses_post.json |   14 +
 libcloud/test/compute/fixtures/gce/zones.json   |  207 ++
 .../gce/zones_europe-west1-a_instances.json     |  145 ++
 .../zones_europe-west1-a_instances_post.json    |   15 +
 ...rope-west1-a_machineTypes_n1-standard-1.json |   14 +
 .../fixtures/gce/zones_us-central1-a.json       |   40 +
 .../fixtures/gce/zones_us-central1-a_disks.json |   37 +
 .../gce/zones_us-central1-a_disks_lcdisk.json   |   10 +
 ...zones_us-central1-a_disks_lcdisk_delete.json |   15 +
 .../gce/zones_us-central1-a_disks_post.json     |   14 +
 .../gce/zones_us-central1-a_instances.json      |  232 +++
 ...ones_us-central1-a_instances_lcnode-000.json |   42 +
 ...-central1-a_instances_lcnode-000_delete.json |   15 +
 ...ones_us-central1-a_instances_lcnode-001.json |   42 +
 ...-central1-a_instances_lcnode-001_delete.json |   15 +
 ...zones_us-central1-a_instances_node-name.json |   42 +
 ...1-a_instances_node-name_attachDisk_post.json |   15 +
 ...s-central1-a_instances_node-name_delete.json |   15 +
 ...1-a_instances_node-name_detachDisk_post.json |   15 +
 ...ntral1-a_instances_node-name_reset_post.json |   15 +
 ...ral1-a_instances_node-name_setTags_post.json |   15 +
 .../gce/zones_us-central1-a_instances_post.json |   14 +
 .../gce/zones_us-central1-a_machineTypes.json   |  374 ++++
 ...s-central1-a_machineTypes_n1-standard-1.json |   14 +
 libcloud/test/compute/test_gce.py               |  703 +++++++
 libcloud/test/secrets.py-dist                   |    3 +
 75 files changed, 8039 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/c1980ca7/demos/gce_demo.py
----------------------------------------------------------------------
diff --git a/demos/gce_demo.py b/demos/gce_demo.py
new file mode 100755
index 0000000..92a31b2
--- /dev/null
+++ b/demos/gce_demo.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+
+# This example performs several tasks on Google Compute Engine.  It can be run
+# directly or can be imported into an interactive python session.  This can
+# also serve as an integration test for the GCE Node Driver.
+#
+# To run interactively:
+#    - Make sure you have valid values in secrets.py
+#      (For more information about setting up your credentials, see the
+#      libcloud/common/google.py docstring)
+#    - Run 'python' in this directory, then:
+#        import gce_demo
+#        gce = gce_demo.get_gce_driver()
+#        gce.list_nodes()
+#        etc.
+#    - Or, to run the full demo from the interactive python shell:
+#        import gce_demo
+#        gce_demo.CLEANUP = False               # optional
+#        gce_demo.MAX_NODES = 4                 # optional
+#        gce_demo.DATACENTER = 'us-central1-a'  # optional
+#        gce_demo.main()
+
+import os.path
+import sys
+
+try:
+    import secrets
+except ImportError:
+    secrets = None
+
+# Add parent dir of this file's dir to sys.path (OS-agnostically)
+sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__),
+                                 os.path.pardir)))
+
+from libcloud.compute.types import Provider
+from libcloud.compute.providers import get_driver
+
+# Maximum number of 1-CPU nodes to allow to run simultaneously
+MAX_NODES = 5
+
+# String that all resource names created by the demo will start with
+# WARNING: Any resource that has a matching name will be destroyed.
+DEMO_BASE_NAME = 'libcloud-demo'
+
+# Datacenter to create resources in
+DATACENTER = 'us-central1-a'
+
+# Clean up resources at the end (can be set to false in order to
+# inspect resources at the end of the run). Resources will be cleaned
+# at the beginning regardless.
+CLEANUP = True
+
+args = getattr(secrets, 'GCE_PARAMS', ())
+kwargs = getattr(secrets, 'GCE_KEYWORD_PARAMS', {})
+
+
+# ==== HELPER FUNCTIONS ====
+def get_gce_driver():
+    driver = get_driver(Provider.GCE)(*args, datacenter=DATACENTER, **kwargs)
+    return driver
+
+
+def display(title, resource_list):
+    """
+    Display a list of resources.
+
+    @param  title: String to be printed at the heading of the list.
+    @type   title: C{str}
+
+    @param  resource_list: List of resources to display
+    @type   resource_list: Any C{object} with a C{name} attribute
+    """
+    print('%s:' % title)
+    for item in resource_list[:10]:
+        print('   %s' % item.name)
+
+
+def clean_up(base_name, node_list=None, resource_list=None):
+    """
+    Destroy all resources that have a name beginning with 'base_name'.
+
+    @param  base_name: String with the first part of the name of resources
+                       to destroy
+    @type   base_name: C{str}
+
+    @keyword  node_list: List of nodes to consider for deletion
+    @type     node_list: C{list} of L{Node}
+
+    @keyword  resource_list: List of resources to consider for deletion
+    @type     resource_list: C{list} of I{Resource Objects}
+    """
+    if node_list is None:
+        node_list = []
+    if resource_list is None:
+        resource_list = []
+    # Use ex_destroy_multiple_nodes to destroy nodes
+    del_nodes = []
+    for node in node_list:
+        if node.name.startswith(base_name):
+            del_nodes.append(node)
+
+    result = gce.ex_destroy_multiple_nodes(del_nodes)
+    for i, success in enumerate(result):
+        if success:
+            print('   Deleted %s' % del_nodes[i].name)
+        else:
+            print('   Failed to delete %s' % del_nodes[i].name)
+
+    # Destroy everything else with just the destroy method
+    for resource in resource_list:
+        if resource.name.startswith(base_name):
+            if resource.destroy():
+                print('   Deleted %s' % resource.name)
+            else:
+                print('   Failed to Delete %s' % resource.name)
+
+
+# ==== DEMO CODE STARTS HERE ====
+def main():
+    global gce
+    gce = get_gce_driver()
+    # Get project info and print name
+    project = gce.ex_get_project()
+    print('Project: %s' % project.name)
+
+    # == Get Lists of Everything and Display the lists (up to 10) ==
+    # These can either just return values for the current datacenter (zone)
+    # or for everything.
+    all_nodes = gce.list_nodes(ex_zone='all')
+    display('Nodes', all_nodes)
+
+    all_addresses = gce.ex_list_addresses(region='all')
+    display('Addresses', all_addresses)
+
+    all_volumes = gce.list_volumes(ex_zone='all')
+    display('Volumes', all_volumes)
+
+    # This can return everything, but there is a large amount of overlap,
+    # so we'll just get the sizes from the current zone.
+    sizes = gce.list_sizes()
+    display('Sizes', sizes)
+
+    # These are global
+    firewalls = gce.ex_list_firewalls()
+    display('Firewalls', firewalls)
+
+    networks = gce.ex_list_networks()
+    display('Networks', networks)
+
+    images = gce.list_images()
+    display('Images', images)
+
+    locations = gce.list_locations()
+    display('Locations', locations)
+
+    zones = gce.ex_list_zones()
+    display('Zones', zones)
+
+    # == Clean up any old demo resources ==
+    print('Cleaning up any "%s" resources:' % DEMO_BASE_NAME)
+    clean_up(DEMO_BASE_NAME, all_nodes,
+             all_addresses + all_volumes + firewalls + networks)
+
+    # == Create Node with non-persistent disk ==
+    if MAX_NODES > 1:
+        print('Creating Node with non-persistent disk:')
+        name = '%s-np-node' % DEMO_BASE_NAME
+        node_1 = gce.create_node(name, 'n1-standard-1', 'debian-7',
+                                 ex_tags=['libcloud'])
+        print('   Node %s created' % name)
+
+        # == Create, and attach a disk ==
+        print('Creating a new disk:')
+        disk_name = '%s-attach-disk' % DEMO_BASE_NAME
+        volume = gce.create_volume(1, disk_name)
+        if volume.attach(node_1):
+            print ('   Attached %s to %s' % (volume.name, node_1.name))
+
+        if CLEANUP:
+            # == Detach the disk ==
+            if gce.detach_volume(volume, ex_node=node_1):
+                print('   Detached %s from %s' % (volume.name, node_1.name))
+
+    # == Create Node with persistent disk ==
+    print('Creating Node with Persistent disk:')
+    name = '%s-persist-node' % DEMO_BASE_NAME
+    # Use objects this time instead of names
+    # Get latest Debian 7 image
+    image = gce.ex_get_image('debian-7')
+    # Get Machine Size
+    size = gce.ex_get_size('n1-standard-1')
+    # Create Disk.  Size is None to just take default of image
+    volume_name = '%s-boot-disk' % DEMO_BASE_NAME
+    volume = gce.create_volume(None, volume_name, image=image)
+    # Create Node with Disk
+    node_2 = gce.create_node(name, size, image, ex_tags=['libcloud'],
+                             ex_boot_disk=volume)
+    print('   Node %s created with attached disk %s' % (node_2.name,
+                                                        volume.name))
+
+    # == Update Tags for Node ==
+    print('Updating Tags for %s' % node_2.name)
+    tags = node_2.extra['tags']
+    tags.append('newtag')
+    if gce.ex_set_node_tags(node_2, tags):
+        print('   Tags updated for %s' % node_2.name)
+    check_node = gce.ex_get_node(node_2.name)
+    print('   New tags: %s' % check_node.extra['tags'])
+
+    # == Create Multiple nodes at once ==
+    base_name = '%s-multiple-nodes' % DEMO_BASE_NAME
+    number = MAX_NODES - 2
+    if number > 0:
+        print('Creating Multiple Nodes (%s):' % number)
+        multi_nodes = gce.ex_create_multiple_nodes(base_name, size, image,
+                                                   number,
+                                                   ex_tags=['libcloud'])
+        for node in multi_nodes:
+            print('   Node %s created.' % node.name)
+
+    # == Create a Network ==
+    print('Creating Network:')
+    name = '%s-network' % DEMO_BASE_NAME
+    cidr = '10.10.0.0/16'
+    network_1 = gce.ex_create_network(name, cidr)
+    print('   Network %s created' % network_1.name)
+
+    # == Create a Firewall ==
+    print('Creating a Firewall:')
+    name = '%s-firewall' % DEMO_BASE_NAME
+    allowed = [{'IPProtocol': 'tcp',
+                'ports': ['3141']}]
+    firewall_1 = gce.ex_create_firewall(name, allowed, network=network_1,
+                                        source_tags=['libcloud'])
+    print('   Firewall %s created' % firewall_1.name)
+
+    # == Create a Static Address ==
+    print('Creating an Address:')
+    name = '%s-address' % DEMO_BASE_NAME
+    address_1 = gce.ex_create_address(name)
+    print('   Address %s created with IP %s' % (address_1.name,
+                                                address_1.address))
+
+    # == List Updated Resources in current zone/region ==
+    print('Updated Resources in current zone/region:')
+    nodes = gce.list_nodes()
+    display('Nodes', nodes)
+
+    addresses = gce.ex_list_addresses()
+    display('Addresses', addresses)
+
+    volumes = gce.list_volumes()
+    display('Volumes', volumes)
+
+    firewalls = gce.ex_list_firewalls()
+    display('Firewalls', firewalls)
+
+    networks = gce.ex_list_networks()
+    display('Networks', networks)
+
+    if CLEANUP:
+        print('Cleaning up %s resources created.' % DEMO_BASE_NAME)
+        clean_up(DEMO_BASE_NAME, nodes,
+                 addresses + volumes + firewalls + networks)
+
+if __name__ == '__main__':
+    main()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c1980ca7/demos/secrets.py-dist
----------------------------------------------------------------------
diff --git a/demos/secrets.py-dist b/demos/secrets.py-dist
index f7e3fc6..82c3de1 100644
--- a/demos/secrets.py-dist
+++ b/demos/secrets.py-dist
@@ -22,6 +22,9 @@ 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 = ('client_id', 'client_secret')  # Installed App Authentication
+GCE_KEYWORD_PARAMS = {'project': 'project_name'}
 HOSTINGCOM_PARAMS = ('user', 'secret')
 IBM_PARAMS = ('user', 'secret')
 # OPENSTACK_PARAMS = ('user_name', 'api_key', secure_bool, 'host', port_int)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c1980ca7/libcloud/common/google.py
----------------------------------------------------------------------
diff --git a/libcloud/common/google.py b/libcloud/common/google.py
new file mode 100644
index 0000000..5c6e524
--- /dev/null
+++ b/libcloud/common/google.py
@@ -0,0 +1,520 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+"""
+Module for Google Connection and Authentication classes.
+
+Information about setting up your Google OAUTH2 credentials:
+
+For libcloud, there are two basic methods for authenticating to Google using
+OAUTH2: Service Accounts and Client IDs for Installed Applications.
+
+Both are initially set up from the
+U{API Console<https://code.google.com/apis/console#access>}
+
+Setting up Service Account authentication (note that you need the PyCrypto
+package installed to use this):
+    - Go to the API Console
+    - Click on "Create another client ID..."
+    - Select "Service account" and click on "Create client ID"
+    - Download the Private Key
+    - The key that you download is a PKCS12 key.  It needs to be converted to
+      the PEM format.
+    - Convert the key using OpenSSL (the default password is 'notasecret'):
+      C{openssl pkcs12 -in YOURPRIVKEY.p12 -nodes -nocerts
+      | openssl rsa -out PRIV.pem}
+    - Move the .pem file to a safe location.
+    - To Authenticate, you will need to pass the Service Account's "Email
+      address" in as the user_id and the path to the .pem file as the key.
+
+Setting up Installed Application authentication:
+    - Go to the API Connsole
+    - Click on "Create another client ID..."
+    - Select "Installed application" and click on "Create client ID"
+    - To Authenticate, pass in the "Client ID" as the user_id and the "Client
+      secret" as the key
+    - The first time that you do this, the libcloud will give you a URL to
+      visit.  Copy and paste the URL into a browser.
+    - When you go to the URL it will ask you to log in (if you aren't already)
+      and ask you if you want to allow the project access to your account.
+    - Click on Accept and you will be given a code.
+    - Paste that code at the prompt given to you by the Google libcloud
+      connection.
+    - At that point, a token & refresh token will be stored in your home
+      directory and will be used for authentication.
+
+Please remember to secure your keys and access tokens.
+"""
+from __future__ import with_statement
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+import base64
+import calendar
+import errno
+import time
+import datetime
+import os
+import socket
+
+from libcloud.utils.py3 import urlencode, urlparse, PY3
+from libcloud.common.base import (ConnectionUserAndKey, JsonResponse,
+                                  PollingConnection)
+from libcloud.compute.types import (InvalidCredsError,
+                                    MalformedResponseError,
+                                    LibcloudError)
+
+try:
+    from Crypto.Hash import SHA256
+    from Crypto.PublicKey import RSA
+    from Crypto.Signature import PKCS1_v1_5
+except ImportError:
+    # The pycrypto library is unavailable
+    SHA256 = None
+    RSA = None
+    PKCS1_v1_5 = None
+
+TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+
+class GoogleAuthError(LibcloudError):
+    """Generic Error class for various authentication errors."""
+    def __init__(self, value):
+        self.value = value
+
+    def __repr__(self):
+        return repr(self.value)
+
+
+class GoogleResponse(JsonResponse):
+    pass
+
+
+class GoogleBaseDriver(object):
+    name = "Google API"
+
+
+class GoogleBaseAuthConnection(ConnectionUserAndKey):
+    """
+    Base class for Google Authentication.  Should be subclassed for specific
+    types of authentication.
+    """
+    driver = GoogleBaseDriver
+    responseCls = GoogleResponse
+    name = 'Google Auth'
+    host = 'accounts.google.com'
+    auth_path = '/o/oauth2/auth'
+
+    def __init__(self, user_id, key, scope,
+                 redirect_uri='urn:ietf:wg:oauth:2.0:oob',
+                 login_hint=None, **kwargs):
+        """
+        @param  user_id: The email address (for service accounts) or Client ID
+                         (for installed apps) to be used for authentication.
+        @type   user_id: C{str}
+
+        @param  key: The RSA Key (for service accounts) or file path containing
+                     key or Client Secret (for installed apps) to be used for
+                     authentication.
+        @type   key: C{str}
+
+        @param  scope: A list of urls defining the scope of authentication
+                       to grant.
+        @type   scope: C{list}
+
+        @keyword  redirect_uri: The Redirect URI for the authentication
+                                request.  See Google OAUTH2 documentation for
+                                more info.
+        @type     redirect_uri: C{str}
+
+        @keyword  login_hint: Login hint for authentication request.  Useful
+                              for Installed Application authentication.
+        @type     login_hint: C{str}
+        """
+
+        self.scope = " ".join(scope)
+        self.redirect_uri = redirect_uri
+        self.login_hint = login_hint
+
+        super(GoogleBaseAuthConnection, self).__init__(user_id, key, **kwargs)
+
+    def _now(self):
+        return datetime.datetime.utcnow()
+
+    def add_default_headers(self, headers):
+        headers['Content-Type'] = "application/x-www-form-urlencoded"
+        headers['Host'] = self.host
+        return headers
+
+    def _token_request(self, request_body):
+        """
+        Return an updated token from a token request body.
+
+        @param  request_body: A dictionary of values to send in the body of the
+                              token request.
+        @type   request_body: C{dict}
+
+        @return:  A dictionary with updated token information
+        @rtype:   C{dict}
+        """
+        data = urlencode(request_body)
+        now = self._now()
+        response = self.request('/o/oauth2/token', method='POST', data=data)
+        token_info = response.object
+        if 'expires_in' in token_info:
+            expire_time = now + datetime.timedelta(
+                seconds=token_info['expires_in'])
+            token_info['expire_time'] = expire_time.strftime(TIMESTAMP_FORMAT)
+        return token_info
+
+
+class GoogleInstalledAppAuthConnection(GoogleBaseAuthConnection):
+    """Authentication connection for "Installed Application" authentication."""
+    def get_code(self):
+        """
+        Give the user a URL that they can visit to authenticate and obtain a
+        code.  This method will ask for that code that the user can paste in.
+
+        @return:  Code supplied by the user after authenticating
+        @rtype:   C{str}
+        """
+        auth_params = {'response_type': 'code',
+                       'client_id': self.user_id,
+                       'redirect_uri': self.redirect_uri,
+                       'scope': self.scope,
+                       'state': 'Libcloud Request'}
+        if self.login_hint:
+            auth_params['login_hint'] = self.login_hint
+
+        data = urlencode(auth_params)
+
+        url = 'https://%s%s?%s' % (self.host, self.auth_path, data)
+        print('Please Go to the following URL and sign in:')
+        print(url)
+        if PY3:
+            code = input('Enter Code:')
+        else:
+            code = raw_input('Enter Code:')
+        return code
+
+    def get_new_token(self):
+        """
+        Get a new token. Generally used when no previous token exists or there
+        is no refresh token
+
+        @return:  Dictionary containing token information
+        @rtype:   C{dict}
+        """
+        # Ask the user for a code
+        code = self.get_code()
+
+        token_request = {'code': code,
+                         'client_id': self.user_id,
+                         'client_secret': self.key,
+                         'redirect_uri': self.redirect_uri,
+                         'grant_type': 'authorization_code'}
+
+        return self._token_request(token_request)
+
+    def refresh_token(self, token_info):
+        """
+        Use the refresh token supplied in the token info to get a new token.
+
+        @param  token_info: Dictionary containing current token information
+        @type   token_info: C{dict}
+
+        @return:  A dictionary containing updated token information.
+        @rtype:   C{dict}
+        """
+        if 'refresh_token' not in token_info:
+            return self.get_new_token()
+        refresh_request = {'refresh_token': token_info['refresh_token'],
+                           'client_id': self.user_id,
+                           'client_secret': self.key,
+                           'grant_type': 'refresh_token'}
+
+        new_token = self._token_request(refresh_request)
+        if 'refresh_token' not in new_token:
+            new_token['refresh_token'] = token_info['refresh_token']
+        return new_token
+
+
+class GoogleServiceAcctAuthConnection(GoogleBaseAuthConnection):
+    """Authentication class for "Service Account" authentication."""
+    def __init__(self, user_id, key, *args, **kwargs):
+        """
+        Check to see if PyCrypto is available, and convert key file path into a
+        key string if the key is in a file.
+
+        @param  user_id: Email address to be used for Service Account
+                authentication.
+        @type   user_id: C{str}
+
+        @param  key: The RSA Key or path to file containing the key.
+        @type   key: C{str}
+        """
+        if SHA256 is None:
+            raise GoogleAuthError('PyCrypto library required for '
+                                  'Service Accout Authentication.')
+        # Check to see if 'key' is a file and read the file if it is.
+        keypath = os.path.expanduser(key)
+        is_file_path = os.path.exists(keypath) and os.path.isfile(keypath)
+        if is_file_path:
+            with open(keypath, 'r') as f:
+                key = f.read()
+        super(GoogleServiceAcctAuthConnection, self).__init__(
+            user_id, key, *args, **kwargs)
+
+    def get_new_token(self):
+        """
+        Get a new token using the email address and RSA Key.
+
+        @return:  Dictionary containing token information
+        @rtype:   C{dict}
+        """
+        # The header is always the same
+        header = {'alg': 'RS256', 'typ': 'JWT'}
+        header_enc = base64.urlsafe_b64encode(json.dumps(header))
+
+        # Construct a claim set
+        claim_set = {'iss': self.user_id,
+                     'scope': self.scope,
+                     'aud': 'https://accounts.google.com/o/oauth2/token',
+                     'exp': int(time.time()) + 3600,
+                     'iat': int(time.time())}
+        claim_set_enc = base64.urlsafe_b64encode(json.dumps(claim_set))
+
+        # The message contains both the header and claim set
+        message = '%s.%s' % (header_enc, claim_set_enc)
+        # Then the message is signed using the key supplied
+        key = RSA.importKey(self.key)
+        hash_func = SHA256.new(message)
+        signer = PKCS1_v1_5.new(key)
+        signature = base64.urlsafe_b64encode(signer.sign(hash_func))
+
+        # Finally the message and signature are sent to get a token
+        jwt = '%s.%s' % (message, signature)
+        request = {'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+                   'assertion': jwt}
+
+        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 contining token information.
+                            (Not used, but here for compatibility)
+        @type   token_info: C{dict}
+
+        @return:  A dictionary containing updated token information.
+        @rtype:   C{dict}
+        """
+        return self.get_new_token()
+
+
+class GoogleBaseConnection(ConnectionUserAndKey, PollingConnection):
+    """Base connection class for interacting with Google APIs."""
+    driver = GoogleBaseDriver
+    responseCls = GoogleResponse
+    host = 'www.googleapis.com'
+    poll_interval = 2.0
+    timeout = 120
+
+    def __init__(self, user_id, key, auth_type=None,
+                 credential_file=None, **kwargs):
+        """
+        Determine authentication type, set up appropriate authentication
+        connection and get initial authentication information.
+
+        @param  user_id: The email address (for service accounts) or Client ID
+                         (for installed apps) to be used for authentication.
+        @type   user_id: C{str}
+
+        @param  key: The RSA Key (for service accounts) or file path containing
+                     key or Client Secret (for installed apps) to be used for
+                     authentication.
+        @type   key: C{str}
+
+        @keyword  auth_type: Accepted values are "SA" or "IA"
+                             ("Service Account" or "Installed Application").
+                             If not supplied, auth_type will be guessed based
+                             on value of user_id.
+        @type     auth_type: C{str}
+
+        @keyword  credential_file: Path to file for caching authentication
+                                   information.
+        @type     credential_file: C{str}
+        """
+        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.
+            if '@' in user_id:
+                auth_type = 'SA'
+            else:
+                auth_type = 'IA'
+        if 'scope' in kwargs:
+            self.scope = kwargs['scope']
+            kwargs.pop('scope', None)
+        self.token_info = self._get_token_info_from_file()
+        if auth_type == 'SA':
+            self.auth_conn = GoogleServiceAcctAuthConnection(
+                user_id, key, self.scope, **kwargs)
+        elif auth_type == 'IA':
+            self.auth_conn = GoogleInstalledAppAuthConnection(
+                user_id, key, self.scope, **kwargs)
+        else:
+            raise GoogleAuthError('auth_type should be \'SA\' or \'IA\'')
+
+        if self.token_info is None:
+            self.token_info = self.auth_conn.get_new_token()
+            self._write_token_info_to_file()
+
+        self.token_expire_time = datetime.datetime.strptime(
+            self.token_info['expire_time'], TIMESTAMP_FORMAT)
+
+        super(GoogleBaseConnection, self).__init__(user_id, key, **kwargs)
+
+    def _now(self):
+        return datetime.datetime.utcnow()
+
+    def add_default_headers(self, headers):
+        """
+        @inherits: L{Connection.add_default_headers}
+        """
+        headers['Content-Type'] = "application/json"
+        headers['Host'] = self.host
+        return headers
+
+    def pre_connect_hook(self, params, headers):
+        """
+        Check to make sure that token hasn't expired.  If it has, get an
+        updated token.  Also, add the token to the headers.
+
+        @inherits: L{Connection.pre_connect_hook}
+        """
+        now = self._now()
+        if self.token_expire_time < now:
+            self.token_info = self.auth_conn.refresh_token(self.token_info)
+            self.token_expire_time = datetime.datetime.strptime(
+                self.token_info['expire_time'], TIMESTAMP_FORMAT)
+            self._write_token_info_to_file()
+        headers['Authorization'] = 'Bearer %s' % (
+            self.token_info['access_token'])
+
+        return params, headers
+
+    def encode_data(self, data):
+        """Encode data to JSON"""
+        return json.dumps(data)
+
+    def request(self, *args, **kwargs):
+        """
+        @inherits: L{Connection.request}
+        """
+        # Adds some retry logic for the occasional
+        # "Connection Reset by peer" error.
+        retries = 4
+        tries = 0
+        while tries < (retries - 1):
+            try:
+                return super(GoogleBaseConnection, self).request(
+                    *args, **kwargs)
+            except socket.error:
+                e = sys.exc_info()[1]
+                if e.errno == errno.ECONNRESET:
+                    tries = tries + 1
+                else:
+                    raise e
+        # One more time, then give up.
+        return super(GoogleBaseConnecion, self).request(*args, **kwargs)
+
+    def _get_token_info_from_file(self):
+        """
+        Read credential file and return token information.
+
+        @return:  Token information dictionary, or None
+        @rtype:   C{dict} or C{None}
+        """
+        token_info = None
+        filename = os.path.realpath(os.path.expanduser(self.credential_file))
+
+        try:
+            with open(filename, 'r') as f:
+                data = f.read()
+            token_info = json.loads(data)
+        except IOError:
+            pass
+        return token_info
+
+    def _write_token_info_to_file(self):
+        """
+        Write token_info to credential file.
+        """
+        filename = os.path.realpath(os.path.expanduser(self.credential_file))
+        data = json.dumps(self.token_info)
+        with open(filename, 'w') as f:
+            f.write(data)
+
+    def has_completed(self, response):
+        """
+        Determine if operation has completed based on response.
+
+        @param  response: JSON response
+        @type   response: I{responseCls}
+
+        @return:  True if complete, False otherwise
+        @rtype:   C{bool}
+        """
+        if response.object['status'] == 'DONE':
+            return True
+        else:
+            return False
+
+    def get_poll_request_kwargs(self, response, context, request_kwargs):
+        """
+        @inherits: L{PollingConnection.get_poll_request_kwargs}
+        """
+        return {'action': response.object['selfLink']}
+
+    def morph_action_hook(self, action):
+        """
+        Update action to correct request path.
+
+        In many places, the Google API returns a full URL to a resource.
+        This will strip the scheme and host off of the path and just return
+        the request.  Otherwise, it will append the base request_path to
+        the action.
+
+        @param  action: The action to be called in the http request
+        @type   action: C{str}
+
+        @return:  The modified request based on the action
+        @rtype:   C{str}
+        """
+        if action.startswith('https://'):
+            u = urlparse.urlsplit(action)
+            request = urlparse.urlunsplit(('', '', u[2], u[3], u[4]))
+        else:
+            request = self.request_path + action
+        return request

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c1980ca7/libcloud/compute/drivers/__init__.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/__init__.py b/libcloud/compute/drivers/__init__.py
index 68f273a..9c6c078 100644
--- a/libcloud/compute/drivers/__init__.py
+++ b/libcloud/compute/drivers/__init__.py
@@ -27,6 +27,7 @@ __all__ = [
     'elasticstack',
     'elastichosts',
     'cloudsigma',
+    'gce',
     'gogrid',
     'hostvirtual',
     'ibm_sce',


Mime
View raw message