blob: f9257d47fc5af61fee24742334e945c039d989e7 [file] [log] [blame]
# 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