adds dns support for BuddyDNS
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/d102beef Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/d102beef Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/d102beef Branch: refs/heads/trunk Commit: d102beefbe25343102c8e551fb50db8e1f745bf4 Parents: a1f07cf Author: lostbird <lostbird@lostbird.(none)> Authored: Sun Apr 10 06:40:09 2016 +0200 Committer: anthony-shaw <anthony.p.s...@gmail.com> Committed: Wed Apr 13 08:31:18 2016 +1000 ---------------------------------------------------------------------- docs/dns/drivers/buddyns.rst | 23 +++ docs/examples/dns/buddyns/instantiate_driver.py | 5 + libcloud/common/buddyns.py | 62 ++++++++ libcloud/dns/drivers/buddyns.py | 135 ++++++++++++++++ libcloud/dns/types.py | 1 + .../fixtures/buddyns/create_zone_success.json | 8 + .../fixtures/buddyns/delete_zone_success.json | 0 .../dns/fixtures/buddyns/empty_zones_list.json | 1 + .../dns/fixtures/buddyns/get_zone_success.json | 7 + .../test/dns/fixtures/buddyns/list_zones.json | 18 +++ .../fixtures/buddyns/zone_already_exists.json | 3 + .../fixtures/buddyns/zone_does_not_exist.json | 1 + libcloud/test/dns/test_buddyns.py | 155 +++++++++++++++++++ libcloud/test/secrets.py-dist | 1 + 14 files changed, 420 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/docs/dns/drivers/buddyns.rst ---------------------------------------------------------------------- diff --git a/docs/dns/drivers/buddyns.rst b/docs/dns/drivers/buddyns.rst new file mode 100644 index 0000000..0c577db --- /dev/null +++ b/docs/dns/drivers/buddyns.rst @@ -0,0 +1,23 @@ +BuddyNS Driver Documentation +========================== + +BuddyNS.com is part of FrontDam GmbH, a small swiss company making +systems for uptime of Internet services, and run by a tribe of passionate +people who work great together. + +Read more at: https://www.buddyns.com/about/ + +Instantiating the driver +------------------------ + +.. literalinclude:: /examples/dns/buddyns/instantiate_driver.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.dns.drivers.buddyns.BuddyNSDNSDriver + :members: + :inherited-members: + +.. https://www.buddyns.com/support/api/v2/ \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/docs/examples/dns/buddyns/instantiate_driver.py ---------------------------------------------------------------------- diff --git a/docs/examples/dns/buddyns/instantiate_driver.py b/docs/examples/dns/buddyns/instantiate_driver.py new file mode 100644 index 0000000..c29eb0f --- /dev/null +++ b/docs/examples/dns/buddyns/instantiate_driver.py @@ -0,0 +1,5 @@ +from libcloud.dns.types import Provider +from libcloud.dns.providers import get_driver + +cls = get_driver(Provider.BUDDYNS) +driver = cls('<api key>') http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/common/buddyns.py ---------------------------------------------------------------------- diff --git a/libcloud/common/buddyns.py b/libcloud/common/buddyns.py new file mode 100644 index 0000000..38a589e --- /dev/null +++ b/libcloud/common/buddyns.py @@ -0,0 +1,62 @@ +from libcloud.common.base import ConnectionKey, JsonResponse + + +__all__ = [ + 'API_HOST', + 'BuddyNSException', + 'BuddyNSResponse', + 'BuddyNSConnection' +] + +# Endpoint for buddyns api +API_HOST = 'www.buddyns.com' + + +class BuddyNSResponse(JsonResponse): + errors = [] + objects = [] + + def __init__(self, response, connection): + super(BuddyNSResponse, self).__init__(response=response, + connection=connection) + self.errors, self.objects = self.parse_body_and_errors() + if not self.success(): + raise BuddyNSException(code=self.status, + message=self.errors.pop()['detail']) + + def parse_body_and_errors(self): + js = super(BuddyNSResponse, self).parse_body() + if 'detail' in js: + self.errors.append(js) + else: + self.objects.append(js) + + return self.errors, self.objects + + def success(self): + return len(self.errors) == 0 + + +class BuddyNSConnection(ConnectionKey): + host = API_HOST + responseCls = BuddyNSResponse + + def add_default_headers(self, headers): + headers['content-type'] = 'application/json' + headers['Authorization'] = 'Token' + ' ' + self.key + + return headers + + +class BuddyNSException(Exception): + + def __init__(self, code, message): + self.code = code + self.message = message + self.args = (code, message) + + def __str__(self): + return "%s %s" % (self.code, self.message) + + def __repr__(self): + return "BuddyNSException %s %s" % (self.code, self.message) http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/dns/drivers/buddyns.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/drivers/buddyns.py b/libcloud/dns/drivers/buddyns.py new file mode 100644 index 0000000..992b4d1 --- /dev/null +++ b/libcloud/dns/drivers/buddyns.py @@ -0,0 +1,135 @@ +import sys + +try: + import simplejson as json +except ImportError: + import json + +from libcloud.dns.types import Provider, ZoneDoesNotExistError,\ + ZoneAlreadyExistsError +from libcloud.dns.base import DNSDriver, Zone +from libcloud.common.buddyns import BuddyNSConnection, BuddyNSResponse,\ + BuddyNSException + +__all__ = [ + 'BuddyNSDNSDriver' +] + + +class BuddyNSDNSResponse(BuddyNSResponse): + pass + + +class BuddyNSDNSConnection(BuddyNSConnection): + responseCls = BuddyNSDNSResponse + + +class BuddyNSDNSDriver(DNSDriver): + name = 'BuddyNS DNS' + website = 'https://www.buddyns.com' + type = Provider.BUDDYNS + connectionCls = BuddyNSDNSConnection + + def list_zones(self): + action = '/api/v2/zone/' + response = self.connection.request(action=action, method='GET') + zones = self._to_zones(items=response.parse_body()) + + return zones + + def get_zone(self, zone_id): + """ + :param zone_id: Zone domain name (e.g. example.com) + :return: :class:`Zone` + """ + action = '/api/v2/zone/%s' % zone_id + try: + response = self.connection.request(action=action, method='GET') + except BuddyNSException: + e = sys.exc_info()[1] + if e.message == 'Not found': + raise ZoneDoesNotExistError(value=e.message, driver=self, + zone_id=zone_id) + else: + raise e + zone = self._to_zone(response.parse_body()) + + return zone + + def create_zone(self, domain, type='master', ttl=None, extra=None): + """ + :param domain: Zone domain name (e.g. example.com) + :type domain: ``str`` + + :param type: Zone type (This is not really used. See API docs for extra + parameters) + :type type: ``str`` + + :param ttl: TTL for new records (This is used through the extra param) + :type ttl: ``int`` + + :param extra: Extra attributes that are specific to the driver + such as ttl. + :type extra: ``dict`` + + :rtype: :class:`Zone` + Do not forget to pass the master in extra, + extra = {'master':'65.55.37.62'} for example. + """ + action = '/api/v2/zone/' + data = {'name': domain} + if extra is not None: + data.update(extra) + post_data = json.dumps(data) + try: + response = self.connection.request(action=action, method='POST', + data=post_data) + except BuddyNSException: + e = sys.exc_info()[1] + if e.message == 'Invalid zone submitted for addition.': + raise ZoneAlreadyExistsError(value=e.message, driver=self, + zone_id=domain) + else: + raise e + + zone = self._to_zone(response.parse_body()) + + return zone + + def delete_zone(self, zone): + """ + :param zone: Zone to be deleted. + :type zone: :class:`Zone` + + :return: Boolean + """ + action = '/api/v2/zone/%s' % zone.domain + try: + self.connection.request(action=action, method='DELETE') + except BuddyNSException: + e = sys.exc_info()[1] + if e.message == 'Not found': + raise ZoneDoesNotExistError(value=e.message, driver=self, + zone_id=zone.id) + else: + raise e + + return True + + def _to_zone(self, item): + common_keys = ['name', ] + extra = {} + for key in item: + if key not in common_keys: + extra[key] = item.get(key) + zone = Zone(domain=item['name'], id=item['name'], type=None, + extra=extra, ttl=None, driver=self) + + return zone + + def _to_zones(self, items): + zones = [] + for item in items: + zones.append(self._to_zone(item)) + + return zones http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/dns/types.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index 91111ed..0ef8417 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -53,6 +53,7 @@ class Provider(object): NSONE = 'nsone' LUADNS = 'luadns' NFSN = 'nfsn' + BUDDYNS = 'buddyns' # Deprecated RACKSPACE_US = 'rackspace_us' RACKSPACE_UK = 'rackspace_uk' http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/create_zone_success.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/create_zone_success.json b/libcloud/test/dns/fixtures/buddyns/create_zone_success.json new file mode 100644 index 0000000..82151a0 --- /dev/null +++ b/libcloud/test/dns/fixtures/buddyns/create_zone_success.json @@ -0,0 +1,8 @@ +{ "name": "microsoft.com", + "name_idn": "microsoft.com", + "serial": null, + "master": "65.55.37.62", + "creation_ts": null, + "status": "https://www.buddyns.com/api/v2/zone/anexample.com/status/", + "delegation": "https://www.buddyns.com/api/v2/zone/anexample.com/delegation/" +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/delete_zone_success.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/delete_zone_success.json b/libcloud/test/dns/fixtures/buddyns/delete_zone_success.json new file mode 100644 index 0000000..e69de29 http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/empty_zones_list.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/empty_zones_list.json b/libcloud/test/dns/fixtures/buddyns/empty_zones_list.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/libcloud/test/dns/fixtures/buddyns/empty_zones_list.json @@ -0,0 +1 @@ +[] http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/get_zone_success.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/get_zone_success.json b/libcloud/test/dns/fixtures/buddyns/get_zone_success.json new file mode 100644 index 0000000..9ced518 --- /dev/null +++ b/libcloud/test/dns/fixtures/buddyns/get_zone_success.json @@ -0,0 +1,7 @@ +{ "name": "myexample.com", + "name_idn": "myexample.com", + "serial": null, + "master": "65.55.37.62", + "creation_ts": "2016-04-09T06:20:05.140", + "status": "https://www.buddyns.com/api/v2/zone/myexample.com/status/", + "delegation": "https://www.buddyns.com/api/v2/zone/myexample.com/delegation/"} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/list_zones.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/list_zones.json b/libcloud/test/dns/fixtures/buddyns/list_zones.json new file mode 100644 index 0000000..7397e6a --- /dev/null +++ b/libcloud/test/dns/fixtures/buddyns/list_zones.json @@ -0,0 +1,18 @@ +[ { + "name" : "microsoft.com", + "name_idn" : "microsoft.com", + "creation_ts" : "2013-11-06T19:39:38.205", + "master" : "65.55.37.62", + "serial" : 2013110601, + "status": "/api/v2/zone/microsoft.com/status/", + "delegation": "/api/v2/zone/microsoft.com/delegation/" +}, +{ + "name" : "google.de", + "name_idn" : "google.de", + "creation_ts" : "2012-06-06T19:53:07.269", + "master" : "154.15.200.6", + "serial" : 1383743519, + "status": "google.de/status/", + "delegation": "/api/v2/zone/bücher.de/delegation/" + } ] \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/zone_already_exists.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/zone_already_exists.json b/libcloud/test/dns/fixtures/buddyns/zone_already_exists.json new file mode 100644 index 0000000..55a07e1 --- /dev/null +++ b/libcloud/test/dns/fixtures/buddyns/zone_already_exists.json @@ -0,0 +1,3 @@ + {"errors": + {"name": ["Zone with this Domain already exists."]}, + "detail": "Invalid zone submitted for addition."} http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/fixtures/buddyns/zone_does_not_exist.json ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/buddyns/zone_does_not_exist.json b/libcloud/test/dns/fixtures/buddyns/zone_does_not_exist.json new file mode 100644 index 0000000..ae53cb6 --- /dev/null +++ b/libcloud/test/dns/fixtures/buddyns/zone_does_not_exist.json @@ -0,0 +1 @@ +{"detail": "Not found"} http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/dns/test_buddyns.py ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/test_buddyns.py b/libcloud/test/dns/test_buddyns.py new file mode 100644 index 0000000..121daed --- /dev/null +++ b/libcloud/test/dns/test_buddyns.py @@ -0,0 +1,155 @@ +import sys +import unittest + +from libcloud.test import MockHttp +from libcloud.test.file_fixtures import DNSFileFixtures +from libcloud.test.secrets import DNS_PARAMS_BUDDYNS +from libcloud.dns.drivers.buddyns import BuddyNSDNSDriver +from libcloud.utils.py3 import httplib +from libcloud.dns.types import ZoneDoesNotExistError, ZoneAlreadyExistsError +from libcloud.dns.base import Zone + + +class BuddyNSDNSTests(unittest.TestCase): + def setUp(self): + BuddyNSMockHttp.type = None + BuddyNSDNSDriver.connectionCls.conn_classes = (None, BuddyNSMockHttp) + self.driver = BuddyNSDNSDriver(*DNS_PARAMS_BUDDYNS) + self.test_zone = Zone(id='test.com', type='master', ttl=None, + domain='test.com', extra={}, driver=self) + + def test_list_zones_empty(self): + BuddyNSMockHttp.type = 'EMPTY_ZONES_LIST' + zones = self.driver.list_zones() + + self.assertEqual(zones, []) + + def test_list_zones_success(self): + BuddyNSMockHttp.type = 'LIST_ZONES' + zones = self.driver.list_zones() + + self.assertEqual(len(zones), 2) + + zone = zones[0] + self.assertEqual(zone.id, 'microsoft.com') + self.assertEqual(zone.type, None) + self.assertEqual(zone.domain, 'microsoft.com') + self.assertEqual(zone.ttl, None) + + zone = zones[1] + self.assertEqual(zone.id, 'google.de') + self.assertEqual(zone.type, None) + self.assertEqual(zone.domain, 'google.de') + self.assertEqual(zone.ttl, None) + + def test_delete_zone_zone_does_not_exist(self): + BuddyNSMockHttp.type = 'DELETE_ZONE_ZONE_DOES_NOT_EXIST' + + try: + self.driver.delete_zone(zone=self.test_zone) + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, self.test_zone.id) + else: + self.fail('Exception was not thrown') + + def test_delete_zone_success(self): + BuddyNSMockHttp.type = 'DELETE_ZONE_SUCCESS' + status = self.driver.delete_zone(zone=self.test_zone) + + self.assertTrue(status) + + def test_get_zone_zone_does_not_exist(self): + BuddyNSMockHttp.type = 'GET_ZONE_ZONE_DOES_NOT_EXIST' + try: + self.driver.get_zone(zone_id='zonedoesnotexist.com') + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, 'zonedoesnotexist.com') + else: + self.fail('Exception was not thrown') + + def test_get_zone_success(self): + BuddyNSMockHttp.type = 'GET_ZONE_SUCCESS' + zone = self.driver.get_zone(zone_id='myexample.com') + + self.assertEqual(zone.id, 'myexample.com') + self.assertEqual(zone.domain, 'myexample.com') + self.assertEqual(zone.type, None) + self.assertEqual(zone.ttl, None) + self.assertEqual(zone.driver, self.driver) + + def test_create_zone_success(self): + BuddyNSMockHttp.type = 'CREATE_ZONE_SUCCESS' + zone = self.driver.create_zone(domain='microsoft.com') + + self.assertEqual(zone.id, 'microsoft.com') + self.assertEqual(zone.domain, 'microsoft.com') + self.assertEqual(zone.type, None), + self.assertEqual(zone.ttl, None) + + def test_create_zone_zone_already_exists(self): + BuddyNSMockHttp.type = 'CREATE_ZONE_ZONE_ALREADY_EXISTS' + + try: + self.driver.create_zone(domain='newzone.com', + extra={'master': '13.0.0.1'}) + except ZoneAlreadyExistsError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, 'newzone.com') + else: + self.fail('Exception was not thrown') + + +class BuddyNSMockHttp(MockHttp): + fixtures = DNSFileFixtures('buddyns') + + def _api_v2_zone_EMPTY_ZONES_LIST(self, method, url, body, headers): + body = self.fixtures.load('empty_zones_list.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_LIST_ZONES(self, method, url, body, headers): + body = self.fixtures.load('list_zones.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_zonedoesnotexist_com_GET_ZONE_ZONE_DOES_NOT_EXIST( + self, method, url, body, headers): + body = self.fixtures.load('zone_does_not_exist.json') + + return 404, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_myexample_com_GET_ZONE_SUCCESS(self, method, url, body, + headers): + body = self.fixtures.load('get_zone_success.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_test_com_DELETE_ZONE_SUCCESS( + self, method, url, body, headers): + body = self.fixtures.load('delete_zone_success.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_test_com_DELETE_ZONE_ZONE_DOES_NOT_EXIST( + self, method, url, body, headers): + body = self.fixtures.load('zone_does_not_exist.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_CREATE_ZONE_SUCCESS(self, method, + url, body, headers): + body = self.fixtures.load('create_zone_success.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + def _api_v2_zone_CREATE_ZONE_ZONE_ALREADY_EXISTS( + self, method, url, body, headers): + body = self.fixtures.load('zone_already_exists.json') + + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + +if __name__ == '__main__': + sys.exit(unittest.main()) http://git-wip-us.apache.org/repos/asf/libcloud/blob/d102beef/libcloud/test/secrets.py-dist ---------------------------------------------------------------------- diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist index 2f7e202..4412f13 100644 --- a/libcloud/test/secrets.py-dist +++ b/libcloud/test/secrets.py-dist @@ -86,6 +86,7 @@ DNS_PARAMS_CLOUDFLARE = ('u...@example.com', 'key') DNS_PARAMS_AURORADNS = ('apikey', 'secretkey') DNS_PARAMS_NSONE = ('key', ) DNS_PARAMS_LUADNS = ('user', 'key') +DNS_PARAMS_BUDDYNS = ('key', ) # Container CONTAINER_PARAMS_DOCKER = ('user', 'password')