diff --git a/gcloud/compute/__init__.py b/gcloud/compute/__init__.py new file mode 100644 index 000000000000..c7f0ac6035f1 --- /dev/null +++ b/gcloud/compute/__init__.py @@ -0,0 +1,14 @@ +__version__ = '0.1' + +# TODO: Allow specific scopes and authorization levels. +SCOPE = ('https://www.googleapis.com/auth/compute') +"""The scope required for authenticating as a Compute Engine consumer.""" + + +def get_connection(project_name, client_email, private_key_path): + from gcloud.credentials import Credentials + from gcloud.compute.connection import Connection + + credentials = Credentials.get_for_service_account( + client_email, private_key_path, scope=SCOPE) + return Connection(project_name=project_name, credentials=credentials) diff --git a/gcloud/compute/connection.py b/gcloud/compute/connection.py new file mode 100644 index 000000000000..3c72555a7d27 --- /dev/null +++ b/gcloud/compute/connection.py @@ -0,0 +1,115 @@ +import json +import urllib + +from gcloud import connection +from gcloud.compute import exceptions +from gcloud.compute.instance import Instance + + +class Connection(connection.Connection): + """A connection to the Google Compute Engine via the Protobuf API. + + This class should understand only the basic types (and protobufs) + in method arguments, however should be capable of returning advanced types. + + :type credentials: :class:`gcloud.credentials.Credentials` + :param credentials: The OAuth2 Credentials to use for this connection. + """ + + API_BASE_URL = 'https://www.googleapis.com' + """The base of the API call URL.""" + + API_VERSION = 'v1' + """The version of the API, used in building the API call's URL.""" + + API_URL_TEMPLATE = ('{api_base_url}/compute/{api_version}/{path}') + """A template used to craft the URL pointing toward a particular API call.""" + + _EMPTY = object() + """A pointer to represent an empty value for default arguments.""" + + def __init__(self, project_name=None, *args, **kwargs): + + super(Connection, self).__init__(*args, **kwargs) + + self.project_name = project_name + + def build_api_url(self, path, query_params=None, api_base_url=None, + api_version=None): + + url = self.API_URL_TEMPLATE.format( + api_base_url=(api_base_url or self.API_BASE_URL), + api_version=(api_version or self.API_VERSION), + path=path) + + query_params = query_params or {} + query_params.update({'project': self.project_name}) + url += '?' + urllib.urlencode(query_params) + + return url + + def make_request(self, method, url, data=None, content_type=None, + headers=None): + + headers = headers or {} + headers['Accept-Encoding'] = 'gzip' + + if data: + content_length = len(str(data)) + else: + content_length = 0 + + headers['Content-Length'] = content_length + + if content_type: + headers['Content-Type'] = content_type + + return self.http.request(uri=url, method=method, headers=headers, + body=data) + + def api_request(self, method, path=None, query_params=None, + data=None, content_type=None, + api_base_url=None, api_version=None, + expect_json=True): + + url = self.build_api_url(path=path, query_params=query_params, + api_base_url=api_base_url, + api_version=api_version) + + # Making the executive decision that any dictionary + # data will be sent properly as JSON. + if data and isinstance(data, dict): + data = json.dumps(data) + content_type = 'application/json' + + response, content = self.make_request( + method=method, url=url, data=data, content_type=content_type) + + # TODO: Add better error handling. + if response.status == 404: + raise exceptions.NotFoundError(response, content) + elif not 200 <= response.status < 300: + raise exceptions.ConnectionError(response, content) + + if content and expect_json: + # TODO: Better checking on this header for JSON. + content_type = response.get('content-type', '') + if not content_type.startswith('application/json'): + raise TypeError('Expected JSON, got %s' % content_type) + return json.loads(content) + + return content + + def get_instance(self, instance_name, zone): + instance = self.new_instance(instance_name, zone) + response = self.api_request(method='GET', path=instance.path) + return Instance.from_dict(response, connection=self) + + def reset_instance(self, instance): + self.api_request(method='POST', path=instance.path + 'reset') + return True + + # TODO: Add instance and error handling. + def new_instance(self, instance, zone): + if isinstance(instance, basestring): + return Instance(connection=self, name=instance, zone=zone) diff --git a/gcloud/compute/demo/__init__.py b/gcloud/compute/demo/__init__.py new file mode 100644 index 000000000000..61ec46e3eb6b --- /dev/null +++ b/gcloud/compute/demo/__init__.py @@ -0,0 +1,15 @@ +import os +from gcloud import compute + + +__all__ = ['get_connection', 'CLIENT_EMAIL', 'PRIVATE_KEY_PATH', + 'PROJECT_NAME'] + + +CLIENT_EMAIL = '524635209885-rda26ks46309o10e0nc8rb7d33rn0hlm@developer.gserviceaccount.com' +PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'demo.key') +PROJECT_NAME = 'gceremote' + + +def get_connection(): + return compute.get_connection(PROJECT_NAME, CLIENT_EMAIL, PRIVATE_KEY_PATH) diff --git a/gcloud/compute/demo/demo.key b/gcloud/compute/demo/demo.key new file mode 100644 index 000000000000..c933626a99b8 Binary files /dev/null and b/gcloud/compute/demo/demo.key differ diff --git a/gcloud/compute/demo/demo.py b/gcloud/compute/demo/demo.py new file mode 100644 index 000000000000..1d9e401b6b4d --- /dev/null +++ b/gcloud/compute/demo/demo.py @@ -0,0 +1,17 @@ +# Welcome to the gCloud Compute Demo! (hit enter) + +# We're going to walk through some of the basics..., +# Don't worry though. You don't need to do anything, just keep hitting enter... + +# Let's start by importing the demo module and getting a connection: +from gcloud.compute import demo +connection = demo.get_connection() + +# OK, now let's retrieve an instance +instance = connection.get_instance('gcloud-computeengine-instance', + 'us-central1-b') + +# Let us give that instance a reset - Got the reset! +instance.reset() + +# Thats it for now more is coming soon diff --git a/gcloud/compute/exceptions.py b/gcloud/compute/exceptions.py new file mode 100644 index 000000000000..298996a40df1 --- /dev/null +++ b/gcloud/compute/exceptions.py @@ -0,0 +1,18 @@ +# TODO: Make these super useful. + + +class ComputeError(Exception): + pass + + +class ConnectionError(ComputeError): + + def __init__(self, response, content): + message = str(response) + content + super(ConnectionError, self).__init__(message) + + +class NotFoundError(ConnectionError): + + def __init__(self, response, content): + self.message = 'GET %s returned a 404.' % (response.url) diff --git a/gcloud/compute/instance.py b/gcloud/compute/instance.py new file mode 100644 index 000000000000..19faaf2d3596 --- /dev/null +++ b/gcloud/compute/instance.py @@ -0,0 +1,33 @@ +class Instance(object): + + def __init__(self, connection=None, name=None, zone=None): + self.connection = connection + self.name = name + self.zone = zone + + @classmethod + def from_dict(cls, instance_dict, connection=None): + """Construct a new bucket from a dictionary of data from Cloud Storage. + + :type bucket_dict: dict + :param bucket_dict: The dictionary of data to construct a bucket from. + + :rtype: :class:`Bucket` + :returns: A bucket constructed from the data provided. + """ + + return cls(connection=connection, name=instance_dict['name'], + zone=instance_dict['zone'].split('/').pop()) + + @property + def path(self): + """The URL path to this instances.""" + + if not self.name: + raise ValueError('Cannot determine path without instance zone and name.') + + return ('projects/%s/zones/%s/instances/%s/' % + (self.connection.project_name, self.zone, self.name)) + + def reset(self): + return self.connection.reset_instance(self)