libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From anthonys...@apache.org
Subject libcloud git commit: add PowerDNS driver Closes #758
Date Mon, 18 Apr 2016 17:05:12 GMT
Repository: libcloud
Updated Branches:
  refs/heads/trunk de401aca8 -> 7034ea8c5


add PowerDNS driver
Closes #758


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

Branch: refs/heads/trunk
Commit: 7034ea8c5f91213ff502a9c2bbcd1e7d9aceb6d9
Parents: de401ac
Author: Ken Dreyer <ktdreyer@ktdreyer.com>
Authored: Tue Apr 12 16:23:21 2016 -0600
Committer: anthony-shaw <anthonyshaw@apache.org>
Committed: Tue Apr 19 03:04:54 2016 +1000

----------------------------------------------------------------------
 docs/dns/_supported_methods.rst                 |   2 +
 docs/dns/_supported_providers.rst               |   2 +
 docs/dns/drivers/powerdns.rst                   |  43 ++
 .../examples/dns/powerdns/instantiate_driver.py |  13 +
 libcloud/dns/drivers/powerdns.py                | 460 +++++++++++++++++++
 libcloud/dns/providers.py                       |   2 +
 libcloud/dns/types.py                           |  10 +
 .../dns/fixtures/powerdns/list_records.json     |  49 ++
 .../test/dns/fixtures/powerdns/list_zones.json  |  30 ++
 libcloud/test/dns/test_powerdns.py              | 190 ++++++++
 10 files changed, 801 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/docs/dns/_supported_methods.rst
----------------------------------------------------------------------
diff --git a/docs/dns/_supported_methods.rst b/docs/dns/_supported_methods.rst
index ce1f8f1..f812164 100644
--- a/docs/dns/_supported_methods.rst
+++ b/docs/dns/_supported_methods.rst
@@ -19,6 +19,7 @@ Provider            list zones list records create zone update zone create
recor
 `NFSN DNS`_         no         yes          no          no          yes           no    
       no          yes          
 `NS1 DNS`_          yes        yes          yes         no          yes           yes   
       yes         yes          
 `Point DNS`_        yes        yes          yes         yes         yes           yes   
       yes         yes          
+`PowerDNS`_         yes        yes          yes         no          yes           yes   
       yes         yes          
 `Rackspace DNS`_    yes        yes          yes         yes         yes           yes   
       yes         yes          
 `Route53 DNS`_      yes        yes          yes         no          yes           yes   
       yes         yes          
 `Softlayer DNS`_    yes        yes          yes         no          yes           yes   
       yes         yes          
@@ -44,6 +45,7 @@ Provider            list zones list records create zone update zone create
recor
 .. _`NFSN DNS`: https://www.nearlyfreespeech.net
 .. _`NS1 DNS`: https://ns1.com
 .. _`Point DNS`: https://pointhq.com/
+.. _`PowerDNS`: https://www.powerdns.com/
 .. _`Rackspace DNS`: http://www.rackspace.com/
 .. _`Route53 DNS`: http://aws.amazon.com/route53/
 .. _`Softlayer DNS`: https://www.softlayer.com

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/docs/dns/_supported_providers.rst
----------------------------------------------------------------------
diff --git a/docs/dns/_supported_providers.rst b/docs/dns/_supported_providers.rst
index 75232b2..2ff4308 100644
--- a/docs/dns/_supported_providers.rst
+++ b/docs/dns/_supported_providers.rst
@@ -19,6 +19,7 @@ Provider            Documentation                             Provider Constant
 `NFSN DNS`_         :doc:`Click </dns/drivers/nfsn>`          NFSN              single
region driver :mod:`libcloud.dns.drivers.nfsn`         :class:`NFSNDNSDriver`        
 `NS1 DNS`_                                                    NSONE             single region
driver :mod:`libcloud.dns.drivers.nsone`        :class:`NsOneDNSDriver`       
 `Point DNS`_        :doc:`Click </dns/drivers/pointdns>`      POINTDNS          single
region driver :mod:`libcloud.dns.drivers.pointdns`     :class:`PointDNSDriver`       
+`PowerDNS`_         :doc:`Click </dns/drivers/powerdns>`      POWERDNS          single
region driver :mod:`libcloud.dns.drivers.powerdns`     :class:`PowerDNSDriver`       
 `Rackspace DNS`_                                              RACKSPACE         us, uk  
            :mod:`libcloud.dns.drivers.rackspace`    :class:`RackspaceDNSDriver`   
 `Route53 DNS`_                                                ROUTE53           single region
