Add new driver for Google Compute Engine. Signed-off-by: Tomaz Muraus <[email protected]>
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/b0782175 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/b0782175 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/b0782175 Branch: refs/heads/trunk Commit: b0782175a3546bbd0dae67f383c0f6884e8a1c94 Parents: b44a0f6 Author: Rick Wright <[email protected]> Authored: Fri Jul 12 12:57:20 2013 -0700 Committer: Tomaz Muraus <[email protected]> Committed: Sat Jul 13 01:37:13 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/b0782175/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/b0782175/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/b0782175/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/b0782175/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',
