You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by an...@apache.org on 2016/03/31 00:46:56 UTC

[3/6] libcloud git commit: adds dns support for luadns

adds dns support for luadns


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/683f2b4c
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/683f2b4c
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/683f2b4c

Branch: refs/heads/trunk
Commit: 683f2b4c35992e1d0b120a059f8ee5dc1bcb860c
Parents: 28a16e2
Author: lostbird <lostbird@lostbird.(none)>
Authored: Wed Mar 30 01:51:53 2016 +0200
Committer: anthony-shaw <an...@gmail.com>
Committed: Thu Mar 31 09:46:24 2016 +1100

----------------------------------------------------------------------
 libcloud/common/luadns.py                       |  69 +++++
 libcloud/dns/drivers/luadns.py                  | 293 ++++++++++++++++++
 .../fixtures/luadns/create_record_success.json  |  10 +
 .../fixtures/luadns/create_zone_success.json    |  10 +
 .../fixtures/luadns/delete_record_success.json  |   0
 .../fixtures/luadns/delete_zone_success.json    |   0
 .../dns/fixtures/luadns/empty_records_list.json |   1 +
 .../dns/fixtures/luadns/empty_zones_list.json   |   1 +
 .../test/dns/fixtures/luadns/get_record.json    |  10 +
 libcloud/test/dns/fixtures/luadns/get_zone.json |  42 +++
 .../fixtures/luadns/record_does_not_exist.json  |   1 +
 .../test/dns/fixtures/luadns/records_list.json  |  22 ++
 .../fixtures/luadns/zone_already_exists.json    |   4 +
 .../fixtures/luadns/zone_does_not_exist.json    |   1 +
 .../test/dns/fixtures/luadns/zones_list.json    |  22 ++
 libcloud/test/dns/test_luadns.py                | 306 +++++++++++++++++++
 libcloud/test/secrets.py-dist                   |   1 +
 17 files changed, 793 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/common/luadns.py
