| # 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 |