Repository: libcloud
Updated Branches:
  refs/heads/trunk 2a450e407 -> e2c9f62da


add NearlyFreeSpeech.net DNS driver
Closes #733


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

Branch: refs/heads/trunk
Commit: e2c9f62da71875b66b119764952db830e0cf809c
Parents: 2a450e4
Author: Ken Dreyer <ktdre...@ktdreyer.com>
Authored: Wed Mar 30 07:46:00 2016 -0600
Committer: anthony-shaw <anthony.p.s...@gmail.com>
Committed: Fri Apr 1 09:33:33 2016 +1100

----------------------------------------------------------------------
 docs/dns/_supported_providers.rst               |   2 +
 docs/dns/drivers/nfsn.net                       |  25 +++
 docs/examples/dns/nfsn/instantiate_driver.py    |   5 +
 libcloud/common/nfsn.py                         | 114 +++++++++++
 libcloud/dns/drivers/nfsn.py                    | 198 +++++++++++++++++++
 libcloud/dns/providers.py                       |   2 +
 libcloud/dns/types.py                           |   2 +-
 libcloud/test/common/test_nfsn.py               |  65 ++++++
 .../test/dns/fixtures/nfsn/list_one_record.json |   9 +
 .../test/dns/fixtures/nfsn/list_records.json    |  16 ++
 .../dns/fixtures/nfsn/list_records_created.json |  23 +++
 .../dns/fixtures/nfsn/record_not_removed.json   |   4 +
 .../test/dns/fixtures/nfsn/zone_not_found.json  |   4 +
 libcloud/test/dns/test_nfsn.py                  | 148 ++++++++++++++
 14 files changed, 616 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/docs/dns/_supported_providers.rst
----------------------------------------------------------------------
diff --git a/docs/dns/_supported_providers.rst 
b/docs/dns/_supported_providers.rst
index 884c03d..5454f0e 100644
--- a/docs/dns/_supported_providers.rst
+++ b/docs/dns/_supported_providers.rst
@@ -14,6 +14,7 @@ Provider              Documentation                           
  Provider constan
 `Host Virtual DNS`_   :doc:`Click </dns/drivers/hostvirtual>`   HOSTVIRTUAL    
   :mod:`libcloud.dns.drivers.hostvirtual`  :class:`HostVirtualDNSDriver` 
 `Linode DNS`_                                                   LINODE         
   :mod:`libcloud.dns.drivers.linode`       :class:`LinodeDNSDriver`      
 `Liquidweb DNS`_      :doc:`Click </dns/drivers/liquidweb>`     LIQUIDWEB      
   :mod:`libcloud.dns.drivers.liquidweb`    :class:`LiquidWebDNSDriver`   
+`NearlyFreeSpeech.net DNS`_                                     NFSN           
   :mod:`libcloud.dns.drivers.nfsn`         :class:`NFSNDNSDriver`      
 `Point DNS`_          :doc:`Click </dns/drivers/pointdns>`      POINTDNS       
   :mod:`libcloud.dns.drivers.pointdns`     :class:`PointDNSDriver`       
 `Rackspace DNS`_                                                RACKSPACE      
   :mod:`libcloud.dns.drivers.rackspace`    :class:`RackspaceDNSDriver`   
 `Rackspace DNS (UK)`_                                           RACKSPACE_UK   
   :mod:`libcloud.dns.drivers.rackspace`    :class:`RackspaceUKDNSDriver` 
@@ -37,6 +38,7 @@ Provider              Documentation                           
  Provider constan
 .. _`Host Virtual DNS`: https://www.hostvirtual.com/
 .. _`Linode DNS`: http://www.linode.com/
 .. _`Liquidweb DNS`: https://www.liquidweb.com
