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())