From 1bd304d7b98a16f88d2d702e71ae285d0f4217c5 Mon Sep 17 00:00:00 2001 From: Kevin Leyow Date: Mon, 31 Mar 2014 21:31:27 -0500 Subject: [PATCH] Fix #77 - Initialized gcloud dns. Cleaned up dns. Addressed comments on dns. Added function to create changes. Changed project_id to project and addressed comments. --- gcloud/connection.py | 84 +++++++++++++++++++++++++++++++++++++ gcloud/dns/__init__.py | 21 ++++++++++ gcloud/dns/connection.py | 63 ++++++++++++++++++++++++++++ gcloud/dns/demo/__init__.py | 19 +++++++++ gcloud/dns/demo/demo.py | 8 ++++ gcloud/dns/exceptions.py | 6 +++ gcloud/dns/zone.py | 38 +++++++++++++++++ gcloud/exceptions.py | 18 ++++++++ 8 files changed, 257 insertions(+) create mode 100644 gcloud/dns/__init__.py create mode 100644 gcloud/dns/connection.py create mode 100644 gcloud/dns/demo/__init__.py create mode 100644 gcloud/dns/demo/demo.py create mode 100644 gcloud/dns/exceptions.py create mode 100644 gcloud/dns/zone.py create mode 100644 gcloud/exceptions.py diff --git a/gcloud/connection.py b/gcloud/connection.py index 35855e89b4452..d13967726a418 100644 --- a/gcloud/connection.py +++ b/gcloud/connection.py @@ -1,4 +1,8 @@ import httplib2 +import json +import urllib + +import exceptions class Connection(object): @@ -42,3 +46,83 @@ def http(self): self._http = self._credentials.authorize(self._http) return self._http + +class JsonConnection(Connection): + + API_BASE_URL = 'https://www.googleapis.com' + """The base of the API call URL.""" + + _EMPTY = object() + """A pointer to represent an empty value for default arguments.""" + + def __init__(self, project=None, *args, **kwargs): + + super(JsonConnection, self).__init__(*args, **kwargs) + + self.project = project + + 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}) + 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 diff --git a/gcloud/dns/__init__.py b/gcloud/dns/__init__.py new file mode 100644 index 0000000000000..9fda99a801c17 --- /dev/null +++ b/gcloud/dns/__init__.py @@ -0,0 +1,21 @@ +__version__ = '0.1' + +# TODO: Allow specific scopes and authorization levels. +SCOPE = ('https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/ndev.clouddns.readonly', + 'https://www.googleapis.com/auth/ndev.clouddns.readwrite') +"""The scope required for authenticating as a Cloud DNS consumer.""" + + +def get_connection(project, client_email, private_key_path): + from gcloud.credentials import Credentials + from gcloud.dns.connection import Connection + + credentials = Credentials.get_for_service_account( + client_email, private_key_path, scope=SCOPE) + return Connection(project=project, credentials=credentials) + + +def get_zone(zone_name, project, client_email, private_key_path): + connection = get_connection(project, client_email, private_key_path) + return connection.get_zone(zone_name) diff --git a/gcloud/dns/connection.py b/gcloud/dns/connection.py new file mode 100644 index 0000000000000..9a81cc86745f5 --- /dev/null +++ b/gcloud/dns/connection.py @@ -0,0 +1,63 @@ +import json + +from gcloud import connection +from gcloud.dns.zone import Zone + + +class Connection(connection.JsonConnection): + API_VERSION = 'v1beta1' + """The version of the API, used in building the API call's URL.""" + + API_URL_TEMPLATE = ('{api_base_url}/dns/{api_version}/projects/{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=None, *args, **kwargs): + + super(Connection, self).__init__(*args, **kwargs) + + self.project = project + + def create_zone(self, data): + zone = self.new_zone(data['name']) + response = self.api_request(method='POST', path=zone.path, + data=data) + return Zone.from_dict(response, connection=self) + + def delete_zone(self, zone): + zone = self.new_zone(zone) + self.api_request(method='DELETE', path=zone.path + + zone.name) + return True + + def get_zone(self, zone): + zone = self.new_zone(zone) + response = self.api_request(method='GET', path=zone.path) + return Zone.from_dict(response['managedZones'][0], + connection=self) + + def list_zones(self): + zone = self.new_zone('test') + response = self.api_request(method='GET', path=zone.path) + print json.dumps(response, indent=2) + + def new_zone(self, zone): + if isinstance(zone, Zone): + return zone + + # Support Python 2 and 3. + try: + string_type = basestring + except NameError: + string_type = str + + if isinstance(zone, string_type): + return Zone(connection=self, name=zone) + + def create_changes(self, zone, data): + zone = self.new_zone(zone) + self.api_request(method='POST', path=zone.path + zone.name + '/changes', + data=data) + return True diff --git a/gcloud/dns/demo/__init__.py b/gcloud/dns/demo/__init__.py new file mode 100644 index 0000000000000..dfc75a1349886 --- /dev/null +++ b/gcloud/dns/demo/__init__.py @@ -0,0 +1,19 @@ +import os +from gcloud import dns + + +__all__ = ['get_connection', 'get_zone' 'CLIENT_EMAIL', 'PRIVATE_KEY_PATH', + 'PROJECT'] + + +CLIENT_EMAIL = '' +PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'demo.key') +PROJECT = '' + + +def get_connection(): + return dns.get_connection(PROJECT, CLIENT_EMAIL, PRIVATE_KEY_PATH) + + +def get_zone(zone_name): + return dns.get_zone(zone_name, PROJECT, CLIENT_EMAIL, PRIVATE_KEY_PATH) diff --git a/gcloud/dns/demo/demo.py b/gcloud/dns/demo/demo.py new file mode 100644 index 0000000000000..99d5d2c872a14 --- /dev/null +++ b/gcloud/dns/demo/demo.py @@ -0,0 +1,8 @@ +# Welcome to the gCloud DNS 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.dns import demo +connection = demo.get_connection() diff --git a/gcloud/dns/exceptions.py b/gcloud/dns/exceptions.py new file mode 100644 index 0000000000000..2f92ff94c8f81 --- /dev/null +++ b/gcloud/dns/exceptions.py @@ -0,0 +1,6 @@ +from gcloud.exceptions import gcloudError +# TODO: Make these super useful. + + +class DNSError(gcloudError): + pass diff --git a/gcloud/dns/zone.py b/gcloud/dns/zone.py new file mode 100644 index 0000000000000..ccf619a7772cc --- /dev/null +++ b/gcloud/dns/zone.py @@ -0,0 +1,38 @@ +class Zone(object): + + def __init__(self, connection=None, creation_time=None, description=None, + dns_name=None, id=None, kind=None, name=None, + name_servers=None): + self.connection = connection + self.creation_time = creation_time + self.description = description + self.dns_name = dns_name + self.id = id + self.kind = kind + self.name = name + self.name_servers = name_servers + + @classmethod + def from_dict(cls, zone_dict, connection=None): + + return cls(connection=connection, + creation_time=zone_dict['creationTime'], + description=zone_dict['description'], + dns_name=zone_dict['dnsName'], id=zone_dict['id'], + kind=zone_dict['kind'], name=zone_dict['name'], + name_servers=zone_dict['nameServers']) + + @property + def path(self): + """The URL path to this zone.""" + + if not self.connection.project: + raise ValueError('Cannot determine path without project name.') + + return self.connection.project + '/managedZones/' + + def delete(self): + return self.connection.delete_zone(self) + + def get(self): + return self.connection.get_zone(self) diff --git a/gcloud/exceptions.py b/gcloud/exceptions.py new file mode 100644 index 0000000000000..38bcbdd7f4e9b --- /dev/null +++ b/gcloud/exceptions.py @@ -0,0 +1,18 @@ +# TODO: Make these super useful. + + +class gcloudError(Exception): + pass + + +class ConnectionError(gcloudError): + + def __init__(self, response, content): + message = str(response) + content + super(ConnectionError, self).__init__(message) + + +class NotFoundError(gcloudError): + + def __init__(self, response, content): + self.message = 'GET %s returned a 404.' % (response.url)