+.. _`NearlyFreeSpeech.net DNS`: https://www.nearlyfreespeech.net/
 .. _`Point DNS`: https://pointhq.com/
 .. _`Rackspace DNS`: http://www.rackspace.com/
 .. _`Rackspace DNS (UK)`: http://www.rackspace.com/

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/docs/dns/drivers/nfsn.net
----------------------------------------------------------------------
diff --git a/docs/dns/drivers/nfsn.net b/docs/dns/drivers/nfsn.net
new file mode 100644
index 0000000..89551f4
--- /dev/null
+++ b/docs/dns/drivers/nfsn.net
@@ -0,0 +1,25 @@
+NFSN DNS Driver Documentation
+===================================
+
+`NFSN`_, Inc. is a U.S. company that provides web hosting and domain name
+server services.
+
+Instantiating the driver
+------------------------
+
+To instantiate the driver you need to pass the account name and API key to the
+driver constructor as shown below. Obtain your API key from NFSN by submitting
+a secure support request via the `control panel`_.
+
+.. literalinclude:: /examples/dns/nfsn/instantiate_driver.py
+   :language: python
+
+API Docs
+--------
+
+.. autoclass:: libcloud.dns.drivers.nfsn.NFSNDNSDriver
+    :members:
+    :inherited-members:
+
+.. _`NFSN`: https://www.nearlyfreespeech.net/
+.. _`control panel`: https://members.nearlyfreespeech.net/

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/docs/examples/dns/nfsn/instantiate_driver.py
----------------------------------------------------------------------
diff --git a/docs/examples/dns/nfsn/instantiate_driver.py 
b/docs/examples/dns/nfsn/instantiate_driver.py
new file mode 100644
index 0000000..60d9b0e
--- /dev/null
+++ b/docs/examples/dns/nfsn/instantiate_driver.py
@@ -0,0 +1,5 @@
+from libcloud.dns.types import Provider
+from libcloud.dns.providers import get_driver
+
+cls = get_driver(Provider.NFSN)
+driver = cls('<account name>', '<api key>')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/common/nfsn.py
----------------------------------------------------------------------
diff --git a/libcloud/common/nfsn.py b/libcloud/common/nfsn.py
new file mode 100644
index 0000000..4286f0c
--- /dev/null
+++ b/libcloud/common/nfsn.py
@@ -0,0 +1,114 @@
+# 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.
+import hashlib
+import random
+import string
+import time
+
+from libcloud.common.base import ConnectionUserAndKey
+from libcloud.common.base import JsonResponse
+from libcloud.common.types import InvalidCredsError, ProviderError
+from libcloud.utils.py3 import basestring, httplib, urlencode
+
+
+SALT_CHARACTERS = string.ascii_letters + string.digits
+
+
+class NFSNException(ProviderError):
+    def __init__(self, value, http_code, code, driver=None):
+        self.code = code
+        super(NFSNException, self).__init__(value, http_code, driver)
+
+
+class NFSNResponse(JsonResponse):
+
+    def parse_error(self):
+        if self.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError('Invalid provider credentials')
+
+        body = self.parse_body()
+
+        if isinstance(body, basestring):
+            return body + ' (HTTP Code: %d)' % self.status
+
+        error = body.get('error', None)
+        debug = body.get('debug', None)
+        # If we only have one of "error" or "debug", use the one that we have.
+        # If we have both, use both, with a space character in between them.
+        value = 'No message specified'
+        if error is not None:
+            value = error
+        if debug is not None:
+            value = debug
+        if error is not None and value is not None:
+            value = error + ' ' + value
+        value = value + ' (HTTP Code: %d)' % self.status
+
+        return value
+
+
+class NFSNConnection(ConnectionUserAndKey):
+    host = 'api.nearlyfreespeech.net'
+    responseCls = NFSNResponse
+    allow_insecure = False
+
+    def _header(self, action, data):
+        """ Build the contents of the X-NFSN-Authentication HTTP header. See
+        https://members.nearlyfreespeech.net/wiki/API/Introduction for
+        more explanation. """
+        login = self.user_id
+        timestamp = self._timestamp()
+        salt = self._salt()
+        api_key = self.key
+        data = urlencode(data)
+        data_hash = hashlib.sha1(data.encode('utf-8')).hexdigest()
+
+        string = ';'.join((login, timestamp, salt, api_key, action, data_hash))
+        string_hash = hashlib.sha1(string.encode('utf-8')).hexdigest()
+
+        return ';'.join((login, timestamp, salt, string_hash))
+
+    def request(self, action, params=None, data='', headers=None,
+                method='GET'):
+        """ Add the X-NFSN-Authentication header to an HTTP request. """
+        if not headers:
+            headers = {}
+        if not params:
+            params = {}
+        header = self._header(action, data)
+
+        headers['X-NFSN-Authentication'] = header
+        if method == 'POST':
+            headers['Content-Type'] = 'application/x-www-form-urlencoded'
+
+        return ConnectionUserAndKey.request(self, action, params, data,
+                                            headers, method)
+
+    def encode_data(self, data):
+        """ NFSN expects the body to be regular key-value pairs that are not
+        JSON-encoded. """
+        if data:
+            data = urlencode(data)
+        return data
+
+    def _salt(self):
+        """ Return a 16-character alphanumeric string. """
+        r = random.SystemRandom()
+        return ''.join(r.choice(SALT_CHARACTERS) for _ in range(16))
+
+    def _timestamp(self):
+        """ Return the current number of seconds since the Unix epoch,
+        as a string. """
+        return str(int(time.time()))

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/dns/drivers/nfsn.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/drivers/nfsn.py b/libcloud/dns/drivers/nfsn.py
new file mode 100644
index 0000000..0231ec4
--- /dev/null
+++ b/libcloud/dns/drivers/nfsn.py
@@ -0,0 +1,198 @@
+# 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.
+"""
+NFSN DNS Driver
+"""
+import re
+import sys
+
+from libcloud.common.exceptions import BaseHTTPError
+from libcloud.common.nfsn import NFSNConnection
+from libcloud.dns.base import DNSDriver, Zone, Record
+from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError
+from libcloud.dns.types import RecordAlreadyExistsError
+from libcloud.dns.types import Provider, RecordType
+from libcloud.utils.py3 import httplib
+
+__all__ = [
+    'NFSNDNSDriver',
+]
+
+# The NFSN API does not return any internal "ID" strings for any DNS records.
+# This means that we must set all returned Record objects' id properties to
+# None. It also means that we cannot implement libcloud APIs that rely on
+# record_id, such as get_record(). Instead, the NFSN-specific
+# ex_get_records_by() method will return the desired Record objects.
+#
+# Additionally, the NFSN API does not provide ways to create, delete, or list
+# all zones, so create_zone(), delete_zone(), and list_zones() are not
+# implemented.
+
+
+class NFSNDNSDriver(DNSDriver):
+    type = Provider.NFSN
+    name = 'NFSN DNS'
+    website = 'https://www.nearlyfreespeech.net'
+    connectionCls = NFSNConnection
+
+    RECORD_TYPE_MAP = {
+        RecordType.A: 'A',
+        RecordType.AAAA: 'AAAA',
+        RecordType.CNAME: 'CNAME',
+        RecordType.MX: 'MX',
+        RecordType.NS: 'NS',
+        RecordType.SRV: 'SRV',
+        RecordType.TXT: 'TXT',
+        RecordType.PTR: 'PTR',
+    }
+
+    def list_records(self, zone):
+        """
+        Return a list of all records for the provided zone.
+
+        :param zone: Zone to list records for.
+        :type zone: :class:`Zone`
+
+        :return: ``list`` of :class:`Record`
+        """
+        # Just use ex_get_records_by() with no name or type filters.
+        return self.ex_get_records_by(zone)
+
+    def get_zone(self, zone_id):
+        """
+        Return a Zone instance.
+
+        :param zone_id: name of the required zone, for example "example.com".
+        :type  zone_id: ``str``
+
+        :rtype: :class:`Zone`
+        :raises: ZoneDoesNotExistError: If no zone could be found.
+        """
+        # We will check if there is a serial property for this zone. If so,
+        # then the zone exists.
+        try:
+            self.connection.request(action='/dns/%s/serial' % zone_id)
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.NOT_FOUND:
+                raise ZoneDoesNotExistError(zone_id=None, driver=self,
+                                            value=e.message)
+            raise e
+        return Zone(id=None, domain=zone_id, type='master', ttl=3600,
+                    driver=self)
+
+    def ex_get_records_by(self, zone, name=None, type=None):
+        """
+        Return a list of records for the provided zone, filtered by name and/or
+        type.
+
+        :param zone: Zone to list records for.
+        :type zone: :class:`Zone`
+
+        :param zone: Zone where the requested records are found.
+        :type  zone: :class:`Zone`
+
+        :param name: name of the records, for example "www". (optional)
+        :type  name: ``str``
+
+        :param type: DNS record type (A, MX, TXT). (optional)
+        :type  type: :class:`RecordType`
+
+        :return: ``list`` of :class:`Record`
+        """
+        payload = {}
+        if name is not None:
+            payload['name'] = name
+        if type is not None:
+            payload['type'] = type
+
+        action = '/dns/%s/listRRs' % zone.domain
+        response = self.connection.request(action=action, data=payload,
+                                           method='POST')
+        return self._to_records(response, zone)
+
+    def create_record(self, name, zone, type, data, extra=None):
+        """
+        Create a new 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 where the requested record is created.
+        :type  zone: :class:`Zone`
+
+        :param type: DNS record type (A, MX, TXT).
+        :type  type: :class:`RecordType`
+
+        :param data: Data for the record (depends on the record type).
+        :type  data: ``str``
+
+        :param extra: Extra attributes (driver specific, e.g. 'ttl').
+                      (optional)
+        :type extra: ``dict``
+
+        :rtype: :class:`Record`
+        """
+        action = '/dns/%s/addRR' % zone.domain
+        payload = {'name': name, 'data': data, 'type': type}
+        if extra is not None and extra.get('ttl', None) is not None:
+            payload['ttl'] = extra['ttl']
+        try:
+            self.connection.request(action=action, data=payload, method='POST')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            exists_re = re.compile(r'That RR already exists on the domain')
+            if e.code == httplib.BAD_REQUEST and \
+               re.search(exists_re, e.message) is not None:
+                value = '"%s" already exists in %s' % (name, zone.domain)
+                raise RecordAlreadyExistsError(value=value, driver=self,
+                                               record_id=None)
+            raise e
+        return self.ex_get_records_by(zone=zone, name=name, type=type)[0]
+
+    def delete_record(self, record):
+        """
+        Use this method to delete a record.
+
+        :param record: record to delete
+        :type record: `Record`
+
+        :rtype: Bool
+        """
+        action = '/dns/%s/removeRR' % record.zone.domain
+        payload = {'name': record.name, 'data': record.data,
+                   'type': record.type}
+        try:
+            self.connection.request(action=action, data=payload, method='POST')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.NOT_FOUND:
+                raise RecordDoesNotExistError(value=e.message, driver=self,
+                                              record_id=None)
+            raise e
+        return True
+
+    def _to_record(self, item, zone):
+        ttl = int(item['ttl'])
+        return Record(id=None, name=item['name'], data=item['data'],
+                      type=item['type'], zone=zone, driver=self, ttl=ttl)
+
+    def _to_records(self, items, zone):
+        records = []
+        for item in items.object:
+            records.append(self._to_record(item, zone))
+        return records

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/dns/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py
index e8e1619..5f2e56a 100644
--- a/libcloud/dns/providers.py
+++ b/libcloud/dns/providers.py
@@ -57,6 +57,8 @@ DRIVERS = {
     ('libcloud.dns.drivers.godaddy', 'GoDaddyDNSDriver'),
     Provider.CLOUDFLARE:
     ('libcloud.dns.drivers.cloudflare', 'CloudFlareDNSDriver'),
+    Provider.NFSN:
+    ('libcloud.dns.drivers.nfsn', 'NFSNDNSDriver'),
 
     # Deprecated
     Provider.RACKSPACE_US:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/dns/types.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py
index b474906..4fba3b2 100644
--- a/libcloud/dns/types.py
+++ b/libcloud/dns/types.py
@@ -50,7 +50,7 @@ class Provider(object):
     CLOUDFLARE = 'cloudflare'
     NSONE = 'nsone'
     LUADNS = 'luadns'
-
+    NFSN = 'nfsn'
     # Deprecated
     RACKSPACE_US = 'rackspace_us'
     RACKSPACE_UK = 'rackspace_uk'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/common/test_nfsn.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_nfsn.py 
b/libcloud/test/common/test_nfsn.py
new file mode 100644
index 0000000..5e77f32
--- /dev/null
+++ b/libcloud/test/common/test_nfsn.py
@@ -0,0 +1,65 @@
+from mock import Mock, patch
+import string
+import sys
+import unittest
+
+from libcloud.common.nfsn import NFSNConnection
+from libcloud.test import LibcloudTestCase, MockHttp
+from libcloud.utils.py3 import httplib
+
+
+mock_time = Mock()
+mock_time.return_value = 1000000
+
+mock_salt = Mock()
+mock_salt.return_value = 'yumsalty1234'
+
+mock_header = 
'testid;1000000;yumsalty1234;66dfb282a9532e5b8e6a9517764d5fbc001a4a2e'
+
+
+class NFSNConnectionTestCase(LibcloudTestCase):
+
+    def setUp(self):
+        NFSNConnection.conn_classes = (None, NFSNMockHttp)
+        NFSNMockHttp.type = None
+        self.driver = NFSNConnection('testid', 'testsecret')
+
+    def test_salt_length(self):
+        self.assertEqual(16, len(self.driver._salt()))
+
+    def test_salt_is_unique(self):
+        s1 = self.driver._salt()
+        s2 = self.driver._salt()
+        self.assertNotEqual(s1, s2)
+
+    def test_salt_characters(self):
+        """ salt must be alphanumeric """
+        salt_characters = string.ascii_letters + string.digits
+        for c in self.driver._salt():
+            self.assertIn(c, salt_characters)
+
+    @patch('time.time', mock_time)
+    def test_timestamp(self):
+        """ Check that timestamp uses time.time """
+        self.assertEqual('1000000', self.driver._timestamp())
+
+    @patch('time.time', mock_time)
+    @patch('libcloud.common.nfsn.NFSNConnection._salt', mock_salt)
+    def test_auth_header(self):
+        """ Check that X-NFSN-Authentication is set """
+        response = self.driver.request(action='/testing')
+        self.assertEqual(httplib.OK, response.status)
+
+
+class NFSNMockHttp(MockHttp):
+
+    def _testing(self, method, url, body, headers):
+        if headers['X-NFSN-Authentication'] == mock_header:
+            return (httplib.OK, '', {}, httplib.responses[httplib.OK])
+        else:
+            return (httplib.UNAUTHORIZED, '', {},
+                    httplib.responses[httplib.UNAUTHORIZED])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/dns/fixtures/nfsn/list_one_record.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/nfsn/list_one_record.json 
b/libcloud/test/dns/fixtures/nfsn/list_one_record.json
new file mode 100644
index 0000000..5af4865
--- /dev/null
+++ b/libcloud/test/dns/fixtures/nfsn/list_one_record.json
@@ -0,0 +1,9 @@
+[
+    {
+        "data": "192.0.2.1",
+        "name": "",
+        "scope": "member",
+        "ttl": "3600",
+        "type": "A"
+    }
+]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/dns/fixtures/nfsn/list_records.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/nfsn/list_records.json 
b/libcloud/test/dns/fixtures/nfsn/list_records.json
new file mode 100644
index 0000000..bf9fa0c
--- /dev/null
+++ b/libcloud/test/dns/fixtures/nfsn/list_records.json
@@ -0,0 +1,16 @@
+[
+    {
+        "data": "192.0.2.1",
+        "name": "",
+        "scope": "member",
+        "ttl": "3600",
+        "type": "A"
+    },
+    {
+        "data": "ns.phx2.nearlyfreespeech.net.",
+        "name": "",
+        "scope": "member",
+        "ttl": "3600",
+        "type": "NS"
+    }
+]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/dns/fixtures/nfsn/list_records_created.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/nfsn/list_records_created.json 
b/libcloud/test/dns/fixtures/nfsn/list_records_created.json
new file mode 100644
index 0000000..ca929a4
--- /dev/null
+++ b/libcloud/test/dns/fixtures/nfsn/list_records_created.json
@@ -0,0 +1,23 @@
+[
+    {
+        "data": "127.0.0.1",
+        "name": "newrecord",
+        "scope": "member",
+        "ttl": "900",
+        "type": "A"
+    },
+    {
+        "data": "192.0.2.1",
+        "name": "",
+        "scope": "member",
+        "ttl": "3600",
+        "type": "A"
+    },
+    {
+        "data": "ns.phx2.nearlyfreespeech.net.",
+        "name": "",
+        "scope": "member",
+        "ttl": "3600",
+        "type": "NS"
+    }
+]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/dns/fixtures/nfsn/record_not_removed.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/nfsn/record_not_removed.json 
b/libcloud/test/dns/fixtures/nfsn/record_not_removed.json
new file mode 100644
index 0000000..28b5663
--- /dev/null
+++ b/libcloud/test/dns/fixtures/nfsn/record_not_removed.json
@@ -0,0 +1,4 @@
+{
+    "error": "The specified resource record could not be removed.",
+    "debug": "The resource record does not exist."
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/dns/fixtures/nfsn/zone_not_found.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/nfsn/zone_not_found.json 
b/libcloud/test/dns/fixtures/nfsn/zone_not_found.json
new file mode 100644
index 0000000..fbb865c
--- /dev/null
+++ b/libcloud/test/dns/fixtures/nfsn/zone_not_found.json
@@ -0,0 +1,4 @@
+{
+    "error": "The API request was not valid.",
+    "debug": "The requested object instance does not exist or cannot be 
accessed."
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/e2c9f62d/libcloud/test/dns/test_nfsn.py
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/test_nfsn.py b/libcloud/test/dns/test_nfsn.py
new file mode 100644
index 0000000..0a07c2a
--- /dev/null
+++ b/libcloud/test/dns/test_nfsn.py
@@ -0,0 +1,148 @@
+# 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
+
+import sys
+import unittest
+
+from libcloud.utils.py3 import httplib
+
+from libcloud.dns.base import Record, Zone
+from libcloud.dns.drivers.nfsn import NFSNDNSDriver
+from libcloud.dns.types import RecordType, ZoneDoesNotExistError
+from libcloud.dns.types import RecordDoesNotExistError
+
+from libcloud.test import LibcloudTestCase, MockHttp
+from libcloud.test.file_fixtures import DNSFileFixtures
+
+
+class NFSNTestCase(LibcloudTestCase):
+
+    def setUp(self):
+        NFSNDNSDriver.connectionCls.conn_classes = (None, NFSNMockHttp)
+        NFSNMockHttp.type = None
+        self.driver = NFSNDNSDriver('testid', 'testsecret')
+
+        self.test_zone = Zone(id='example.com', domain='example.com',
+                              driver=self.driver, type='master', ttl=None,
+                              extra={})
+        self.test_record = Record(id=None, name='', data='192.0.2.1',
+                                  type=RecordType.A, zone=self.test_zone,
+                                  driver=self.driver, extra={})
+
+    def test_list_zones(self):
+        with self.assertRaises(NotImplementedError):
+            self.driver.list_zones()
+
+    def test_create_zone(self):
+        with self.assertRaises(NotImplementedError):
+            self.driver.create_zone('example.com')
+
+    def test_get_zone(self):
+        zone = self.driver.get_zone('example.com')
+        self.assertEquals(zone.id, None)
+        self.assertEquals(zone.domain, 'example.com')
+
+    def test_delete_zone(self):
+        with self.assertRaises(NotImplementedError):
+            self.driver.delete_zone(self.test_zone)
+
+    def test_create_record(self):
+        NFSNMockHttp.type = 'CREATED'
+        record = self.test_zone.create_record(name='newrecord',
+                                              type=RecordType.A,
+                                              data='127.0.0.1',
+                                              extra={'ttl': 900})
+        self.assertEquals(record.id, None)
+        self.assertEquals(record.name, 'newrecord')
+        self.assertEquals(record.data, '127.0.0.1')
+        self.assertEquals(record.type, RecordType.A)
+        self.assertEquals(record.ttl, 900)
+
+    def test_get_record(self):
+        with self.assertRaises(NotImplementedError):
+            self.driver.get_record('example.com', '12345')
+
+    def test_delete_record(self):
+        self.assertTrue(self.test_record.delete())
+
+    def test_list_records(self):
+        records = self.driver.list_records(self.test_zone)
+        self.assertEqual(len(records), 2)
+
+    def test_ex_get_records_by(self):
+        NFSNMockHttp.type = 'ONE_RECORD'
+        records = self.driver.ex_get_records_by(self.test_zone,
+                                                type=RecordType.A)
+        self.assertEqual(len(records), 1)
+        record = records[0]
+        self.assertEquals(record.name, '')
+        self.assertEquals(record.data, '192.0.2.1')
+        self.assertEquals(record.type, RecordType.A)
+        self.assertEquals(record.ttl, 3600)
+
+    def test_get_zone_not_found(self):
+        NFSNMockHttp.type = 'NOT_FOUND'
+        with self.assertRaises(ZoneDoesNotExistError):
+            self.driver.get_zone('example.com')
+
+    def test_delete_record_not_found(self):
+        NFSNMockHttp.type = 'NOT_FOUND'
+        with self.assertRaises(RecordDoesNotExistError):
+            self.assertTrue(self.test_record.delete())
+
+
+class NFSNMockHttp(MockHttp):
+    fixtures = DNSFileFixtures('nfsn')
+    base_headers = {'content-type': 'application/x-nfsn-api'}
+
+    def _dns_example_com_addRR_CREATED(self, method, url, body, headers):
+        return (httplib.OK, '', self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _dns_example_com_listRRs(self, method, url, body, headers):
+        body = self.fixtures.load('list_records.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _dns_example_com_listRRs_CREATED(self, method, url, body, headers):
+        body = self.fixtures.load('list_records_created.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _dns_example_com_removeRR(self, method, url, body, headers):
+        return (httplib.OK, '', self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _dns_example_com_serial(self, method, url, body, headers):
+        return (httplib.OK, '12345', self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _dns_example_com_listRRs_ONE_RECORD(self, method, url, body, headers):
+        body = self.fixtures.load('list_one_record.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _dns_example_com_serial_NOT_FOUND(self, method, url, body, headers):
+        body = self.fixtures.load('zone_not_found.json')
+        return (httplib.NOT_FOUND, body, self.base_headers,
+                httplib.responses[httplib.NOT_FOUND])
+
+    def _dns_example_com_removeRR_NOT_FOUND(self, method, url, body, headers):
+        body = self.fixtures.load('record_not_removed.json')
+        return (httplib.NOT_FOUND, body, self.base_headers,
+                httplib.responses[httplib.NOT_FOUND])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

Reply via email to