613 lines
22 KiB
Python
613 lines
22 KiB
Python
# 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.
|
|
"""
|
|
AuroraDNS DNS Driver
|
|
"""
|
|
|
|
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.common.types import LibcloudError
|
|
|
|
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 AuroraDNSHealthCheckType(object):
|
|
"""
|
|
Healthcheck type.
|
|
"""
|
|
HTTP = 'HTTP'
|
|
HTTPS = 'HTTPS'
|
|
TCP = 'TCP'
|
|
|
|
|
|
class HealthCheckError(LibcloudError):
|
|
error_type = 'HealthCheckError'
|
|
|
|
def __init__(self, value, driver, health_check_id):
|
|
self.health_check_id = health_check_id
|
|
super(HealthCheckError, self).__init__(value=value, driver=driver)
|
|
|
|
def __str__(self):
|
|
return self.__repr__()
|
|
|
|
def __repr__(self):
|
|
return ('<%s in %s, health_check_id=%s, value=%s>' %
|
|
(self.error_type, repr(self.driver),
|
|
self.health_check_id, self.value))
|
|
|
|
|
|
class HealthCheckDoesNotExistError(HealthCheckError):
|
|
error_type = 'HealthCheckDoesNotExistError'
|
|
|
|
|
|
class AuroraDNSHealthCheck(object):
|
|
"""
|
|
AuroraDNS Healthcheck resource.
|
|
"""
|
|
|
|
def __init__(self, id, type, hostname, ipaddress, port, interval, path,
|
|
threshold, health, enabled, zone, driver, extra=None):
|
|
"""
|
|
:param id: Healthcheck id
|
|
:type id: ``str``
|
|
|
|
:param hostname: Hostname or FQDN of the target
|
|
:type hostname: ``str``
|
|
|
|
:param ipaddress: IPv4 or IPv6 address of the target
|
|
:type ipaddress: ``str``
|
|
|
|
:param port: The port on the target to monitor
|
|
:type port: ``int``
|
|
|
|
:param interval: The interval of the health check
|
|
:type interval: ``int``
|
|
|
|
:param path: The path to monitor on the target
|
|
:type path: ``str``
|
|
|
|
:param threshold: The threshold of before marking a check as failed
|
|
:type threshold: ``int``
|
|
|
|
:param health: The current health of the health check
|
|
:type health: ``bool``
|
|
|
|
:param enabled: If the health check is currently enabled
|
|
:type enabled: ``bool``
|
|
|
|
:param zone: Zone instance.
|
|
:type zone: :class:`Zone`
|
|
|
|
:param driver: DNSDriver instance.
|
|
:type driver: :class:`DNSDriver`
|
|
|
|
:param extra: (optional) Extra attributes (driver specific).
|
|
:type extra: ``dict``
|
|
"""
|
|
self.id = str(id) if id else None
|
|
self.type = type
|
|
self.hostname = hostname
|
|
self.ipaddress = ipaddress
|
|
self.port = int(port) if port else None
|
|
self.interval = int(interval)
|
|
self.path = path
|
|
self.threshold = int(threshold)
|
|
self.health = bool(health)
|
|
self.enabled = bool(enabled)
|
|
self.zone = zone
|
|
self.driver = driver
|
|
self.extra = extra or {}
|
|
|
|
def update(self, type=None, hostname=None, ipaddress=None, port=None,
|
|
interval=None, path=None, threshold=None, enabled=None,
|
|
extra=None):
|
|
return self.driver.ex_update_healthcheck(healthcheck=self, type=type,
|
|
hostname=hostname,
|
|
ipaddress=ipaddress,
|
|
port=port, path=path,
|
|
interval=interval,
|
|
threshold=threshold,
|
|
enabled=enabled, extra=extra)
|
|
|
|
def delete(self):
|
|
return self.driver.ex_delete_healthcheck(healthcheck=self)
|
|
|
|
def __repr__(self):
|
|
return ('<AuroraDNSHealthCheck: zone=%s, id=%s, type=%s, hostname=%s, '
|
|
'ipaddress=%s, port=%d, interval=%d, health=%s, provider=%s'
|
|
'...>' %
|
|
(self.zone.id, self.id, self.type, self.hostname,
|
|
self.ipaddress, self.port, self.interval, self.health,
|
|
self.driver.name))
|
|
|
|
|
|
class AuroraDNSResponse(JsonResponse):
|
|
def success(self):
|
|
return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
|
|
|
|
def parse_error(self):
|
|
status = int(self.status)
|
|
error = {'driver': self, 'value': ''}
|
|
|
|
if status == httplib.UNAUTHORIZED:
|
|
error['value'] = 'Authentication failed'
|
|
raise InvalidCredsError(**error)
|
|
elif status == httplib.FORBIDDEN:
|
|
error['value'] = 'Authorization failed'
|
|
error['http_code'] = status
|
|
raise ProviderError(**error)
|
|
elif status == httplib.NOT_FOUND:
|
|
context = self.connection.context
|
|
if context['resource'] == 'zone':
|
|
error['zone_id'] = context['id']
|
|
raise ZoneDoesNotExistError(**error)
|
|
elif context['resource'] == 'record':
|
|
error['record_id'] = context['id']
|
|
raise RecordDoesNotExistError(**error)
|
|
elif context['resource'] == 'healthcheck':
|
|
error['health_check_id'] = context['id']
|
|
raise HealthCheckDoesNotExistError(**error)
|
|
elif status == httplib.CONFLICT:
|
|
context = self.connection.context
|
|
if context['resource'] == 'zone':
|
|
error['zone_id'] = context['id']
|
|
raise ZoneAlreadyExistsError(**error)
|
|
elif status == httplib.BAD_REQUEST:
|
|
context = self.connection.context
|
|
body = self.parse_body()
|
|
raise ProviderError(value=body['errormsg'],
|
|
http_code=status, driver=self)
|
|
|
|
|
|
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.SRV: 'SRV',
|
|
RecordType.TXT: 'TXT',
|
|
RecordType.DS: 'DS',
|
|
RecordType.PTR: 'PTR',
|
|
RecordType.SSHFP: 'SSHFP',
|
|
RecordType.TLSA: 'TLSA'
|
|
}
|
|
|
|
HEALTHCHECK_TYPE_MAP = {
|
|
AuroraDNSHealthCheckType.HTTP: 'HTTP',
|
|
AuroraDNSHealthCheckType.HTTPS: 'HTTPS',
|
|
AuroraDNSHealthCheckType.TCP: 'TCP'
|
|
}
|
|
|
|
def iterate_zones(self):
|
|
res = self.connection.request('/zones')
|
|
for zone in res.parse_body():
|
|
yield self.__res_to_zone(zone)
|
|
|
|
def iterate_records(self, zone):
|
|
self.connection.set_context({'resource': 'zone', 'id': zone.id})
|
|
res = self.connection.request('/zones/%s/records' % zone.id)
|
|
|
|
for record in res.parse_body():
|
|
yield self.__res_to_record(zone, record)
|
|
|
|
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': self.RECORD_TYPE_MAP[type],
|
|
'content': data
|
|
}
|
|
|
|
rdata = self.__merge_extra_data(rdata, extra)
|
|
|
|
if 'ttl' not in rdata:
|
|
rdata['ttl'] = DEFAULT_ZONE_TTL
|
|
|
|
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.set_context({'resource': 'zone', 'id': zone.id})
|
|
self.connection.request('/zones/%s' % zone.id, method='DELETE')
|
|
return True
|
|
|
|
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')
|
|
return True
|
|
|
|
def list_record_types(self):
|
|
types = []
|
|
for record_type in self.RECORD_TYPE_MAP.keys():
|
|
types.append(record_type)
|
|
|
|
return types
|
|
|
|
def update_record(self, record, name, type, data, extra=None):
|
|
rdata = {}
|
|
|
|
if name is not None:
|
|
rdata['name'] = name
|
|
|
|
if type is not None:
|
|
rdata['type'] = self.RECORD_TYPE_MAP[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 ex_list_healthchecks(self, zone):
|
|
"""
|
|
List all Health Checks in a zone.
|
|
|
|
:param zone: Zone to list health checks for.
|
|
:type zone: :class:`Zone`
|
|
|
|
:return: ``list`` of :class:`AuroraDNSHealthCheck`
|
|
"""
|
|
healthchecks = []
|
|
self.connection.set_context({'resource': 'zone', 'id': zone.id})
|
|
res = self.connection.request('/zones/%s/health_checks' % zone.id)
|
|
|
|
for healthcheck in res.parse_body():
|
|
healthchecks.append(self.__res_to_healthcheck(zone, healthcheck))
|
|
|
|
return healthchecks
|
|
|
|
def ex_get_healthcheck(self, zone, health_check_id):
|
|
"""
|
|
Get a single Health Check from a zone
|
|
|
|
:param zone: Zone in which the health check is
|
|
:type zone: :class:`Zone`
|
|
|
|
:param health_check_id: ID of the required health check
|
|
:type health_check_id: ``str``
|
|
|
|
:return: :class:`AuroraDNSHealthCheck`
|
|
"""
|
|
self.connection.set_context({'resource': 'healthcheck',
|
|
'id': health_check_id})
|
|
res = self.connection.request('/zones/%s/health_checks/%s'
|
|
% (zone.id, health_check_id))
|
|
check = res.parse_body()
|
|
|
|
return self.__res_to_healthcheck(zone, check)
|
|
|
|
def ex_create_healthcheck(self, zone, type, hostname, port, path,
|
|
interval, threshold, ipaddress=None,
|
|
enabled=True, extra=None):
|
|
"""
|
|
Create a new Health Check in a zone
|
|
|
|
:param zone: Zone in which the health check should be created
|
|
:type zone: :class:`Zone`
|
|
|
|
:param type: The type of health check to be created
|
|
:type type: :class:`AuroraDNSHealthCheckType`
|
|
|
|
:param hostname: The hostname of the target to monitor
|
|
:type hostname: ``str``
|
|
|
|
:param port: The port of the target to monitor. E.g. 80 for HTTP
|
|
:type port: ``int``
|
|
|
|
:param path: The path of the target to monitor. Only used by HTTP
|
|
at this moment. Usually this is simple /.
|
|
:type path: ``str``
|
|
|
|
:param interval: The interval of checks. 10, 30 or 60 seconds.
|
|
:type interval: ``int``
|
|
|
|
:param threshold: The threshold of failures before the healthcheck is
|
|
marked as failed.
|
|
:type threshold: ``int``
|
|
|
|
:param ipaddress: (optional) The IP Address of the target to monitor.
|
|
You can pass a empty string if this is not required.
|
|
:type ipaddress: ``str``
|
|
|
|
:param enabled: (optional) If this healthcheck is enabled to run
|
|
:type enabled: ``bool``
|
|
|
|
:param extra: (optional) Extra attributes (driver specific).
|
|
:type extra: ``dict``
|
|
|
|
:return: :class:`AuroraDNSHealthCheck`
|
|
"""
|
|
cdata = {
|
|
'type': self.HEALTHCHECK_TYPE_MAP[type],
|
|
'hostname': hostname,
|
|
'ipaddress': ipaddress,
|
|
'port': int(port),
|
|
'interval': int(interval),
|
|
'path': path,
|
|
'threshold': int(threshold),
|
|
'enabled': enabled
|
|
}
|
|
|
|
self.connection.set_context({'resource': 'zone', 'id': zone.id})
|
|
res = self.connection.request('/zones/%s/health_checks' % zone.id,
|
|
method='POST',
|
|
data=json.dumps(cdata))
|
|
|
|
healthcheck = res.parse_body()
|
|
return self.__res_to_healthcheck(zone, healthcheck)
|
|
|
|
def ex_update_healthcheck(self, healthcheck, type=None,
|
|
hostname=None, ipaddress=None, port=None,
|
|
path=None, interval=None, threshold=None,
|
|
enabled=None, extra=None):
|
|
"""
|
|
Update an existing Health Check
|
|
|
|
:param zone: The healthcheck which has to be updated
|
|
:type zone: :class:`AuroraDNSHealthCheck`
|
|
|
|
:param type: (optional) The type of health check to be created
|
|
:type type: :class:`AuroraDNSHealthCheckType`
|
|
|
|
:param hostname: (optional) The hostname of the target to monitor
|
|
:type hostname: ``str``
|
|
|
|
:param ipaddress: (optional) The IP Address of the target to monitor.
|
|
You can pass a empty string if this is not required.
|
|
:type ipaddress: ``str``
|
|
|
|
:param port: (optional) The port of the target to monitor. E.g. 80
|
|
for HTTP
|
|
:type port: ``int``
|
|
|
|
:param path: (optional) The path of the target to monitor.
|
|
Only used by HTTP at this moment. Usually just '/'.
|
|
:type path: ``str``
|
|
|
|
:param interval: (optional) The interval of checks.
|
|
10, 30 or 60 seconds.
|
|
:type interval: ``int``
|
|
|
|
:param threshold: (optional) The threshold of failures before the
|
|
healthcheck is marked as failed.
|
|
:type threshold: ``int``
|
|
|
|
:param enabled: (optional) If this healthcheck is enabled to run
|
|
:type enabled: ``bool``
|
|
|
|
:param extra: (optional) Extra attributes (driver specific).
|
|
:type extra: ``dict``
|
|
|
|
:return: :class:`AuroraDNSHealthCheck`
|
|
"""
|
|
cdata = {}
|
|
|
|
if type is not None:
|
|
cdata['type'] = self.HEALTHCHECK_TYPE_MAP[type]
|
|
|
|
if hostname is not None:
|
|
cdata['hostname'] = hostname
|
|
|
|
if ipaddress is not None:
|
|
if len(ipaddress) == 0:
|
|
cdata['ipaddress'] = None
|
|
else:
|
|
cdata['ipaddress'] = ipaddress
|
|
|
|
if port is not None:
|
|
cdata['port'] = int(port)
|
|
|
|
if path is not None:
|
|
cdata['path'] = path
|
|
|
|
if interval is not None:
|
|
cdata['interval'] = int(interval)
|
|
|
|
if threshold is not None:
|
|
cdata['threshold'] = threshold
|
|
|
|
if enabled is not None:
|
|
cdata['enabled'] = bool(enabled)
|
|
|
|
self.connection.set_context({'resource': 'healthcheck',
|
|
'id': healthcheck.id})
|
|
|
|
self.connection.request('/zones/%s/health_checks/%s'
|
|
% (healthcheck.zone.id,
|
|
healthcheck.id),
|
|
method='PUT',
|
|
data=json.dumps(cdata))
|
|
|
|
return self.ex_get_healthcheck(healthcheck.zone,
|
|
healthcheck.id)
|
|
|
|
def ex_delete_healthcheck(self, healthcheck):
|
|
"""
|
|
Remove an existing Health Check
|
|
|
|
:param zone: The healthcheck which has to be removed
|
|
:type zone: :class:`AuroraDNSHealthCheck`
|
|
"""
|
|
self.connection.set_context({'resource': 'healthcheck',
|
|
'id': healthcheck.id})
|
|
|
|
self.connection.request('/zones/%s/health_checks/%s'
|
|
% (healthcheck.zone.id,
|
|
healthcheck.id),
|
|
method='DELETE')
|
|
return True
|
|
|
|
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['priority'] = record['prio']
|
|
|
|
return Record(id=record['id'], name=name,
|
|
type=record['type'],
|
|
data=record['content'], zone=zone,
|
|
driver=self.connection.driver, ttl=record['ttl'],
|
|
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.connection.driver,
|
|
extra={'created': zone['created'],
|
|
'servers': zone['servers'],
|
|
'account_id': zone['account_id'],
|
|
'cluster_id': zone['cluster_id']})
|
|
|
|
def __res_to_healthcheck(self, zone, healthcheck):
|
|
return AuroraDNSHealthCheck(id=healthcheck['id'],
|
|
type=healthcheck['type'],
|
|
hostname=healthcheck['hostname'],
|
|
ipaddress=healthcheck['ipaddress'],
|
|
health=healthcheck['health'],
|
|
threshold=healthcheck['threshold'],
|
|
path=healthcheck['path'],
|
|
interval=healthcheck['interval'],
|
|
port=healthcheck['port'],
|
|
enabled=healthcheck['enabled'],
|
|
zone=zone, driver=self.connection.driver)
|
|
|
|
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
|