# 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 hmac
import json
import base64
import datetime
from hashlib import sha256
from libcloud.dns.base import Zone, Record, DNSDriver
from libcloud.dns.types import (
RecordType,
ZoneDoesNotExistError,
ZoneAlreadyExistsError,
RecordDoesNotExistError,
)
from libcloud.utils.py3 import b, httplib
from libcloud.common.base import JsonResponse, ConnectionUserAndKey
from libcloud.common.types import LibcloudError, ProviderError, InvalidCredsError
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 AuroraDNSHealthCheckType:
"""
Healthcheck type.
"""
HTTP = "HTTP"
HTTPS = "HTTPS"
TCP = "TCP"
[docs]class HealthCheckError(LibcloudError):
error_type = "HealthCheckError"
def __init__(self, value, driver, health_check_id):
self.health_check_id = health_check_id
super().__init__(value=value, driver=driver)
def __str__(self):
return self.__repr__()
def __repr__(self):
return "<{} in {}, health_check_id={}, value={}>".format(
self.error_type,
repr(self.driver),
self.health_check_id,
self.value,
)
[docs]class HealthCheckDoesNotExistError(HealthCheckError):
error_type = "HealthCheckDoesNotExistError"
[docs]class AuroraDNSHealthCheck:
"""
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 {}
[docs] 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,
)
[docs] 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,
)
)
[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)
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)
[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().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.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",
}
[docs] def iterate_zones(self):
res = self.connection.request("/zones")
for zone in res.parse_body():
yield self.__res_to_zone(zone)
[docs] 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)
[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/{}/records/{}".format(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": 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)
[docs] 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
[docs] def delete_record(self, record):
self.connection.set_context({"resource": "record", "id": record.id})
self.connection.request(
"/zones/{}/records/{}".format(record.zone.id, record.id), method="DELETE"
)
return True
[docs] def list_record_types(self):
types = []
for record_type in self.RECORD_TYPE_MAP.keys():
types.append(record_type)
return types
[docs] 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/{}/records/{}".format(record.zone.id, record.id),
method="PUT",
data=json.dumps(rdata),
)
return self.get_record(record.zone.id, record.id)
[docs] 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
[docs] 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/{}/health_checks/{}".format(zone.id, health_check_id))
check = res.parse_body()
return self.__res_to_healthcheck(zone, check)
[docs] 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)
[docs] 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/{}/health_checks/{}".format(healthcheck.zone.id, healthcheck.id),
method="PUT",
data=json.dumps(cdata),
)
return self.ex_get_healthcheck(healthcheck.zone, healthcheck.id)
[docs] 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/{}/health_checks/{}".format(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