# 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']
[docs]class AuroraDNSResponse(JsonResponse):
[docs] def success(self):
return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
[docs] 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'])
[docs]class AuroraDNSConnection(ConnectionUserAndKey):
host = API_HOST
responseCls = AuroraDNSResponse
[docs] 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')
[docs] 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)
[docs]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',
}
[docs] 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
[docs] 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
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] def delete_zone(self, zone):
self.connection.request('/zones/%s' % zone.id, method='DELETE')
[docs] 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')
[docs] 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