Repository: libcloud Updated Branches: refs/heads/trunk e83dcb091 -> c81bb3abb
LIBCLOUD-735: Implementation of AuroraDNS driver AuroraDNS is the new DNS product of PCextreme under the cloud name 'Aurora'. Closes #563 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/810d3ad9 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/810d3ad9 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/810d3ad9 Branch: refs/heads/trunk Commit: 810d3ad9cf9ec599602cb505d8adb240ccedc1ea Parents: e83dcb0 Author: Wido den Hollander <[email protected]> Authored: Thu Jul 2 21:57:01 2015 +0200 Committer: Tomaz Muraus <[email protected]> Committed: Wed Aug 26 21:31:34 2015 +0200 ---------------------------------------------------------------------- CHANGES.rst | 7 + docs/dns/drivers/auroradns.rst | 63 +++++ .../dns/auroradns/enable_disable_record.py | 13 + .../dns/auroradns/instantiate_driver.py | 6 + libcloud/dns/drivers/auroradns.py | 256 +++++++++++++++++++ libcloud/dns/providers.py | 4 +- libcloud/dns/types.py | 1 + 7 files changed, 349 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/CHANGES.rst ---------------------------------------------------------------------- diff --git a/CHANGES.rst b/CHANGES.rst index 1510cfe..a846ec7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,13 @@ Compute (GITHUB-561) [ZuluPro] +DNS +~~~ + +- Add new driver for AuroraDNS service. + (GITHUB-562, LIBCLOUD-735) + [Wido den Hollander] + Storage ~~~~~~~ http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/docs/dns/drivers/auroradns.rst ---------------------------------------------------------------------- diff --git a/docs/dns/drivers/auroradns.rst b/docs/dns/drivers/auroradns.rst new file mode 100644 index 0000000..56b1da8 --- /dev/null +++ b/docs/dns/drivers/auroradns.rst @@ -0,0 +1,63 @@ +AuroraDNS DNS driver documentation +================================== + +`PCextreme B.V.`_ is a Dutch cloud provider. It provides a public cloud offering +under the name AuroraCompute. All cloud services are under the family name +Aurora. + +`AuroraDNS`_ is a highly available DNS service which also provides health +checking. + +Records can be attached to a health check. When this health check becomes +unhealthy this record will no longer be served. + +This provides the possibility to create loadbalancing over multiple servers +without the requirement of a central loadbalancer in the network. It is also +provider agnostic, health checks can point to any IP/Host on the internet. + +IPv6 +---- +AuroraDNS fully supports IPv6: + +* DNS queries over IPv6 +* AAAA records +* Health checks over IPv6 + +Instantiating a driver +---------------------- + +When you instantiate a driver, you need to pass a your ``key`` and ``secret`` +to the driver constructor. These can be obtained in the control panel of +AuroraDNS. + +For example: + +.. literalinclude:: /examples/dns/auroradns/instantiate_driver.py + :language: python + +Disabling and enabling records +------------------------------ + +Records in can be disabled and enabled. By default all new records are enabled, +but this property can be set during creation and can be updated. + +For example: + +.. literalinclude:: /examples/dns/auroradns/instantiate_driver.py + :language: python + +In this example we create a record, but disable it. This means it will not be +served. + +Afterwards we enable the record and this make the DNS server serve this specific +record. + +API Docs +-------- + +.. autoclass:: libcloud.dns.drivers.auroradns.AuroraDNSDriver + :members: + :inherited-members: + +.. _`PCextreme B.V.`: https://www.pcextreme.nl/ +.. _`AuroraDNS`: https://www.pcextreme.nl/en/aurora/dns http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/docs/examples/dns/auroradns/enable_disable_record.py ---------------------------------------------------------------------- diff --git a/docs/examples/dns/auroradns/enable_disable_record.py b/docs/examples/dns/auroradns/enable_disable_record.py new file mode 100644 index 0000000..ef1d76b --- /dev/null +++ b/docs/examples/dns/auroradns/enable_disable_record.py @@ -0,0 +1,13 @@ +from libcloud.dns.types import Provider +from libcloud.dns.providers import get_driver +from libcloud.dns.types import RecordType + +cls = get_driver(Provider.AURORADNS) + +driver = cls('myapikey', 'mysecret') + +zone = driver.get_zone('auroradns.eu') + +record = zone.create_record(name='www', type=RecordType.AAAA, + data='2a00:f10:452::1', ex_disabled=True) +record.update(ex_disabled=False) http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/docs/examples/dns/auroradns/instantiate_driver.py ---------------------------------------------------------------------- diff --git a/docs/examples/dns/auroradns/instantiate_driver.py b/docs/examples/dns/auroradns/instantiate_driver.py new file mode 100644 index 0000000..1690617 --- /dev/null +++ b/docs/examples/dns/auroradns/instantiate_driver.py @@ -0,0 +1,6 @@ +from libcloud.dns.types import Provider +from libcloud.dns.providers import get_driver + +cls = get_driver(Provider.AURORADNS) + +driver = cls('myapikey', 'mysecret') http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/libcloud/dns/drivers/auroradns.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/drivers/auroradns.py b/libcloud/dns/drivers/auroradns.py new file mode 100644 index 0000000..614de15 --- /dev/null +++ b/libcloud/dns/drivers/auroradns.py @@ -0,0 +1,256 @@ +# 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. + +import base64 +import json +import hmac +import datetime + +from hashlib import sha256 + +from libcloud.utils.py3 import httplib +from libcloud.utils.py3 import b + +from libcloud.common.base import ConnectionUserAndKey, JsonResponse + +from libcloud.common.types import InvalidCredsError, ProviderError + +from libcloud.dns.base import DNSDriver, Zone, Record +from libcloud.dns.types import RecordType, ZoneDoesNotExistError +from libcloud.dns.types import ZoneAlreadyExistsError, RecordDoesNotExistError + + +API_HOST = 'api.auroradns.eu' + +# Default TTL required by libcloud, but doesn't do anything in AuroraDNS +DEFAULT_ZONE_TTL = 3600 +DEFAULT_ZONE_TYPE = 'master' + +VALID_RECORD_PARAMS_EXTRA = ['ttl' 'prio', 'health_check_id', 'disabled'] + + +class AuroraDNSResponse(JsonResponse): + def success(self): + return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED] + + def parse_error(self): + status = int(self.status) + + if status == httplib.UNAUTHORIZED: + raise InvalidCredsError(value='Authentication failed', driver=self) + elif status == httplib.FORBIDDEN: + raise ProviderError(value='Authorization failed', http_code=status, + driver=self) + elif status == httplib.NOT_FOUND: + context = self.connection.context + if context['resource'] == 'zone': + raise ZoneDoesNotExistError(value='', driver=self, + zone_id=context['id']) + elif context['resource'] == 'record': + raise RecordDoesNotExistError(value='', driver=self, + record_id=context['id']) + elif status == httplib.CONFLICT: + context = self.connection.context + if context['resource'] == 'zone': + raise ZoneAlreadyExistsError(value='', driver=self, + zone_id=context['id']) + + +class AuroraDNSConnection(ConnectionUserAndKey): + host = API_HOST + responseCls = AuroraDNSResponse + + def calculate_auth_signature(self, secret_key, method, url, timestamp): + b64_hmac = base64.b64encode( + hmac.new(b(secret_key), + b(method) + b(url) + b(timestamp), + digestmod=sha256).digest() + ) + + return b64_hmac.decode('utf-8') + + def gen_auth_header(self, api_key, secret_key, method, url, timestamp): + signature = self.calculate_auth_signature(secret_key, method, url, + timestamp) + + auth_b64 = base64.b64encode(b('%s:%s' % (api_key, signature))) + return 'AuroraDNSv1 %s' % (auth_b64.decode('utf-8')) + + def request(self, action, params=None, data='', headers=None, + method='GET'): + if not headers: + headers = {} + if not params: + params = {} + + if method in ("POST", "PUT"): + headers = {'Content-Type': 'application/json; charset=UTF-8'} + + t = datetime.datetime.utcnow() + timestamp = t.strftime('%Y%m%dT%H%M%SZ') + + headers['X-AuroraDNS-Date'] = timestamp + headers['Authorization'] = self.gen_auth_header(self.user_id, self.key, + method, action, + timestamp) + + return super(AuroraDNSConnection, self).request(action=action, + params=params, + data=data, + method=method, + headers=headers) + + +class AuroraDNSDriver(DNSDriver): + name = 'AuroraDNS' + website = 'https://www.pcextreme.nl/en/aurora/dns' + connectionCls = AuroraDNSConnection + + RECORD_TYPE_MAP = { + RecordType.A: 'A', + RecordType.AAAA: 'AAAA', + RecordType.CNAME: 'CNAME', + RecordType.MX: 'MX', + RecordType.NS: 'NS', + RecordType.SOA: 'SOA', + RecordType.SPF: 'SPF', + RecordType.SRV: 'SRV', + RecordType.TXT: 'TXT', + } + + def list_zones(self): + zones = [] + + res = self.connection.request('/zones') + for zone in res.parse_body(): + zones.append(self.__res_to_zone(zone)) + + return zones + + def list_records(self, zone): + records = [] + res = self.connection.request('/zones/%s/records' % zone.id) + + for record in res.parse_body(): + records.append(self.__res_to_record(zone, record)) + + return records + + def get_zone(self, zone_id): + self.connection.set_context({'resource': 'zone', 'id': zone_id}) + res = self.connection.request('/zones/%s' % zone_id) + zone = res.parse_body() + return self.__res_to_zone(zone) + + def get_record(self, zone_id, record_id): + self.connection.set_context({'resource': 'record', 'id': record_id}) + res = self.connection.request('/zones/%s/records/%s' % (zone_id, + record_id)) + record = res.parse_body() + + zone = self.get_zone(zone_id) + + return self.__res_to_record(zone, record) + + def create_zone(self, domain, type='master', ttl=None, extra=None): + self.connection.set_context({'resource': 'zone', 'id': domain}) + res = self.connection.request('/zones', method='POST', + data=json.dumps({'name': domain})) + zone = res.parse_body() + return self.__res_to_zone(zone) + + def create_record(self, name, zone, type, data, extra=None): + if name is None: + name = "" + + rdata = { + 'name': name, + 'type': type, + 'content': data + } + + rdata = self.__merge_extra_data(rdata, extra) + + self.connection.set_context({'resource': 'zone', 'id': zone.id}) + res = self.connection.request('/zones/%s/records' % zone.id, + method='POST', + data=json.dumps(rdata)) + + record = res.parse_body() + return self.__res_to_record(zone, record) + + def delete_zone(self, zone): + self.connection.request('/zones/%s' % zone.id, method='DELETE') + + def delete_record(self, record): + self.connection.set_context({'resource': 'record', 'id': record.id}) + self.connection.request('/zones/%s/records/%s' % (record.zone.id, + record.id), + method='DELETE') + + def update_record(self, record, name, type, data, extra): + rdata = {} + + if name is not None: + rdata['name'] = name + + if type is not None: + rdata['type'] = type + + if data is not None: + rdata['content'] = data + + rdata = self.__merge_extra_data(rdata, extra) + + self.connection.set_context({'resource': 'record', 'id': record.id}) + self.connection.request('/zones/%s/records/%s' % (record.zone.id, + record.id), + method='PUT', + data=json.dumps(rdata)) + + return self.get_record(record.zone.id, record.id) + + def __res_to_record(self, zone, record): + if len(record['name']) == 0: + name = None + else: + name = record['name'] + + extra = {} + extra['created'] = record['created'] + extra['modified'] = record['modified'] + extra['disabled'] = record['disabled'] + extra['ttl'] = record['ttl'] + extra['prio'] = record['prio'] + + return Record(id=record['id'], name=name, type=record['type'], + data=record['content'], zone=zone, driver=self, + extra=extra) + + def __res_to_zone(self, zone): + return Zone(id=zone['id'], domain=zone['name'], type=DEFAULT_ZONE_TYPE, + ttl=DEFAULT_ZONE_TTL, driver=self, + extra={'created': zone['created'], + 'servers': zone['servers'], + 'account_id': zone['account_id'], + 'cluster_id': zone['cluster_id']}) + + def __merge_extra_data(self, rdata, extra): + if extra is not None: + for param in VALID_RECORD_PARAMS_EXTRA: + if param in extra: + rdata[param] = extra[param] + + return rdata http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/libcloud/dns/providers.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index a6c92a8..aef0aa7 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -41,7 +41,9 @@ DRIVERS = { Provider.RACKSPACE_US: ('libcloud.dns.drivers.rackspace', 'RackspaceUSDNSDriver'), Provider.RACKSPACE_UK: - ('libcloud.dns.drivers.rackspace', 'RackspaceUKDNSDriver') + ('libcloud.dns.drivers.rackspace', 'RackspaceUKDNSDriver'), + Provider.AURORADNS: + ('libcloud.dns.drivers.auroradns', 'AuroraDNSDriver') } http://git-wip-us.apache.org/repos/asf/libcloud/blob/810d3ad9/libcloud/dns/types.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index cd1ae85..ad2b34a 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -38,6 +38,7 @@ class Provider(object): GOOGLE = 'google' SOFTLAYER = 'softlayer' DIGITAL_OCEAN = 'digitalocean' + AURORADNS = 'auroradns' # Deprecated RACKSPACE_US = 'rackspace_us'
