Repository: libcloud Updated Branches: refs/heads/trunk b515b8f84 -> 9c4d15c67
auroradns: Add support for Health Checks AuroraDNS supports Health Checks and based on the state of these checks records will be served or not. This way a Round-Robin DNS balancing can be achieved pointing to only healthy servers/services. Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/de7862f4 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/de7862f4 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/de7862f4 Branch: refs/heads/trunk Commit: de7862f40fe9548f38ad3f98b7c9682792155265 Parents: b515b8f Author: Wido den Hollander <[email protected]> Authored: Fri Oct 30 16:08:46 2015 +0100 Committer: anthony-shaw <[email protected]> Committed: Fri Jan 15 19:38:50 2016 +1100 ---------------------------------------------------------------------- docs/dns/drivers/auroradns.rst | 19 + .../dns/auroradns/enable_disable_record.py | 2 +- docs/examples/dns/auroradns/health_checks.py | 21 + libcloud/dns/drivers/auroradns.py | 379 ++++++++++++++++++- .../zone_example_com_health_check.json | 14 + .../zone_example_com_health_checks.json | 44 +++ libcloud/test/dns/test_auroradns.py | 47 +++ 7 files changed, 510 insertions(+), 16 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/docs/dns/drivers/auroradns.rst ---------------------------------------------------------------------- diff --git a/docs/dns/drivers/auroradns.rst b/docs/dns/drivers/auroradns.rst index 56b1da8..61831f6 100644 --- a/docs/dns/drivers/auroradns.rst +++ b/docs/dns/drivers/auroradns.rst @@ -52,6 +52,21 @@ served. Afterwards we enable the record and this make the DNS server serve this specific record. +Health Checks +------------- + +AuroraDNS has support for Health Checks which will disable all records attached +to that health check should it fail. With this you can create DNS based +loadbalancing over multiple records. + +In the example below we create a health check and afterwards attach a newly +created record to this health check. + +For example: + +.. literalinclude:: /examples/dns/auroradns/health_checks.py + :language: python + API Docs -------- @@ -59,5 +74,9 @@ API Docs :members: :inherited-members: +.. autoclass:: libcloud.dns.drivers.auroradns.AuroraDNSHealthCheck + :members: + :inherited-members: + .. _`PCextreme B.V.`: https://www.pcextreme.nl/ .. _`AuroraDNS`: https://www.pcextreme.nl/en/aurora/dns http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/docs/examples/dns/auroradns/enable_disable_record.py ---------------------------------------------------------------------- diff --git a/docs/examples/dns/auroradns/enable_disable_record.py b/docs/examples/dns/auroradns/enable_disable_record.py index ef1d76b..e31fad3 100644 --- a/docs/examples/dns/auroradns/enable_disable_record.py +++ b/docs/examples/dns/auroradns/enable_disable_record.py @@ -1,6 +1,6 @@ from libcloud.dns.types import Provider -from libcloud.dns.providers import get_driver from libcloud.dns.types import RecordType +from libcloud.dns.providers import get_driver cls = get_driver(Provider.AURORADNS) http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/docs/examples/dns/auroradns/health_checks.py ---------------------------------------------------------------------- diff --git a/docs/examples/dns/auroradns/health_checks.py b/docs/examples/dns/auroradns/health_checks.py new file mode 100644 index 0000000..f381dd2 --- /dev/null +++ b/docs/examples/dns/auroradns/health_checks.py @@ -0,0 +1,21 @@ +from libcloud.dns.types import Provider, RecordType +from libcloud.dns.providers import get_driver +from libcloud.dns.drivers.auroradns import AuroraDNSHealthCheckType + +cls = get_driver(Provider.AURORADNS) + +driver = cls('myapikey', 'mysecret') + +zone = driver.get_zone('auroradns.eu') + +health_check = driver.ex_create_healthcheck(zone=zone, + type=AuroraDNSHealthCheckType.HTTP, + hostname='web01.auroradns.eu', + path='/', + port=80, + interval=10, + threshold=5) + +record = zone.create_record(name='www', type=RecordType.AAAA, + data='2a00:f10:452::1', + extra={'health_check_id': health_check.id}) http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/libcloud/dns/drivers/auroradns.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/drivers/auroradns.py b/libcloud/dns/drivers/auroradns.py index b7ec55b..7c5d73c 100644 --- a/libcloud/dns/drivers/auroradns.py +++ b/libcloud/dns/drivers/auroradns.py @@ -12,6 +12,9 @@ # 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 @@ -26,6 +29,7 @@ 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 @@ -41,31 +45,152 @@ 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: - raise InvalidCredsError(value='Authentication failed', driver=self) + error['value'] = 'Authentication failed' + raise InvalidCredsError(**error) elif status == httplib.FORBIDDEN: - raise ProviderError(value='Authorization failed', http_code=status, - driver=self) + error['value'] = 'Authorization failed' + error['http_status'] = status + raise ProviderError(**error) elif status == httplib.NOT_FOUND: context = self.connection.context if context['resource'] == 'zone': - raise ZoneDoesNotExistError(value='', driver=self, - zone_id=context['id']) + error['zone_id'] = context['id'] + raise ZoneDoesNotExistError(**error) elif context['resource'] == 'record': - raise RecordDoesNotExistError(value='', driver=self, - record_id=context['id']) + 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': - raise ZoneAlreadyExistsError(value='', driver=self, - zone_id=context['id']) + 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): @@ -130,6 +255,12 @@ class AuroraDNSDriver(DNSDriver): RecordType.TXT: 'TXT', } + HEALTHCHECK_TYPE_MAP = { + AuroraDNSHealthCheckType.HTTP: 'HTTP', + AuroraDNSHealthCheckType.HTTPS: 'HTTPS', + AuroraDNSHealthCheckType.TCP: 'TCP' + } + def list_zones(self): zones = [] @@ -178,12 +309,15 @@ class AuroraDNSDriver(DNSDriver): rdata = { 'name': name, - 'type': type, + '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', @@ -211,7 +345,7 @@ class AuroraDNSDriver(DNSDriver): rdata['name'] = name if type is not None: - rdata['type'] = type + rdata['type'] = self.RECORD_TYPE_MAP[type] if data is not None: rdata['content'] = data @@ -226,6 +360,205 @@ class AuroraDNSDriver(DNSDriver): 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 @@ -239,18 +572,34 @@ class AuroraDNSDriver(DNSDriver): 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, - ttl=record['ttl'], extra=extra) + return Record(id=record['id'], name=name, + type=record['type'], + data=record['content'], zone=zone, + driver=self, ttl=record['ttl'], + extra=extra) def __res_to_zone(self, zone): - return Zone(id=zone['id'], domain=zone['name'], type=DEFAULT_ZONE_TYPE, + 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 __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) + def __merge_extra_data(self, rdata, extra): if extra is not None: for param in VALID_RECORD_PARAMS_EXTRA: http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_check.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_check.json b/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_check.json new file mode 100644 index 0000000..a00f0ce --- /dev/null +++ b/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_check.json @@ -0,0 +1,14 @@ +{ + "created": "2015-08-07T13:56:59Z", + "enabled": true, + "health": true, + "hostname": "www.pcextreme.nl", + "id": "9990ec60-d592-4673-9e7e-9220ed42ee0b", + "interval": 10, + "ipaddress": "109.72.87.252", + "next_run": "2015-08-10T14:22:32Z", + "path": "/", + "port": 8080, + "threshold": 3, + "type": "HTTP" +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_checks.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_checks.json b/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_checks.json new file mode 100644 index 0000000..177fdeb --- /dev/null +++ b/libcloud/test/dns/fixtures/auroradns/zone_example_com_health_checks.json @@ -0,0 +1,44 @@ +[ + { + "created": "2015-08-07T13:56:59Z", + "enabled": true, + "health": true, + "hostname": "www.pcextreme.nl", + "id": "9990ec60-d592-4673-9e7e-9220ed42ee0b", + "interval": 60, + "ipaddress": "109.72.87.252", + "next_run": "2015-08-10T14:22:32Z", + "path": "/", + "port": 80, + "threshold": 3, + "type": "HTTP" + }, + { + "created": "2015-08-07T13:56:59Z", + "enabled": true, + "health": true, + "hostname": "www.pcextreme.nl", + "id": "3f29a813-6a25-41c5-a45e-f771347de526", + "interval": 60, + "ipaddress": "2a00:f10:101::3eb:1", + "next_run": "2015-08-10T14:22:32Z", + "path": "/", + "port": 80, + "threshold": 3, + "type": "HTTP" + }, + { + "created": "2015-08-07T13:56:59Z", + "enabled": true, + "health": true, + "hostname": "www.pcextreme.nl", + "id": "7719a4c5-b319-46e7-a917-3dc57bdab1d4", + "interval": 60, + "ipaddress": null, + "next_run": "2015-08-10T14:22:32Z", + "path": "/", + "port": 80, + "threshold": 3, + "type": "HTTP" + } +] \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/de7862f4/libcloud/test/dns/test_auroradns.py ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/test_auroradns.py b/libcloud/test/dns/test_auroradns.py index 6004650..278f76d 100644 --- a/libcloud/test/dns/test_auroradns.py +++ b/libcloud/test/dns/test_auroradns.py @@ -16,6 +16,7 @@ import sys import json from libcloud.dns.drivers.auroradns import AuroraDNSDriver +from libcloud.dns.drivers.auroradns import AuroraDNSHealthCheckType from libcloud.dns.types import RecordType from libcloud.dns.types import ZoneDoesNotExistError from libcloud.dns.types import ZoneAlreadyExistsError @@ -166,6 +167,43 @@ class AuroraDNSDriverTests(LibcloudTestCase): except: raise + def test_create_health_check(self): + zone = self.driver.get_zone('example.com') + + type = AuroraDNSHealthCheckType.HTTP + hostname = "www.pcextreme.nl" + ipaddress = "109.72.87.252" + port = 8080 + interval = 10 + threshold = 3 + + check = self.driver.ex_create_healthcheck(zone=zone, + type=type, + hostname=hostname, + port=port, + path=None, + interval=interval, + threshold=threshold, + ipaddress=ipaddress) + + self.assertEqual(check.interval, interval) + self.assertEqual(check.threshold, threshold) + self.assertEqual(check.port, port) + self.assertEqual(check.type, type) + self.assertEqual(check.hostname, hostname) + self.assertEqual(check.path, "/") + self.assertEqual(check.ipaddress, ipaddress) + + def test_list_health_checks(self): + zone = self.driver.get_zone('example.com') + checks = self.driver.ex_list_healthchecks(zone) + + self.assertEqual(len(checks), 3) + + for check in checks: + self.assertEqual(check.interval, 60) + self.assertEqual(check.type, AuroraDNSHealthCheckType.HTTP) + class AuroraDNSDriverMockHttp(MockHttpTestCase): fixtures = DNSFileFixtures('auroradns') @@ -205,6 +243,15 @@ class AuroraDNSDriverMockHttp(MockHttpTestCase): body = self.fixtures.load('zone_example_com_records.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _zones_ffb62570_8414_4578_a346_526b44e320b7_health_checks(self, method, + url, body, + headers): + if method == 'POST': + body = self.fixtures.load('zone_example_com_health_check.json') + else: + body = self.fixtures.load('zone_example_com_health_checks.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _zones_1(self, method, url, body, headers): return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.NOT_FOUND])
