Repository: libcloud Updated Branches: refs/heads/trunk 78b95fd9d -> ac75a302d
DigitalOceanDNSDriver implementation. Pulled connection/response from libcloud.compute.driver.digitalocean into libcloud.common.digitalocean Implemented majority functions in libcloud.dns.driver.digitalocean 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/f50b8d42 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/f50b8d42 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/f50b8d42 Branch: refs/heads/trunk Commit: f50b8d421f4b9b930abddda7b400111070c1f2f8 Parents: 78b95fd Author: Javier Castillo II <[email protected]> Authored: Sat Apr 11 01:26:18 2015 +0000 Committer: Tomaz Muraus <[email protected]> Committed: Sun Jun 14 18:05:57 2015 +0800 ---------------------------------------------------------------------- libcloud/common/digitalocean.py | 120 ++++++++++++++ libcloud/dns/drivers/digitalocean.py | 267 ++++++++++++++++++++++++++++++ libcloud/dns/providers.py | 2 + libcloud/dns/types.py | 1 + 4 files changed, 390 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/f50b8d42/libcloud/common/digitalocean.py ---------------------------------------------------------------------- diff --git a/libcloud/common/digitalocean.py b/libcloud/common/digitalocean.py new file mode 100644 index 0000000..097b207 --- /dev/null +++ b/libcloud/common/digitalocean.py @@ -0,0 +1,120 @@ +# 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. + +""" +Common settings and connection objects for DigitalOcean Cloud +""" + +from libcloud.utils.py3 import httplib + +from libcloud.common.base import ConnectionUserAndKey, ConnectionKey +from libcloud.common.base import JsonResponse +from libcloud.common.types import InvalidCredsError + +__all__ = [ + 'DigitalOcean_v1_Response', + 'DigitalOcean_v1_Connection', + 'DigitalOcean_v2_Response' + 'DigitalOcean_v2_Connection', +] + +AUTH_URL = 'https://api.digitalocean.com' + + +class DigitalOcean_v1_Response(JsonResponse): + def parse_error(self): + if self.status == httplib.FOUND and '/api/error' in self.body: + # Hacky, but DigitalOcean error responses are awful + raise InvalidCredsError(self.body) + elif self.status == httplib.UNAUTHORIZED: + body = self.parse_body() + raise InvalidCredsError(body['message']) + else: + body = self.parse_body() + + if 'error_message' in body: + error = '%s (code: %s)' % (body['error_message'], self.status) + else: + error = body + return error + + +class DigitalOcean_v1_Connection(ConnectionUserAndKey): + """ + Connection class for the DigitalOcean (v1) driver. + """ + + host = 'api.digitalocean.com' + responseCls = DigitalOcean_v1_Response + + def add_default_params(self, params): + """ + Add parameters that are necessary for every request + + This method adds ``client_id`` and ``api_key`` to + the request. + """ + params['client_id'] = self.user_id + params['api_key'] = self.key + return params + + +class DigitalOcean_v2_Response(JsonResponse): + valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, + httplib.NO_CONTENT] + + def parse_error(self): + if self.status == httplib.UNAUTHORIZED: + body = self.parse_body() + raise InvalidCredsError(body['message']) + else: + body = self.parse_body() + if 'message' in body: + error = '%s (code: %s)' % (body['message'], self.status) + else: + error = body + return error + + def success(self): + return self.status in self.valid_response_codes + + +class DigitalOcean_v2_Connection(ConnectionKey): + """ + Connection class for the DigitalOcean (v2) driver. + """ + + host = 'api.digitalocean.com' + responseCls = DigitalOcean_v2_Response + + def add_default_headers(self, headers): + """ + Add headers that are necessary for every request + + This method adds ``token`` to the request. + """ + headers['Authorization'] = 'Bearer %s' % (self.key) + headers['Content-Type'] = 'application/json' + return headers + +class DigitalOceanConnection(DigitalOcean_v2_Connection): + """ + Connection class for the DigitalOcean driver. + """ + pass + + +class DigitalOceanResponse(DigitalOcean_v2_Response): + pass http://git-wip-us.apache.org/repos/asf/libcloud/blob/f50b8d42/libcloud/dns/drivers/digitalocean.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/drivers/digitalocean.py b/libcloud/dns/drivers/digitalocean.py new file mode 100644 index 0000000..40357b9 --- /dev/null +++ b/libcloud/dns/drivers/digitalocean.py @@ -0,0 +1,267 @@ +# 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. +""" +Digital Ocean DNS Driver +""" + +__all__ = [ + 'DigitalOceanDNSDriver' +] + +from libcloud.utils.py3 import httplib + +from libcloud.common.digitalocean import DigitalOceanConnection, DigitalOceanResponse +from libcloud.dns.types import Provider, RecordType +from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError +from libcloud.dns.base import DNSDriver, Zone, Record + + +class DigitalOceanDNSDriver(DNSDriver): + connectionCls = DigitalOceanConnection + type = Provider.DIGITAL_OCEAN + name = "DigitalOcean" + website = 'https://www.digitalocean.com' + + RECORD_TYPE_MAP = { + RecordType.NS : 'NS', + RecordType.A : 'A', + RecordType.AAAA : 'AAAA', + RecordType.CNAME : 'CNAME', + RecordType.MX : 'MX', + RecordType.TXT : 'TXT', + RecordType.SRV : 'SRV', + } + + def list_zones(self): + """ + Return a list of zones. + + :return: ``list`` of :class:`Zone` + """ + data = self._paginated_request('/v2/domains', 'domains') + return list(map(self._to_zone, data)) + + def list_records(self, zone): + """ + Return a list of records for the provided zone. + + :param zone: Zone to list records for. + :type zone: :class:`Zone` + + :return: ``list`` of :class:`Record` + """ + data = self._paginated_request('/v2/domains/%s/records' % (zone.id), + 'domain_records') +# TODO: Not use list comprehension to add zone to record for proper data map +# functionality? + return list(map(self._to_record, data, [zone for z in data])) + + def get_zone(self, zone_id): + """ + Return a Zone instance. + + :param zone_id: ID of the required zone + :type zone_id: ``str`` + + :rtype: :class:`Zone` + """ + data = self.connection.request('/v2/domains/%s' % + (zone_id)).object['domain'] + + return self._to_zone(data) + + def get_record(self, zone_id, record_id): + """ + Return a Record instance. + + :param zone_id: ID of the required zone + :type zone_id: ``str`` + + :param record_id: ID of the required record + :type record_id: ``str`` + + :rtype: :class:`Record` + """ + data = self.connection.request('/v2/domains/%s/records/%s' % (zone_id, + record_id)).object['domain_record'] + +# TODO: Any way of not using get_zone which polls the API again +# without breaking the DNSDriver.get_record parameters? + return self._to_record(data, self.get_zone(zone_id)) + + def create_zone(self, domain, type='master', ttl=None, extra=None): + """ + Create a new zone. + + :param domain: Zone domain name (e.g. example.com) + :type domain: ``str`` + + :param type: Zone type (master / slave) (does nothing). + :type type: ``str`` + + :param ttl: TTL for new records. (does nothing) + :type ttl: ``int`` + + :param extra: Extra attributes (to set ip). (optional) + Note: This can be used to set the default A record with + {"ip" : "IP.AD.DR.ESS"} otherwise 127.0.0.1 is used + :type extra: ``dict`` + + :rtype: :class:`Zone` + """ + params = {'name' : domain} + try: + params['ip_address'] = extra['ip'] + except: + params['ip_address'] = '127.0.0.1' + + res = self.connection.request('/v2/domains', params=params, + method='POST') + + return Zone(id=res.object['domain']['name'], + domain=res.object['domain']['name'], + type='master', ttl=1800, driver=self, extra={}) + + def create_record(self, name, zone, type, data, extra=None): + """ + Create a new record. + + :param name: Record name without the domain name (e.g. www). + Note: If you want to create a record for a base domain + name, you should specify empty string ('') for this + argument. + :type name: ``str`` + + :param zone: Zone where the requested record is created. + :type zone: :class:`Zone` + + :param type: DNS record type (A, AAAA, ...). + :type type: :class:`RecordType` + + :param data: Data for the record (depends on the record type). + :type data: ``str`` + + :param extra: Extra attributes for MX and SRV. (Depends on record) + {"priority" : 0, "port" : 443, "weight" : 100} + :type extra: ``dict`` + + :rtype: :class:`Record` + """ + params = { + "type" : self.RECORD_TYPE_MAP[type], + "name" : name, + "data" : data + } + if extra: + try: + params['priority'] = extra['priority'] + except KeyError: + params['priority'] = 'null' + try: + params['port'] = extra['port'] + except KeyError: + params['port'] = 'null' + try: + params['weight'] = extra['weight'] + except KeyError: + params['weight'] = 'null' + + res = self.connection.request('/v2/domains/%s/records' % zone.id, + params=params, + method='POST') + + return Record(id=res.object['domain_record']['id'], + name=res.object['domain_record']['id'], + type=type, data=data, zone=zone, driver=self, extra=extra) + + def delete_zone(self, zone): + """ + Delete a zone. + + Note: This will delete all the records belonging to this zone. + + :param zone: Zone to delete. + :type zone: :class:`Zone` + + :rtype: ``bool`` + """ + params = {} + + res = self.connection.request('/v2/domains/%s' % zone.id, + params=params, method='DELETE') + + return res.status == httplib.NO_CONTENT + + def delete_record(self, record): + """ + Delete a record. + + :param record: Record to delete. + :type record: :class:`Record` + + :rtype: ``bool`` + """ + params = {} + + res = self.connection.request('/v2/domains/%s/records/%s' % ( + record.zone.id, record.id), params=params, + method='DELETE') + return res.status == httplib.NO_CONTENT + +# TODO: If there is a way to push this into libcloud.common.digitalocean +# instead of having it in libcloud.dns.digitalocean and +# libcloud.compute.digitalocean + def _paginated_request(self, url, obj): + """ + Perform multiple calls in order to have a full list of elements when + the API responses are paginated. + + :param url: API endpoint + :type url: ``str`` + + :param obj: Result object key + :type obj: ``str`` + + :return: ``list`` of API response objects + """ + params = {} + data = self.connection.request(url) + try: + pages = data.object['links']['pages']['last'].split('=')[-1] + values = data.object[obj] + for page in range(2, int(pages) + 1): + params.update({'page': page}) + new_data = self.connection.request(url, params=params) + + more_values = new_data.object[obj] + for value in more_values: + values.append(value) + data = values + except KeyError: # No pages. + data = data.object[obj] + + return data + + def _to_record(self, data, zone=None): + extra = {'port' : data['port'], 'priority' : data['priority'], + 'weight' : data['weight']} + return Record(id=data['id'], name=data['name'], + type=self._string_to_record_type(data['type']), + data=data['data'], zone=zone, driver=self, extra=extra) + + def _to_zone(self, data): + extra = {'zone_file' : data['zone_file'],} + return Zone(id=data['name'], domain=data['name'], type='master', + ttl=data['ttl'], driver=self, extra=extra) http://git-wip-us.apache.org/repos/asf/libcloud/blob/f50b8d42/libcloud/dns/providers.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index dc80460..a6c92a8 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -35,6 +35,8 @@ DRIVERS = { Provider.GOOGLE: ('libcloud.dns.drivers.google', 'GoogleDNSDriver'), Provider.SOFTLAYER: ('libcloud.dns.drivers.softlayer', 'SoftLayerDNSDriver'), + Provider.DIGITAL_OCEAN: + ('libcloud.dns.drivers.digitalocean', 'DigitalOceanDNSDriver'), # Deprecated Provider.RACKSPACE_US: ('libcloud.dns.drivers.rackspace', 'RackspaceUSDNSDriver'), http://git-wip-us.apache.org/repos/asf/libcloud/blob/f50b8d42/libcloud/dns/types.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index 32bceb5..cd1ae85 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -37,6 +37,7 @@ class Provider(object): GANDI = 'gandi' GOOGLE = 'google' SOFTLAYER = 'softlayer' + DIGITAL_OCEAN = 'digitalocean' # Deprecated RACKSPACE_US = 'rackspace_us'