----------------------------------------------------------------------
diff --git a/libcloud/common/luadns.py b/libcloud/common/luadns.py
new file mode 100644
index 0000000..373b36a
--- /dev/null
+++ b/libcloud/common/luadns.py
@@ -0,0 +1,69 @@
+import base64
+
+
+from libcloud.common.base import ConnectionUserAndKey, JsonResponse
+from libcloud.utils.py3 import b
+
+__all__ = [
+    'API_HOST',
+    'LuadnsException',
+    'LuadnsResponse',
+    'LuadnsConnection'
+]
+
+# Endpoint for luadns api
+API_HOST = 'api.luadns.com'
+
+
+class LuadnsResponse(JsonResponse):
+    errors = []
+    objects = []
+
+    def __init__(self, response, connection):
+        super(LuadnsResponse, self).__init__(response=response,
+                                             connection=connection)
+        self.errors, self.objects = self.parse_body_and_errors()
+        if not self.success():
+            raise LuadnsException(code=self.status,
+                                  message=self.errors.pop()['message'])
+
+    def parse_body_and_errors(self):
+        js = super(LuadnsResponse, self).parse_body()
+        if 'message' 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 LuadnsConnection(ConnectionUserAndKey):
+    host = API_HOST
+    responseCls = LuadnsResponse
+
+    def add_default_headers(self, headers):
+        b64string = b('%s:%s' % (self.user_id, self.key))
+        encoded = base64.b64encode(b64string).decode('utf-8')
+        authorization = 'Basic ' + encoded
+
+        headers['Accept'] = 'application/json'
+        headers['Authorization'] = authorization
+
+        return headers
+
+
+class LuadnsException(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 "Luadns %s %s" % (self.code, self.message)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/dns/drivers/luadns.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/drivers/luadns.py b/libcloud/dns/drivers/luadns.py
new file mode 100644
index 0000000..b0adecb
--- /dev/null
+++ b/libcloud/dns/drivers/luadns.py
@@ -0,0 +1,293 @@
+import sys
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from libcloud.common.luadns import LuadnsResponse, LuadnsConnection,\
+ LuadnsException
+from libcloud.dns.base import DNSDriver, Zone, Record
+from libcloud.dns.types import Provider, RecordType
+from libcloud.dns.types import ZoneDoesNotExistError, ZoneAlreadyExistsError
+from libcloud.dns.types import RecordDoesNotExistError
+
+
+__all__ = [
+    'LuadnsDNSDriver'
+]
+
+
+class LuadnsDNSResponse(LuadnsResponse):
+    pass
+
+
+class LuadnsDNSConnection(LuadnsConnection):
+    responseCls = LuadnsDNSResponse
+
+
+class LuadnsDNSDriver(DNSDriver):
+    type = Provider.LUADNS
+    name = 'Luadns'
+    website = 'https://www.luadns.com'
+    connectionCls = LuadnsDNSConnection
+
+    RECORD_TYPE_MAP = {
+        RecordType.A: 'A',
+        RecordType.AAAA: 'AAAA',
+        RecordType.CNAME: 'CNAME',
+        RecordType.MX: 'MX',
+        RecordType.NS: 'NS',
+        RecordType.PTR: 'PTR',
+        RecordType.SOA: 'SOA',
+        RecordType.SRV: 'SRV',
+        RecordType.TXT: 'TXT'
+    }
+
+    def list_zones(self):
+        """
+        Return a list of zones.
+
+        :return: ``list`` of :class:`Zone`
+        """
+        action = '/v1/zones'
+        response = self.connection.request(action=action,
+                                           method='GET')
+        zones = self._to_zones(response.parse_body())
+
+        return zones
+
+    def get_zone(self, zone_id):
+        """
+        Return a Zone instance.
+
+        :param zone_id: ID of the required zone
+        :type  zone_id: ``str``
+
+        :rtype: :class:`Zone`
+        """
+        action = '/v1/zones/%s' % zone_id
+        try:
+            response = self.connection.request(action=action)
+        except LuadnsException:
+            e = sys.exc_info()[1]
+            if e.message in ['Zone not found.', 'Resource not found.']:
+                raise ZoneDoesNotExistError(zone_id=zone_id,
+                                            value='', driver=self)
+            else:
+                raise e
+
+        zone = self._to_zone(response.parse_body())
+
+        return zone
+
+    def delete_zone(self, zone):
+        """
+        Delete a zone.
+
+        Note: This will delete all the records belonging to this zone.
+
+        :param zone: Zone to delete.
+        :type  zone: :class:`Zone`
+
+        :rtype: ``bool``
+        """
+        action = '/v1/zones/%s' % zone.id
+        try:
+            response = self.connection.request(action=action,
+                                               method='DELETE')
+        except LuadnsException:
+            e = sys.exc_info()[1]
+            if e.message in ['Resource not found.', 'Zone not found.']:
+                raise ZoneDoesNotExistError(zone_id=zone.id,
+                                            value='', driver=self)
+            else:
+                raise e
+
+        return response.status == 200
+
+    def create_zone(self, domain, type='master', ttl=None, extra=None):
+        """
+        Create a new zone.
+
+        :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 not really used)
+        :type  ttl: ``int``
+
+        :param extra: Extra attributes (driver specific). ('region_support',
+                      'zone_data')
+        :type extra: ``dict``
+
+        :rtype: :class:`Zone`
+        """
+        action = '/v1/zones'
+        data = json.dumps({'name': domain})
+        try:
+            response = self.connection.request(action=action,
+                                               method='POST',
+                                               data=data)
+        except LuadnsException:
+            e = sys.exc_info()[1]
+            if e.message == "Zone '%s' is taken already." % domain:
+                raise ZoneAlreadyExistsError(zone_id=domain,
+                                             value='',
+                                             driver=self)
+            else:
+                raise e
+        zone = self._to_zone(response.parse_body())
+
+        return zone
+
+    def list_records(self, zone):
+        """
+        Return a list of records for the provided zone.
+
+        :param zone: Zone to list records for.
+        :type zone: :class:`Zone`
+
+        :return: ``list`` of :class:`Record`
+        """
+        action = '/v1/zones/%s/records' % zone.id
+        response = self.connection.request(action=action)
+        records = self._to_records(response.parse_body(), zone=zone)
+
+        return records
+
+    def get_record(self, zone_id, record_id):
+        """
+        Return a Record instance.
+
+        :param zone_id: ID of the required zone
+        :type  zone_id: ``str``
+
+        :param record_id: ID of the required record
+        :type  record_id: ``str``
+
+        :rtype: :class:`Record`
+        """
+        zone = self.get_zone(zone_id=zone_id)
+        action = '/v1/zones/%s/records/%s' % (zone_id, record_id)
+        try:
+            response = self.connection.request(action=action)
+        except LuadnsException:
+            e = sys.exc_info()[1]
+            if e.message == 'Record not found.':
+                raise RecordDoesNotExistError(record_id=record_id, driver=self,
+                                              value='')
+            else:
+                raise e
+
+        record = self._to_record(response.parse_body(), zone=zone)
+
+        return record
+
+    def delete_record(self, record):
+        """
+        Delete a record.
+
+        :param record: Record to delete.
+        :type  record: :class:`Record`
+
+        :rtype: ``bool``
+        """
+        action = '/v1/zones/%s/records/%s' % (record.zone.id, record.id)
+        try:
+            response = self.connection.request(action=action,
+                                               method='DELETE')
+        except LuadnsException:
+            e = sys.exc_info()[1]
+            if e.message == 'Record not found.':
+                raise RecordDoesNotExistError(record_id=record.id, driver=self,
+                                              value='')
+            else:
+                raise e
+
+        return response.status == 200
+
+    def create_record(self, name, zone, type, data, extra=None):
+        """
+        Create a record.
+
+        :param name: Record name without the domain name (e.g. www).
+                     Note: If you want to create a record for a base domain
+                     name, you should specify empty string ('') for this
+                     argument.
+        :type  name: ``str``
+
+        :param zone: Zone which the records will be created for.
+        :type zone: :class:`Zone`
+
+        :param type: DNS record type ( 'A', 'AAAA', 'CNAME', 'MX', 'NS',
+                     'PTR', 'SOA', 'SRV', 'TXT').
+        :type  type: :class:`RecordType`
+
+        :param data: Data for the record (depends on the record type).
+        :type  data: ``str``
+
+        :param extra: (optional) Extra attributes ('prio', 'ttl').
+        :type  extra: ``dict``
+
+        :rtype: :class:`Record`
+        """
+        action = '/v1/zones/%s/records' % zone.id
+        to_post = {'name': name, 'content': data, 'type': type,
+                   'zone_id': int(zone.id)}
+        # ttl is required to create a record for luadns
+        # pass it through extra like this: extra={'ttl':ttl}
+        if extra is not None:
+            to_post.update(extra)
+        data = json.dumps(to_post)
+        try:
+            response = self.connection.request(action=action,
+                                               method='POST',
+                                               data=data)
+        except LuadnsException:
+            e = sys.exc_info()[1]
+            raise e
+
+        record = self._to_record(response.parse_body(), zone=zone)
+
+        return record
+
+    def _to_zone(self, item):
+        common_attr = ['id', 'name']
+        extra = {}
+        for key in item:
+            if key not in common_attr:
+                extra[key] = item.get(key)
+        zone = Zone(domain=item['name'], id=item['id'], type=None,
+                    ttl=None, driver=self, extra=extra)
+
+        return zone
+
+    def _to_zones(self, items):
+        zones = []
+        for item in items:
+            zones.append(self._to_zone(item))
+
+        return zones
+
+    def _to_record(self, item, zone):
+        common_attr = ['id', 'content', 'name', 'type']
+        extra = {}
+        for key in item:
+            if key not in common_attr:
+                extra[key] = item.get(key)
+        record = Record(id=item['id'], name=item['name'], type=item['type'],
+                        data=item['content'], zone=zone, driver=self,
+                        extra=extra)
+
+        return record
+
+    def _to_records(self, items, zone):
+        records = []
+        for item in items:
+            records.append(self._to_record(item, zone))
+
+        return records
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/create_record_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/create_record_success.json b/libcloud/test/dns/fixtures/luadns/create_record_success.json
new file mode 100644
index 0000000..c72c2b9
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/create_record_success.json
@@ -0,0 +1,10 @@
+{
+  "id": 31,
+  "name": "test.com.",
+  "type": "A",
+  "content": "127.0.0.1",
+  "ttl": 13,
+  "zone_id": 1,
+  "created_at": "2015-01-17T14:04:35.251785849Z",
+  "updated_at": "2015-01-17T14:04:35.251785972Z"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/create_zone_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/create_zone_success.json b/libcloud/test/dns/fixtures/luadns/create_zone_success.json
new file mode 100644
index 0000000..2df7075
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/create_zone_success.json
@@ -0,0 +1,10 @@
+{
+  "id": 3,
+  "name": "example.org",
+  "synced": false,
+  "queries_count": 0,
+  "records_count": 0,
+  "aliases_count": 0,
+  "redirects_count": 0,
+  "forwards_count": 0
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/delete_record_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/delete_record_success.json b/libcloud/test/dns/fixtures/luadns/delete_record_success.json
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/delete_zone_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/delete_zone_success.json b/libcloud/test/dns/fixtures/luadns/delete_zone_success.json
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/empty_records_list.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/empty_records_list.json b/libcloud/test/dns/fixtures/luadns/empty_records_list.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/empty_records_list.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/empty_zones_list.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/empty_zones_list.json b/libcloud/test/dns/fixtures/luadns/empty_zones_list.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/empty_zones_list.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/get_record.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/get_record.json b/libcloud/test/dns/fixtures/luadns/get_record.json
new file mode 100644
index 0000000..29dc374
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/get_record.json
@@ -0,0 +1,10 @@
+{
+  "id": 31,
+  "name": "example.com.",
+  "type": "MX",
+  "content": "10 mail.example.com.",
+  "ttl": 300,
+  "zone_id": 1,
+  "created_at": "2015-01-17T14:04:35.251785849Z",
+  "updated_at": "2015-01-17T14:04:35.251785972Z"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/get_zone.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/get_zone.json b/libcloud/test/dns/fixtures/luadns/get_zone.json
new file mode 100644
index 0000000..2f8ccf7
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/get_zone.json
@@ -0,0 +1,42 @@
+{
+  "id": 31,
+  "name": "example.org",
+  "synced": false,
+  "queries_count": 0,
+  "records_count": 3,
+  "aliases_count": 0,
+  "redirects_count": 0,
+  "forwards_count": 0,
+  "records": [
+    {
+      "id": 6683,
+      "name": "example.org.",
+      "type": "SOA",
+      "content": "a.ns.luadns.net. hostmaster.luadns.com. 1421501178 1200 120 604800 3600",
+      "ttl": 3600,
+      "zone_id": 3,
+      "created_at": "2015-01-17T13:26:17.52747Z",
+      "updated_at": "2015-01-17T13:26:17.527471Z"
+    },
+    {
+      "id": 6684,
+      "name": "example.org.",
+      "type": "NS",
+      "content": "a.ns.luadns.net.",
+      "ttl": 86400,
+      "zone_id": 3,
+      "created_at": "2015-01-17T13:26:17.529741Z",
+      "updated_at": "2015-01-17T13:26:17.529741Z"
+    },
+    {
+      "id": 6685,
+      "name": "example.org.",
+      "type": "NS",
+      "content": "b.ns.luadns.net.",
+      "ttl": 86400,
+      "zone_id": 3,
+      "created_at": "2015-01-17T13:26:17.531911Z",
+      "updated_at": "2015-01-17T13:26:17.531911Z"
+    }
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/record_does_not_exist.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/record_does_not_exist.json b/libcloud/test/dns/fixtures/luadns/record_does_not_exist.json
new file mode 100644
index 0000000..96f3d9f
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/record_does_not_exist.json
@@ -0,0 +1 @@
+{"status":"Not Found","request_id":"be9cd5fd857254a4059357b2354edb92","message":"Record not found."}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/records_list.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/records_list.json b/libcloud/test/dns/fixtures/luadns/records_list.json
new file mode 100644
index 0000000..d4cd30a
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/records_list.json
@@ -0,0 +1,22 @@
+[
+  {
+    "id": 6683,
+    "name": "example.org.",
+    "type": "NS",
+    "content": "b.ns.luadns.net.",
+    "ttl": 86400,
+    "zone_id": 3,
+    "created_at": "2015-01-17T13:08:37.522452Z",
+    "updated_at": "2015-01-17T13:08:37.522452Z"
+  },
+  {
+    "id": 6684,
+    "name": "example.org.",
+    "type": "NS",
+    "content": "a.ns.luadns.net.",
+    "ttl": 86400,
+    "zone_id": 3,
+    "created_at": "2015-01-17T13:08:37.520623Z",
+    "updated_at": "2015-01-17T13:08:37.520623Z"
+  }
+]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/zone_already_exists.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/zone_already_exists.json b/libcloud/test/dns/fixtures/luadns/zone_already_exists.json
new file mode 100644
index 0000000..18e82d7
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/zone_already_exists.json
@@ -0,0 +1,4 @@
+{ "status":"Forbidden",
+  "request_id":"a75744f55cabe0411e02fa97e1a5d91b",
+  "message":"Zone 'test.com' is taken already."
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/zone_does_not_exist.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/zone_does_not_exist.json b/libcloud/test/dns/fixtures/luadns/zone_does_not_exist.json
new file mode 100644
index 0000000..2de2301
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/zone_does_not_exist.json
@@ -0,0 +1 @@
+{"status":"Not Found","request_id":"be9cd5fd857254a4059357b2354edb92","message":"Zone not found."}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/fixtures/luadns/zones_list.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/luadns/zones_list.json b/libcloud/test/dns/fixtures/luadns/zones_list.json
new file mode 100644
index 0000000..e06e64e
--- /dev/null
+++ b/libcloud/test/dns/fixtures/luadns/zones_list.json
@@ -0,0 +1,22 @@
+[
+  {
+    "id": 1,
+    "name": "example.com",
+    "synced": false,
+    "queries_count": 0,
+    "records_count": 3,
+    "aliases_count": 0,
+    "redirects_count": 0,
+    "forwards_count": 0
+  },
+  {
+    "id": 2,
+    "name": "example.net",
+    "synced": false,
+    "queries_count": 0,
+    "records_count": 3,
+    "aliases_count": 0,
+    "redirects_count": 0,
+    "forwards_count": 0
+  }
+]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/dns/test_luadns.py
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/test_luadns.py b/libcloud/test/dns/test_luadns.py
new file mode 100644
index 0000000..4d174e8
--- /dev/null
+++ b/libcloud/test/dns/test_luadns.py
@@ -0,0 +1,306 @@
+import sys
+import unittest
+
+from libcloud.utils.py3 import httplib
+from libcloud.dns.drivers.luadns import LuadnsDNSDriver
+from libcloud.test import MockHttp
+from libcloud.test.file_fixtures import DNSFileFixtures
+from libcloud.test.secrets import DNS_PARAMS_LUADNS
+from libcloud.dns.types import ZoneDoesNotExistError, ZoneAlreadyExistsError
+from libcloud.dns.types import RecordDoesNotExistError
+from libcloud.dns.types import RecordType
+from libcloud.dns.base import Zone, Record
+
+
+class LuadnsTests(unittest.TestCase):
+
+    def setUp(self):
+        LuadnsMockHttp.type = None
+        LuadnsDNSDriver.connectionCls.conn_classes = (
+            None, LuadnsMockHttp)
+        self.driver = LuadnsDNSDriver(*DNS_PARAMS_LUADNS)
+        self.test_zone = Zone(id='11', type='master', ttl=None,
+                              domain='example.com', extra={},
+                              driver=self.driver)
+        self.test_record = Record(id='13', type=RecordType.A,
+                                  name='example.com', zone=self.test_zone,
+                                  data='127.0.0.1', driver=self, extra={})
+
+    def assertHasKeys(self, dictionary, keys):
+        for key in keys:
+            self.assertTrue(key in dictionary,
+                            'key "%s" not in dictionary' % key)
+
+    def test_list_zones_empty(self):
+        LuadnsMockHttp.type = 'EMPTY_ZONES_LIST'
+        zones = self.driver.list_zones()
+
+        self.assertEqual(zones, [])
+
+    def test_list_zones_success(self):
+        zones = self.driver.list_zones()
+
+        self.assertEqual(len(zones), 2)
+
+        zone = zones[0]
+        self.assertEqual(zone.id, '1')
+        self.assertEqual(zone.domain, 'example.com')
+        self.assertEqual(zone.type, None)
+        self.assertEqual(zone.driver, self.driver)
+        self.assertEqual(zone.ttl, None)
+
+        second_zone = zones[1]
+        self.assertEqual(second_zone.id, '2')
+        self.assertEqual(second_zone.domain, 'example.net')
+        self.assertEqual(second_zone.type, None)
+        self.assertEqual(second_zone.driver, self.driver)
+        self.assertEqual(second_zone.ttl, None)
+
+    def test_get_zone_zone_does_not_exist(self):
+        LuadnsMockHttp.type = 'ZONE_DOES_NOT_EXIST'
+        try:
+            self.driver.get_zone(zone_id='13')
+        except ZoneDoesNotExistError:
+            e = sys.exc_info()[1]
+            self.assertEqual(e.zone_id, '13')
+        else:
+            self.fail('Exception was not thrown')
+
+    def test_get_zone_success(self):
+        LuadnsMockHttp.type = 'GET_ZONE_SUCCESS'
+        zone = self.driver.get_zone(zone_id='31')
+
+        self.assertEqual(zone.id, '31')
+        self.assertEqual(zone.domain, 'example.org')
+        self.assertEqual(zone.type, None)
+        self.assertEqual(zone.ttl, None)
+        self.assertEqual(zone.driver, self.driver)
+
+    def test_delete_zone_success(self):
+        LuadnsMockHttp.type = 'DELETE_ZONE_SUCCESS'
+        zone = self.test_zone
+        status = self.driver.delete_zone(zone=zone)
+
+        self.assertEqual(status, True)
+
+    def test_delete_zone_zone_does_not_exist(self):
+        LuadnsMockHttp.type = 'DELETE_ZONE_ZONE_DOES_NOT_EXIST'
+        zone = self.test_zone
+        try:
+            self.driver.delete_zone(zone=zone)
+        except ZoneDoesNotExistError:
+            e = sys.exc_info()[1]
+            self.assertEqual(e.zone_id, '11')
+        else:
+            self.fail('Exception was not thrown')
+
+    def test_create_zone_success(self):
+        LuadnsMockHttp.type = 'CREATE_ZONE_SUCCESS'
+        zone = self.driver.create_zone(domain='example.org')
+
+        self.assertEqual(zone.id, '3')
+        self.assertEqual(zone.domain, 'example.org')
+        self.assertEqual(zone.type, None)
+        self.assertEqual(zone.ttl, None)
+        self.assertEqual(zone.driver, self.driver)
+
+    def test_create_zone_zone_zone_already_exists(self):
+        LuadnsMockHttp.type = 'CREATE_ZONE_ZONE_ALREADY_EXISTS'
+        try:
+            self.driver.create_zone(domain='test.com')
+        except ZoneAlreadyExistsError:
+            e = sys.exc_info()[1]
+            self.assertEqual(e.zone_id, 'test.com')
+        else:
+            self.fail('Exception was not thrown')
+
+    def test_list_records_empty(self):
+        LuadnsMockHttp.type = 'EMPTY_RECORDS_LIST'
+        zone = self.test_zone
+        records = self.driver.list_records(zone=zone)
+
+        self.assertEqual(records, [])
+
+    def test_list_records_success(self):
+        LuadnsMockHttp.type = 'LIST_RECORDS_SUCCESS'
+        zone = self.test_zone
+        records = self.driver.list_records(zone=zone)
+
+        self.assertEqual(len(records), 2)
+
+        record = records[0]
+        self.assertEqual(record.id, '6683')
+        self.assertEqual(record.type, 'NS')
+        self.assertEqual(record.name, 'example.org.')
+        self.assertEqual(record.data, 'b.ns.luadns.net.')
+        self.assertEqual(record.zone, self.test_zone)
+        self.assertEqual(record.zone.id, '11')
+
+        second_record = records[1]
+        self.assertEqual(second_record.id, '6684')
+        self.assertEqual(second_record.type, 'NS')
+        self.assertEqual(second_record.name, 'example.org.')
+        self.assertEqual(second_record.data, 'a.ns.luadns.net.')
+        self.assertEqual(second_record.zone, self.test_zone)
+
+    def test_get_record_record_does_not_exist(self):
+        LuadnsMockHttp.type = 'GET_RECORD_RECORD_DOES_NOT_EXIST'
+        try:
+            self.driver.get_record(zone_id='31', record_id='31')
+        except RecordDoesNotExistError:
+            e = sys.exc_info()[1]
+            self.assertEqual(e.record_id, '31')
+        else:
+            self.fail('Exception was not thrown')
+
+    def test_get_record_success(self):
+        LuadnsMockHttp.type = 'GET_RECORD_SUCCESS'
+        record = self.driver.get_record(zone_id='31', record_id='31')
+
+        self.assertEqual(record.id, '31')
+        self.assertEqual(record.type, 'MX')
+        self.assertEqual(record.name, 'example.com.')
+        self.assertEqual(record.data, '10 mail.example.com.')
+
+    def test_delete_record_success(self):
+        LuadnsMockHttp.type = 'DELETE_RECORD_SUCCESS'
+        record = self.test_record
+        status = self.driver.delete_record(record=record)
+
+        self.assertEqual(status, True)
+
+    def test_delete_record_RECORD_DOES_NOT_EXIST_ERROR(self):
+        LuadnsMockHttp.type = 'DELETE_RECORD_RECORD_DOES_NOT_EXIST'
+        record = self.test_record
+        try:
+            self.driver.delete_record(record=record)
+        except RecordDoesNotExistError:
+            e = sys.exc_info()[1]
+            self.assertEqual(e.record_id, '13')
+        else:
+            self.fail('Exception was not thrown')
+
+    def test_create_record_success(self):
+        LuadnsMockHttp.type = 'CREATE_RECORD_SUCCESS'
+        record = self.driver.create_record(name='test.com.',
+                                           zone=self.test_zone,
+                                           type='A',
+                                           data='127.0.0.1',
+                                           extra={'ttl': 13})
+        self.assertEqual(record.id, '31')
+        self.assertEqual(record.name, 'test.com.')
+        self.assertEqual(record.data, '127.0.0.1')
+        self.assertEqual(record.ttl, None)
+
+    def test_record_already_exists_error(self):
+        pass
+
+
+class LuadnsMockHttp(MockHttp):
+    fixtures = DNSFileFixtures('luadns')
+
+    def _v1_zones(self, method, url, body, headers):
+        body = self.fixtures.load('zones_list.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_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 _v1_zones_13_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 _v1_zones_31_GET_ZONE_SUCCESS(self, method, url,
+                                      body, headers):
+        body = self.fixtures.load('get_zone.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_11_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 _v1_zones_11_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 _v1_zones_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 _v1_zones_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]
+
+    def _v1_zones_11_records_EMPTY_RECORDS_LIST(self, method, url, body,
+                                                headers):
+        body = self.fixtures.load('empty_records_list.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_11_records_LIST_RECORDS_SUCCESS(self, method, url,
+                                                  body, headers):
+        body = self.fixtures.load('records_list.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_31_records_31_GET_RECORD_RECORD_DOES_NOT_EXIST(
+            self, method, url, body, headers):
+        body = self.fixtures.load('record_does_not_exist.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_31_GET_RECORD_RECORD_DOES_NOT_EXIST(
+            self, method, url, body, headers):
+
+        body = self.fixtures.load('get_zone.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_31_GET_RECORD_SUCCESS(self, method, url,
+                                        body, headers):
+        body = self.fixtures.load('get_zone.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_31_records_31_GET_RECORD_SUCCESS(self, method, url,
+                                                   body, headers):
+        body = self.fixtures.load('get_record.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_11_records_13_DELETE_RECORD_SUCCESS(self, method, url,
+                                                      body, headers):
+        body = self.fixtures.load('delete_record_success.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_11_records_13_DELETE_RECORD_RECORD_DOES_NOT_EXIST(
+            self, method, url, body, headers):
+        body = self.fixtures.load('record_does_not_exist.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+    def _v1_zones_11_records_CREATE_RECORD_SUCCESS(self, method, url,
+                                                   body, headers):
+        body = self.fixtures.load('create_record_success.json')
+
+        return httplib.OK, body, {}, httplib.responses[httplib.OK]
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/683f2b4c/libcloud/test/secrets.py-dist
----------------------------------------------------------------------
diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist
index 480ac3f..2f7e202 100644
--- a/libcloud/test/secrets.py-dist
+++ b/libcloud/test/secrets.py-dist
@@ -85,6 +85,7 @@ DNS_PARAMS_GODADDY = ('customer-id', 'api_user', 'api_key')
 DNS_PARAMS_CLOUDFLARE = ('user@example.com', 'key')
 DNS_PARAMS_AURORADNS = ('apikey', 'secretkey')
 DNS_PARAMS_NSONE = ('key', )
+DNS_PARAMS_LUADNS = ('user', 'key')
 
 # Container
 CONTAINER_PARAMS_DOCKER = ('user', 'password')