You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2013/07/13 01:45:25 UTC
[10/10] git commit: Add new driver for Google Compute Engine.
Add new driver for Google Compute Engine.
Signed-off-by: Tomaz Muraus <to...@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 <ri...@google.com>
Authored: Fri Jul 12 12:57:20 2013 -0700
Committer: Tomaz Muraus <to...@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',