driver :mod:`libcloud.dns.drivers.route53`      :class:`Route53DNSDriver`     
 `Softlayer DNS`_                                              SOFTLAYER         single region
driver :mod:`libcloud.dns.drivers.softlayer`    :class:`SoftLayerDNSDriver`   
@@ -44,6 +45,7 @@ Provider            Documentation                             Provider Constant
 .. _`NFSN DNS`: https://www.nearlyfreespeech.net
 .. _`NS1 DNS`: https://ns1.com
 .. _`Point DNS`: https://pointhq.com/
+.. _`PowerDNS`: https://www.powerdns.com/
 .. _`Rackspace DNS`: http://www.rackspace.com/
 .. _`Route53 DNS`: http://aws.amazon.com/route53/
 .. _`Softlayer DNS`: https://www.softlayer.com

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/docs/dns/drivers/powerdns.rst
----------------------------------------------------------------------
diff --git a/docs/dns/drivers/powerdns.rst b/docs/dns/drivers/powerdns.rst
new file mode 100644
index 0000000..0dd41a2
--- /dev/null
+++ b/docs/dns/drivers/powerdns.rst
@@ -0,0 +1,43 @@
+PowerDNS Driver Documentation
+=============================
+
+`PowerDNS`_ is an open-source DNS server.
+
+The current libcloud PowerDNS driver uses the HTTP API from PowerDNS 3.x by
+default. Please read the `PowerDNS 3 HTTP API documentation`_ to enable the
+HTTP API on your PowerDNS server. Specifically, you will need to set the
+following in ``pdns.conf``::
+
+  experimental-json-interface=yes
+  experimental-api-key=changeme
+  webserver=yes
+
+For PowerDNS 4.x, please read the `PowerDNS 4 HTTP API documentation`_. The
+``pdns.conf`` options are slightly different (the options are no longer
+prefixed with ``experimental-``)::
+
+  json-interface=yes
+  api-key=changeme
+  webserver=yes
+
+Be sure to reload the pdns service after any configuration changes.
+
+Instantiating the driver
+------------------------
+
+To instantiate the driver you need to pass the API key, hostname, and webserver
+HTTP port to the driver constructor as shown below.
+
+.. literalinclude:: /examples/dns/powerdns/instantiate_driver.py
+   :language: python
+
+API Docs
+--------
+
+.. autoclass:: libcloud.dns.drivers.powerdns.PowerDNSDriver
+    :members:
+    :inherited-members:
+
+.. _`PowerDNS`: https://doc.powerdns.com/
+.. _`PowerDNS 3 HTTP API documentation`: https://doc.powerdns.com/3/httpapi/README/
+.. _`PowerDNS 4 HTTP API documentation`: https://doc.powerdns.com/md/httpapi/README/

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/docs/examples/dns/powerdns/instantiate_driver.py
----------------------------------------------------------------------
diff --git a/docs/examples/dns/powerdns/instantiate_driver.py b/docs/examples/dns/powerdns/instantiate_driver.py
new file mode 100644
index 0000000..5baefa6
--- /dev/null
+++ b/docs/examples/dns/powerdns/instantiate_driver.py
@@ -0,0 +1,13 @@
+from libcloud.dns.types import Provider
+from libcloud.dns.providers import get_driver
+
+cls = get_driver(Provider.POWERDNS)
+
+# powerdns3.example.com is running PowerDNS v3.x.
+driver = cls(key='changeme', host='powerdns3.example.com', port=8081)
+
+# OR:
+
+# powerdns4.example.com is running PowerDNS v4.x, so it uses api_version v1.
+driver = cls(key='changeme', host='powerdns4.example.com', port=8081,
+             api_version='v1')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/libcloud/dns/drivers/powerdns.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/drivers/powerdns.py b/libcloud/dns/drivers/powerdns.py
new file mode 100644
index 0000000..56e2381
--- /dev/null
+++ b/libcloud/dns/drivers/powerdns.py
@@ -0,0 +1,460 @@
+# 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.
+"""
+PowerDNS Driver
+"""
+import json
+import sys
+
+from libcloud.common.base import ConnectionKey, JsonResponse
+from libcloud.common.exceptions import BaseHTTPError
+from libcloud.common.types import InvalidCredsError, MalformedResponseError
+from libcloud.dns.base import DNSDriver, Zone, Record
+from libcloud.dns.types import ZoneDoesNotExistError, ZoneAlreadyExistsError
+from libcloud.dns.types import Provider, RecordType
+from libcloud.utils.py3 import httplib
+
+__all__ = [
+    'PowerDNSDriver',
+]
+
+
+class PowerDNSResponse(JsonResponse):
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299
+
+    def parse_error(self):
+        if self.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError('Invalid provider credentials')
+
+        try:
+            body = self.parse_body()
+        except MalformedResponseError:
+            e = sys.exc_info()[1]
+            body = '%s: %s' % (e.value, e.body)
+        try:
+            errors = [body['error']]
+        except TypeError:
+            # parse_body() gave us a simple string, not a dict.
+            return '%s (HTTP Code: %d)' % (body, self.status)
+        try:
+            errors.append(body['errors'])
+        except KeyError:
+            # The PowerDNS API does not return the "errors" list all the time.
+            pass
+
+        return '%s (HTTP Code: %d)' % (' '.join(errors), self.status)
+
+
+class PowerDNSConnection(ConnectionKey):
+    responseCls = PowerDNSResponse
+
+    def add_default_headers(self, headers):
+        headers['X-API-Key'] = self.key
+        return headers
+
+
+class PowerDNSDriver(DNSDriver):
+    type = Provider.POWERDNS
+    name = 'PowerDNS'
+    website = 'https://www.powerdns.com/'
+    connectionCls = PowerDNSConnection
+
+    RECORD_TYPE_MAP = {
+        RecordType.A: 'A',
+        RecordType.AAAA: 'AAAA',
+        RecordType.AFSDB: 'AFSDB',
+        RecordType.CERT: 'CERT',
+        RecordType.CNAME: 'CNAME',
+        RecordType.DNSKEY: 'DNSKEY',
+        RecordType.DS: 'DS',
+        RecordType.HINFO: 'HINFO',
+        RecordType.KEY: 'KEY',
+        RecordType.LOC: 'LOC',
+        RecordType.MX: 'MX',
+        RecordType.NAPTR: 'NAPTR',
+        RecordType.NS: 'NS',
+        RecordType.NSEC: 'NSEC',
+        RecordType.OPENPGPKEY: 'OPENPGPKEY',
+        RecordType.PTR: 'PTR',
+        RecordType.RP: 'RP',
+        RecordType.RRSIG: 'RRSIG',
+        RecordType.SOA: 'SOA',
+        RecordType.SPF: 'SPF',
+        RecordType.SSHFP: 'SSHFP',
+        RecordType.SRV: 'SRV',
+        RecordType.TLSA: 'TLSA',
+        RecordType.TXT: 'TXT',
+    }
+
+    def __init__(self, key, secret=None, secure=False, host=None, port=None,
+                 api_version='experimental', **kwargs):
+        """
+        PowerDNS Driver defaulting to using PowerDNS 3.x API (ie
+        "experimental").
+
+        :param    key: API key or username to used (required)
+        :type     key: ``str``
+
+        :param    secure: Whether to use HTTPS or HTTP. Note: Off by default
+                          for PowerDNS.
+        :type     secure: ``bool``
+
+        :param    host: Hostname used for connections.
+        :type     host: ``str``
+
+        :param    port: Port used for connections.
+        :type     port: ``int``
+
+        :param    api_version: Specifies the API version to use.
+                               ``experimental`` and ``v1`` are the only valid
+                               options. Defaults to using ``experimental``
+                               (optional)
+        :type     api_version: ``str``
+
+        :return: ``None``
+        """
+        # libcloud doesn't really have a concept of "servers". We'll just use
+        # localhost for now.
+        self.ex_server = 'localhost'
+
+        if api_version == 'experimental':
+            # PowerDNS 3.x has no API root prefix.
+            self.api_root = ''
+        elif api_version == 'v1':
+            # PowerDNS 4.x has an '/api/v1' root prefix.
+            self.api_root = '/api/v1'
+        else:
+            raise NotImplementedError('Unsupported API version: %s' %
+                                      api_version)
+
+        return super(PowerDNSDriver, self).__init__(key=key, secure=secure,
+                                                    host=host, port=port,
+                                                    **kwargs)
+
+    def create_record(self, name, zone, type, data, extra=None):
+        """
+        Create a new record.
+
+        There are two PowerDNS-specific quirks here. Firstly, this method will
+        silently clobber any pre-existing records that might already exist. For
+        example, if PowerDNS already contains a "test.example.com" A record,
+        and you create that record using this function, then the old A record
+        will be replaced with your new one.
+
+        Secondly, PowerDNS requires that you provide a ttl for all new records.
+        In other words, the "extra" parameter must be ``{'ttl':
+        <some-integer>}`` at a minimum.
+
+        :param name: FQDN of the new record, for example "www.example.com".
+        :type  name: ``str``
+
+        :param zone: Zone where the requested record is created.
+        :type  zone: :class:`Zone`
+
+        :param type: DNS record type (A, AAAA, ...).
+        :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').
+                      Note that PowerDNS *requires* a ttl value for every
+                      record.
+        :type extra: ``dict``
+
+        :rtype: :class:`Record`
+        """
+        action = '%s/servers/%s/zones/%s' % (self.api_root, self.ex_server,
+                                             zone.id)
+        if extra is None or extra.get('ttl', None) is None:
+            raise ValueError('PowerDNS requires a ttl value for every record')
+        record = {
+            'content': data,
+            'disabled': False,
+            'name': name,
+            'ttl': extra['ttl'],
+            'type': type,
+        }
+        payload = {'rrsets': [{'name': name,
+                               'type': type,
+                               'changetype': 'REPLACE',
+                               'records': [record]
+                               }]
+                   }
+        try:
+            self.connection.request(action=action, data=json.dumps(payload),
+                                    method='PATCH')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.UNPROCESSABLE_ENTITY and \
+               e.message.startswith('Could not find domain'):
+                raise ZoneDoesNotExistError(zone_id=zone.id, driver=self,
+                                            value=e.message)
+            raise e
+        return Record(id=None, name=name, data=data,
+                      type=type, zone=zone, driver=self, ttl=extra['ttl'])
+
+    def create_zone(self, domain, type=None, ttl=None, extra={}):
+        """
+        Create a new zone.
+
+        There are two PowerDNS-specific quirks here. Firstly, the "type" and
+        "ttl" parameters are ignored (no-ops). The "type" parameter is simply
+        not implemented, and PowerDNS does not have an ability to set a
+        zone-wide default TTL. (TTLs must be set per-record.)
+
+        Secondly, PowerDNS requires that you provide a list of nameservers for
+        the zone upon creation.  In other words, the "extra" parameter must be
+        ``{'nameservers': ['ns1.example.org']}`` at a minimum.
+
+        :param name: Zone domain name (e.g. example.com)
+        :type  name: ``str``
+
+        :param domain: Zone type (master / slave). (optional).  Note that the
+                       PowerDNS driver does nothing with this parameter.
+        :type  domain: :class:`Zone`
+
+        :param ttl: TTL for new records. (optional). Note that the PowerDNS
+                    driver does nothing with this parameter.
+        :type  ttl: ``int``
+
+        :param extra: Extra attributes (driver specific).
+                      For example, specify
+                      ``extra={'nameservers': ['ns1.example.org']}`` to set
+                      a list of nameservers for this new zone.
+        :type extra: ``dict``
+
+        :rtype: :class:`Zone`
+        """
+        action = '%s/servers/%s/zones' % (self.api_root, self.ex_server)
+        if extra is None or extra.get('nameservers', None) is None:
+            msg = 'PowerDNS requires a list of nameservers for every new zone'
+            raise ValueError(msg)
+        payload = {'name': domain, 'kind': 'Native'}
+        payload.update(extra)
+        zone_id = domain + '.'
+        try:
+            self.connection.request(action=action, data=json.dumps(payload),
+                                    method='POST')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.UNPROCESSABLE_ENTITY and \
+               e.message.startswith("Domain '%s' already exists" % domain):
+                raise ZoneAlreadyExistsError(zone_id=zone_id, driver=self,
+                                             value=e.message)
+            raise e
+        return Zone(id=zone_id, domain=domain, type=None, ttl=None,
+                    driver=self, extra=extra)
+
+    def delete_record(self, record):
+        """
+        Use this method to delete a record.
+
+        :param record: record to delete
+        :type record: `Record`
+
+        :rtype: ``bool``
+        """
+        action = '%s/servers/%s/zones/%s' % (self.api_root, self.ex_server,
+                                             record.zone.id)
+        payload = {'rrsets': [{'name': record.name,
+                               'type': record.type,
+                               'changetype': 'DELETE',
+                               }]
+                   }
+        try:
+            self.connection.request(action=action, data=json.dumps(payload),
+                                    method='PATCH')
+        except BaseHTTPError:
+            # I'm not sure if we should raise a ZoneDoesNotExistError here. The
+            # base DNS API only specifies that we should return a bool. So,
+            # let's ignore this code for now.
+            # e = sys.exc_info()[1]
+            # if e.code == httplib.UNPROCESSABLE_ENTITY and \
+            #     e.message.startswith('Could not find domain'):
+            #     raise ZoneDoesNotExistError(zone_id=zone.id, driver=self,
+            #                                 value=e.message)
+            # raise e
+            return False
+        return True
+
+    def delete_zone(self, zone):
+        """
+        Use this method to delete a zone.
+
+        :param zone: zone to delete
+        :type zone: `Zone`
+
+        :rtype: ``bool``
+        """
+        action = '%s/servers/%s/zones/%s' % (self.api_root, self.ex_server,
+                                             zone.id)
+        try:
+            self.connection.request(action=action, method='DELETE')
+        except BaseHTTPError:
+            # I'm not sure if we should raise a ZoneDoesNotExistError here. The
+            # base DNS API only specifies that we should return a bool. So,
+            # let's ignore this code for now.
+            # e = sys.exc_info()[1]
+            # if e.code == httplib.UNPROCESSABLE_ENTITY and \
+            #     e.message.startswith('Could not find domain'):
+            #     raise ZoneDoesNotExistError(zone_id=zone.id, driver=self,
+            #                                 value=e.message)
+            # raise e
+            return False
+        return True
+
+    def get_zone(self, zone_id):
+        """
+        Return a Zone instance.
+
+        (Note that PowerDNS does not support per-zone TTL defaults, so all Zone
+        objects will have ``ttl=None``.)
+
+        :param zone_id: name of the required zone with the trailing period, for
+                        example "example.com.".
+        :type  zone_id: ``str``
+
+        :rtype: :class:`Zone`
+        :raises: ZoneDoesNotExistError: If no zone could be found.
+        """
+        action = '%s/servers/%s/zones/%s' % (self.api_root, self.ex_server,
+                                             zone_id)
+        try:
+            response = self.connection.request(action=action, method='GET')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.UNPROCESSABLE_ENTITY:
+                raise ZoneDoesNotExistError(zone_id=zone_id, driver=self,
+                                            value=e.message)
+            raise e
+        return self._to_zone(response.object)
+
+    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`
+        """
+        action = '%s/servers/%s/zones/%s' % (self.api_root, self.ex_server,
+                                             zone.id)
+        try:
+            response = self.connection.request(action=action, method='GET')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.UNPROCESSABLE_ENTITY and \
+               e.message.startswith('Could not find domain'):
+                raise ZoneDoesNotExistError(zone_id=zone.id, driver=self,
+                                            value=e.message)
+            raise e
+        return self._to_records(response, zone)
+
+    def list_zones(self):
+        """
+        Return a list of zones.
+
+        :return: ``list`` of :class:`Zone`
+        """
+        action = '%s/servers/%s/zones' % (self.api_root, self.ex_server)
+        response = self.connection.request(action=action, method='GET')
+        return self._to_zones(response)
+
+    def update_record(self, record, name, type, data, extra=None):
+        """
+        Update an existing record.
+
+        :param record: Record to update.
+        :type  record: :class:`Record`
+
+        :param name: FQDN of the new record, for example "www.example.com".
+        :type  name: ``str``
+
+        :param type: DNS record type (A, AAAA, ...).
+        :type  type: :class:`RecordType`
+
+        :param data: Data for the record (depends on the record type).
+        :type  data: ``str``
+
+        :param extra: (optional) Extra attributes (driver specific).
+        :type  extra: ``dict``
+
+        :rtype: :class:`Record`
+        """
+        action = '%s/servers/%s/zones/%s' % (self.api_root, self.ex_server,
+                                             record.zone.id)
+        if extra is None or extra.get('ttl', None) is None:
+            raise ValueError('PowerDNS requires a ttl value for every record')
+        updated_record = {
+            'content': data,
+            'disabled': False,
+            'name': name,
+            'ttl': extra['ttl'],
+            'type': type,
+        }
+        payload = {'rrsets': [{'name': record.name,
+                               'type': record.type,
+                               'changetype': 'DELETE',
+                               },
+                              {'name': name,
+                               'type': type,
+                               'changetype': 'REPLACE',
+                               'records': [updated_record]
+                               }]
+                   }
+        try:
+            self.connection.request(action=action, data=json.dumps(payload),
+                                    method='PATCH')
+        except BaseHTTPError:
+            e = sys.exc_info()[1]
+            if e.code == httplib.UNPROCESSABLE_ENTITY and \
+               e.message.startswith('Could not find domain'):
+                raise ZoneDoesNotExistError(zone_id=record.zone.id,
+                                            driver=self, value=e.message)
+            raise e
+        return Record(id=None, name=name, data=data, type=type,
+                      zone=record.zone, driver=self, ttl=extra['ttl'])
+
+    def _to_zone(self, item):
+        extra = {}
+        for e in ['kind', 'dnssec', 'account', 'masters', 'serial',
+                  'notified_serial', 'last_check']:
+            extra[e] = item[e]
+        # XXX: we have to hard-code "ttl" to "None" here because PowerDNS does
+        # not support per-zone ttl defaults. However, I don't know what "type"
+        # should be; probably not None.
+        return Zone(id=item['id'], domain=item['name'], type=None,
+                    ttl=None, driver=self, extra=extra)
+
+    def _to_zones(self, items):
+        zones = []
+        for item in items.object:
+            zones.append(self._to_zone(item))
+        return zones
+
+    def _to_record(self, item, zone):
+        return Record(id=None, name=item['name'], data=item['content'],
+                      type=item['type'], zone=zone, driver=self,
+                      ttl=item['ttl'])
+
+    def _to_records(self, items, zone):
+        records = []
+        for item in items.object['records']:
+            records.append(self._to_record(item, zone))
+        return records

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/libcloud/dns/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py
index ad55385..38aa39f 100644
--- a/libcloud/dns/providers.py
+++ b/libcloud/dns/providers.py
@@ -73,6 +73,8 @@ DRIVERS = {
     ('libcloud.dns.drivers.luadns', 'LuadnsDNSDriver'),
     Provider.BUDDYNS:
     ('libcloud.dns.drivers.buddyns', 'BuddyNSDNSDriver'),
+    Provider.POWERDNS:
+    ('libcloud.dns.drivers.powerdns', 'PowerDNSDriver'),
 
     # Deprecated
     Provider.RACKSPACE_US:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/libcloud/dns/types.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py
index d1fac12..e6fc28d 100644
--- a/libcloud/dns/types.py
+++ b/libcloud/dns/types.py
@@ -53,6 +53,7 @@ class Provider(object):
     NFSN = 'nfsn'
     NSONE = 'nsone'
     POINTDNS = 'pointdns'
+    POWERDNS = 'powerdns'
     RACKSPACE = 'rackspace'
     ROUTE53 = 'route53'
     SOFTLAYER = 'softlayer'
@@ -77,22 +78,31 @@ class RecordType(object):
     """
     A = 'A'
     AAAA = 'AAAA'
+    AFSDB = 'A'
     ALIAS = 'ALIAS'
+    CERT = 'CERT'
     CNAME = 'CNAME'
     DNAME = 'DNAME'
+    DNSKEY = 'DNSKEY'
+    DS = 'DS'
     GEO = 'GEO'
     HINFO = 'HINFO'
+    KEY = 'KEY'
     LOC = 'LOC'
     MX = 'MX'
     NAPTR = 'NAPTR'
     NS = 'NS'
+    NSEC = 'NSEC'
+    OPENPGPKEY = 'OPENPGPKEY'
     PTR = 'PTR'
     REDIRECT = 'REDIRECT'
     RP = 'RP'
+    RRSIG = 'RRSIG'
     SOA = 'SOA'
     SPF = 'SPF'
     SRV = 'SRV'
     SSHFP = 'SSHFP'
+    TLSA = 'TLSA'
     TXT = 'TXT'
     URL = 'URL'
     WKS = 'WKS'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/libcloud/test/dns/fixtures/powerdns/list_records.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/powerdns/list_records.json b/libcloud/test/dns/fixtures/powerdns/list_records.json
new file mode 100644
index 0000000..99ea2e0
--- /dev/null
+++ b/libcloud/test/dns/fixtures/powerdns/list_records.json
@@ -0,0 +1,49 @@
+{
+   "id":"example.com.",
+   "url":"/servers/localhost/zones/example.com.",
+   "name":"example.com",
+   "kind":"Native",
+   "dnssec":false,
+   "account":"",
+   "masters":[
+
+   ],
+   "serial":2016041501,
+   "notified_serial":0,
+   "last_check":0,
+   "soa_edit_api":"",
+   "soa_edit":"",
+   "records":[
+      {
+         "name":"example.com",
+         "type":"NS",
+         "ttl":3600,
+         "disabled":false,
+         "content":"ns1.example.com"
+      },
+      {
+         "name":"example.com",
+         "type":"SOA",
+         "ttl":3600,
+         "disabled":false,
+         "content":"a.misconfigured.powerdns.server hostmaster.example.com 2016041501 10800
3600 604800 3600"
+      },
+      {
+         "name":"www.example.com",
+         "type":"A",
+         "ttl":86400,
+         "disabled":false,
+         "content":"192.0.5.1"
+      },
+      {
+         "name":"example.com",
+         "type":"A",
+         "ttl":300,
+         "disabled":false,
+         "content":"192.0.5.1"
+      }
+   ],
+   "comments":[
+
+   ]
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/libcloud/test/dns/fixtures/powerdns/list_zones.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/powerdns/list_zones.json b/libcloud/test/dns/fixtures/powerdns/list_zones.json
new file mode 100644
index 0000000..9db222b
--- /dev/null
+++ b/libcloud/test/dns/fixtures/powerdns/list_zones.json
@@ -0,0 +1,30 @@
+[
+   {
+      "id":"example.com.",
+      "url":"/servers/localhost/zones/example.com.",
+      "name":"example.com",
+      "kind":"Native",
+      "dnssec":false,
+      "account":"",
+      "masters":[
+
+      ],
+      "serial":1,
+      "notified_serial":0,
+      "last_check":0
+   },
+   {
+      "id":"example.net.",
+      "url":"/servers/localhost/zones/example.net.",
+      "name":"example.net",
+      "kind":"Native",
+      "dnssec":false,
+      "account":"",
+      "masters":[
+
+      ],
+      "serial":2016041501,
+      "notified_serial":0,
+      "last_check":0
+   }
+]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7034ea8c/libcloud/test/dns/test_powerdns.py
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/test_powerdns.py b/libcloud/test/dns/test_powerdns.py
new file mode 100644
index 0000000..744676f
--- /dev/null
+++ b/libcloud/test/dns/test_powerdns.py
@@ -0,0 +1,190 @@
+# 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
+import json
+
+from libcloud.utils.py3 import httplib
+
+from libcloud.dns.base import Record, Zone
+from libcloud.dns.drivers.powerdns import PowerDNSDriver
+from libcloud.dns.types import ZoneDoesNotExistError, ZoneAlreadyExistsError
+from libcloud.dns.types import RecordType
+
+from libcloud.test import LibcloudTestCase, MockHttp
+from libcloud.test.file_fixtures import DNSFileFixtures
+
+
+class PowerDNSTestCase(LibcloudTestCase):
+
+    def setUp(self):
+        PowerDNSDriver.connectionCls.conn_classes = (PowerDNSMockHttp,
+                                                     PowerDNSMockHttp)
+        PowerDNSMockHttp.type = None
+        self.driver = PowerDNSDriver('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_create_record(self):
+        record = self.test_zone.create_record(name='newrecord.example.com',
+                                              type=RecordType.A,
+                                              data='192.0.5.4',
+                                              extra={'ttl': 86400})
+        self.assertEqual(record.id, None)
+        self.assertEqual(record.name, 'newrecord.example.com')
+        self.assertEqual(record.data, '192.0.5.4')
+        self.assertEqual(record.type, RecordType.A)
+        self.assertEqual(record.ttl, 86400)
+
+    def test_create_zone(self):
+        extra = {'nameservers': ['ns1.example.org', 'ns2.example.org']}
+        zone = self.driver.create_zone('example.org', extra=extra)
+        self.assertEqual(zone.id, 'example.org.')
+        self.assertEqual(zone.domain, 'example.org')
+        self.assertEqual(zone.type, None)
+        self.assertEqual(zone.ttl, None)
+
+    def test_delete_record(self):
+        self.assertTrue(self.test_record.delete())
+
+    def test_delete_zone(self):
+        self.assertTrue(self.test_zone.delete())
+
+    def test_get_record(self):
+        with self.assertRaises(NotImplementedError):
+            self.driver.get_record('example.com.', '12345')
+
+    def test_get_zone(self):
+        zone = self.driver.get_zone('example.com.')
+        self.assertEqual(zone.id, 'example.com.')
+        self.assertEqual(zone.domain, 'example.com')
+        self.assertEqual(zone.type, None)
+        self.assertEqual(zone.ttl, None)
+
+    def test_list_record_types(self):
+        result = self.driver.list_record_types()
+        self.assertEqual(len(result), 23)
+
+    def test_list_records(self):
+        records = self.driver.list_records(self.test_zone)
+        self.assertEqual(len(records), 4)
+
+    def test_list_zones(self):
+        zones = self.driver.list_zones()
+        self.assertEqual(zones[0].id, 'example.com.')
+        self.assertEqual(zones[0].domain, 'example.com')
+        self.assertEqual(zones[0].type, None)
+        self.assertEqual(zones[0].ttl, None)
+        self.assertEqual(zones[1].id, 'example.net.')
+        self.assertEqual(zones[1].domain, 'example.net')
+        self.assertEqual(zones[1].type, None)
+        self.assertEqual(zones[1].ttl, None)
+
+    def test_update_record(self):
+        record = self.driver.update_record(self.test_record,
+                                           name='newrecord.example.com',
+                                           type=RecordType.A,
+                                           data='127.0.0.1',
+                                           extra={'ttl': 300})
+        self.assertEqual(record.id, None)
+        self.assertEqual(record.name, 'newrecord.example.com')
+        self.assertEqual(record.data, '127.0.0.1')
+        self.assertEqual(record.type, RecordType.A)
+        self.assertEqual(record.ttl, 300)
+
+    def test_update_zone(self):
+        with self.assertRaises(NotImplementedError):
+            self.driver.update_zone(self.test_zone, 'example.net')
+
+    # Test some error conditions
+
+    def test_create_existing_zone(self):
+        PowerDNSMockHttp.type = 'EXISTS'
+        extra = {'nameservers': ['ns1.example.com', 'ns2.example.com']}
+        with self.assertRaises(ZoneAlreadyExistsError):
+            self.driver.create_zone('example.com', extra=extra)
+
+    def test_get_missing_zone(self):
+        PowerDNSMockHttp.type = 'MISSING'
+        with self.assertRaises(ZoneDoesNotExistError):
+            self.driver.get_zone('example.com.')
+
+    def test_delete_missing_record(self):
+        PowerDNSMockHttp.type = 'MISSING'
+        self.assertFalse(self.test_record.delete())
+
+    def test_delete_missing_zone(self):
+        PowerDNSMockHttp.type = 'MISSING'
+        self.assertFalse(self.test_zone.delete())
+
+
+class PowerDNSMockHttp(MockHttp):
+    fixtures = DNSFileFixtures('powerdns')
+    base_headers = {'content-type': 'application/json'}
+
+    def _servers_localhost_zones(self, method, url, body, headers):
+        if method == 'GET':
+            # list_zones()
+            body = self.fixtures.load('list_zones.json')
+        elif method == 'POST':
+            # create_zone()
+            # Don't bother with a fixture for this operation, because we do
+            # nothing with the parsed body anyway.
+            body = ''
+        else:
+            raise NotImplementedError('Unexpected method: %s' % method)
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _servers_localhost_zones_example_com_(self, method, *args, **kwargs):
+        if method == 'GET':
+            # list_records()
+            body = self.fixtures.load('list_records.json')
+        elif method == 'PATCH':
+            # create/update/delete_record()
+            # Don't bother with a fixture for these operations, because we do
+            # nothing with the parsed body anyway.
+            body = ''
+        elif method == 'DELETE':
+            # delete_zone()
+            return (httplib.NO_CONTENT, '', self.base_headers,
+                    httplib.responses[httplib.NO_CONTENT])
+        else:
+            raise NotImplementedError('Unexpected method: %s' % method)
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _servers_localhost_zones_EXISTS(self, method, url, body, headers):
+        # create_zone() is a POST. Raise on all other operations to be safe.
+        if method != 'POST':
+            raise NotImplementedError('Unexpected method: %s' % method)
+        payload = json.loads(body)
+        domain = payload['name']
+        body = json.dumps({'error': "Domain '%s' already exists" % domain})
+        return (httplib.UNPROCESSABLE_ENTITY, body, self.base_headers,
+                'Unprocessable Entity')
+
+    def _servers_localhost_zones_example_com__MISSING(self, *args, **kwargs):
+        return (httplib.UNPROCESSABLE_ENTITY, 'Could not find domain',
+                self.base_headers, 'Unprocessable Entity')
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())


Mime
View raw message