You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by an...@apache.org on 2015/12/04 21:18:49 UTC

libcloud git commit: Add Rackspace RDNS support

Repository: libcloud
Updated Branches:
  refs/heads/trunk 9907c30b1 -> 994f7a411


Add Rackspace RDNS support

New DNS driver methods:
  * ex_iterate_ptr_records
  * ex_get_ptr_record
  * ex_create_ptr_record
  * ex_update_ptr_record
  * ex_delete_ptr_record

This should cover all of the functionality offered by the Rackspace
DNS API in regards to RDNS.
Closes #652
Signed-off-by: Anthony Shaw <an...@gmail.com>


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

Branch: refs/heads/trunk
Commit: 994f7a411d6c278f04acc6f94ab9377a88c235f8
Parents: 9907c30
Author: Greg Hill <gr...@rackspace.com>
Authored: Thu Dec 3 10:20:06 2015 -0600
Committer: Anthony Shaw <an...@gmail.com>
Committed: Sat Dec 5 07:18:53 2015 +1100

----------------------------------------------------------------------
 libcloud/dns/drivers/rackspace.py               | 226 ++++++++++++++++++-
 .../rackspace/create_ptr_record_success.json    |  21 ++
 .../rackspace/delete_ptr_record_success.json    |   8 +
 .../rackspace/list_ptr_records_success.json     |  14 ++
 libcloud/test/dns/test_rackspace.py             | 119 +++++++++-
 5 files changed, 386 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/994f7a41/libcloud/dns/drivers/rackspace.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/drivers/rackspace.py b/libcloud/dns/drivers/rackspace.py
index a5393f4..1e05206 100644
--- a/libcloud/dns/drivers/rackspace.py
+++ b/libcloud/dns/drivers/rackspace.py
@@ -23,6 +23,7 @@ from libcloud.utils.py3 import httplib
 import copy
 
 from libcloud.common.base import PollingConnection
+from libcloud.common.exceptions import BaseHTTPError
 from libcloud.common.types import LibcloudError
 from libcloud.utils.misc import merge_valid_keys, get_new_obj
 from libcloud.common.rackspace import AUTH_URL
@@ -34,7 +35,8 @@ from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError
 from libcloud.dns.base import DNSDriver, Zone, Record
 
 VALID_ZONE_EXTRA_PARAMS = ['email', 'comment', 'ns1']
-VALID_RECORD_EXTRA_PARAMS = ['ttl', 'comment', 'priority']
+VALID_RECORD_EXTRA_PARAMS = ['ttl', 'comment', 'priority', 'created',
+                             'updated']
 
 
 class RackspaceDNSResponse(OpenStack_1_1_Response):
@@ -131,6 +133,28 @@ class RackspaceDNSConnection(OpenStack_1_1_Connection, PollingConnection):
         return public_url
 
 
+class RackspacePTRRecord(object):
+    def __init__(self, id, ip, domain, driver, extra=None):
+        self.id = str(id) if id else None
+        self.ip = ip
+        self.type = RecordType.PTR
+        self.domain = domain
+        self.driver = driver
+        self.extra = extra or {}
+
+    def update(self, domain, extra=None):
+        return self.driver.ex_update_ptr_record(record=self, domain=domain,
+                                                extra=extra)
+
+    def delete(self):
+        return self.driver.ex_delete_ptr_record(record=self)
+
+    def __repr__(self):
+        return ('<%s: ip=%s, domain=%s, provider=%s ...>' %
+                (self.__class__.__name__, self.ip,
+                 self.domain, self.driver.name))
+
+
 class RackspaceDNSDriver(DNSDriver, OpenStackDriverMixin):
     name = 'Rackspace DNS'
     website = 'http://www.rackspace.com/'
@@ -343,6 +367,183 @@ class RackspaceDNSDriver(DNSDriver, OpenStackDriverMixin):
                                       method='DELETE')
         return True
 
+    def ex_iterate_ptr_records(self, device):
+        """
+        Return a generator to iterate over existing PTR Records.
+
+        The ``device`` should be an instance of one of these:
+            :class:`libcloud.compute.base.Node`
+            :class:`libcloud.loadbalancer.base.LoadBalancer`
+
+        And it needs to have the following ``extra`` fields set:
+            service_name - the service catalog name for the device
+            uri - the URI pointing to the GET endpoint for the device
+
+        Those are automatically set for you if you got the device from
+        the Rackspace driver for that service.
+
+        For example:
+            server = rs_compute.ex_get_node_details(id)
+            ptr_iter = rs_dns.ex_list_ptr_records(server)
+
+            loadbalancer = rs_lbs.get_balancer(id)
+            ptr_iter = rs_dns.ex_list_ptr_records(loadbalancer)
+
+        Note: the Rackspace DNS API docs indicate that the device 'href' is
+        optional, but testing does not bear this out. It throws a
+        400 Bad Request error if you do not pass in the 'href' from
+        the server or loadbalancer.  So ``device`` is required.
+
+        :param device: the device that owns the IP
+        :rtype: ``generator`` of :class:`RackspacePTRRecord`
+        """
+        _check_ptr_extra_fields(device)
+        params = {'href': device.extra['uri']}
+
+        service_name = device.extra['service_name']
+
+        # without a valid context, the 404 on empty list will blow up
+        # in the error-handling code
+        self.connection.set_context({'resource': 'ptr_records'})
+        try:
+            response = self.connection.request(
+                action='/rdns/%s' % (service_name), params=params).object
+            records = response['records']
+            link = dict(rel=service_name, **params)
+            for item in records:
+                record = self._to_ptr_record(data=item, link=link)
+                yield record
+        except BaseHTTPError as exc:
+            # 404 just means empty list
+            if exc.code == 404:
+                return
+            raise
+
+    def ex_get_ptr_record(self, service_name, record_id):
+        """
+        Get a specific PTR record by id.
+
+        :param service_name: the service catalog name of the linked device(s)
+                             i.e. cloudLoadBalancers or cloudServersOpenStack
+        :param record_id: the id (i.e. PTR-12345) of the PTR record
+        :rtype: instance of :class:`RackspacePTRRecord`
+        """
+        self.connection.set_context({'resource': 'record', 'id': record_id})
+        response = self.connection.request(
+            action='/rdns/%s/%s' % (service_name, record_id)).object
+        item = next(iter(response['recordsList']['records']))
+        return self._to_ptr_record(data=item, link=response['link'])
+
+    def ex_create_ptr_record(self, device, ip, domain, extra=None):
+        """
+        Create a PTR record for a specific IP on a specific device.
+
+        The ``device`` should be an instance of one of these:
+            :class:`libcloud.compute.base.Node`
+            :class:`libcloud.loadbalancer.base.LoadBalancer`
+
+        And it needs to have the following ``extra`` fields set:
+            service_name - the service catalog name for the device
+            uri - the URI pointing to the GET endpoint for the device
+
+        Those are automatically set for you if you got the device from
+        the Rackspace driver for that service.
+
+        For example:
+            server = rs_compute.ex_get_node_details(id)
+            rs_dns.create_ptr_record(server, ip, domain)
+
+            loadbalancer = rs_lbs.get_balancer(id)
+            rs_dns.create_ptr_record(loadbalancer, ip, domain)
+
+        :param device: the device that owns the IP
+        :param ip: the IP for which you want to set reverse DNS
+        :param domain: the fqdn you want that IP to represent
+        :param extra: a ``dict`` with optional extra values:
+            ttl - the time-to-live of the PTR record
+        :rtype: instance of :class:`RackspacePTRRecord`
+        """
+        _check_ptr_extra_fields(device)
+
+        if extra is None:
+            extra = {}
+
+        # the RDNS API reverse the name and data fields for PTRs
+        # the record name *should* be the ip and the data the fqdn
+        data = {
+            "name": domain,
+            "type": RecordType.PTR,
+            "data": ip
+        }
+
+        if 'ttl' in extra:
+            data['ttl'] = extra['ttl']
+
+        payload = {
+            "recordsList": {
+                "records": [data]
+            },
+            "link": {
+                "content": "",
+                "href": device.extra['uri'],
+                "rel": device.extra['service_name'],
+            }
+        }
+        response = self.connection.async_request(
+            action='/rdns', method='POST', data=payload).object
+        item = next(iter(response['response']['records']))
+        return self._to_ptr_record(data=item, link=payload['link'])
+
+    def ex_update_ptr_record(self, record, domain=None, extra=None):
+        """
+        Update a PTR record for a specific IP on a specific device.
+
+        If you need to change the domain or ttl, use this API to
+        update the record by deleting the old one and creating a new one.
+
+        :param record: the original :class:`RackspacePTRRecord`
+        :param domain: the fqdn you want that IP to represent
+        :param extra: a ``dict`` with optional extra values:
+            ttl - the time-to-live of the PTR record
+        :rtype: instance of :class:`RackspacePTRRecord`
+        """
+        if domain is not None and domain == record.domain:
+            domain = None
+
+        if extra is not None:
+            extra = dict(extra)
+            for key in extra:
+                if key in record.extra and record.extra[key] == extra[key]:
+                    del extra[key]
+
+        if domain is None and not extra:
+            # nothing to do, it already matches
+            return record
+
+        _check_ptr_extra_fields(record)
+        ip = record.ip
+
+        self.ex_delete_ptr_record(record)
+        # records have the same metadata in 'extra' as the original device
+        # so you can pass the original record object in instead
+        return self.ex_create_ptr_record(record, ip, domain, extra=extra)
+
+    def ex_delete_ptr_record(self, record):
+        """
+        Delete an existing PTR Record
+
+        :param record: the original :class:`RackspacePTRRecord`
+        :rtype: ``bool``
+        """
+        _check_ptr_extra_fields(record)
+        self.connection.set_context({'resource': 'record', 'id': record.id})
+        self.connection.async_request(
+            action='/rdns/%s' % (record.extra['service_name']),
+            method='DELETE',
+            params={'href': record.extra['uri'], 'ip': record.ip},
+        )
+        return True
+
     def _to_zone(self, data):
         id = data['id']
         domain = data['name']
@@ -377,6 +578,20 @@ class RackspaceDNSDriver(DNSDriver, OpenStackDriverMixin):
                         extra=extra)
         return record
 
+    def _to_ptr_record(self, data, link):
+        id = data['id']
+        ip = data['data']
+        domain = data['name']
+        extra = {'uri': link['href'], 'service_name': link['rel']}
+
+        for key in VALID_RECORD_EXTRA_PARAMS:
+            if key in data:
+                extra[key] = data[key]
+
+        record = RackspacePTRRecord(id=str(id), ip=ip, domain=domain,
+                                    driver=self, extra=extra)
+        return record
+
     def _to_full_record_name(self, domain, name):
         """
         Build a FQDN from a domain and record name.
@@ -449,3 +664,12 @@ def _rackspace_result_has_more(response, result_length, limit):
         if item['rel'] == 'next':
             return True
     return False
+
+
+def _check_ptr_extra_fields(device_or_record):
+    if not (getattr(device_or_record, 'extra') and
+            device_or_record.extra.get('uri') is not None and
+            device_or_record.extra.get('service_name') is not None):
+        raise LibcloudError("Can't create PTR Record for %s because it "
+                            "doesn't have a 'uri' and 'service_name' in "
+                            "'extra'")

http://git-wip-us.apache.org/repos/asf/libcloud/blob/994f7a41/libcloud/test/dns/fixtures/rackspace/create_ptr_record_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/rackspace/create_ptr_record_success.json b/libcloud/test/dns/fixtures/rackspace/create_ptr_record_success.json
new file mode 100644
index 0000000..8ab0104
--- /dev/null
+++ b/libcloud/test/dns/fixtures/rackspace/create_ptr_record_success.json
@@ -0,0 +1,21 @@
+{
+   "request":"{\"recordsList\": {\"records\": [{\"data\": \"127.1.1.1\", \"type\": \"PTR\", \"name\": \"www.foo4.bar.com\"}]}, \"link\": {\"content\": \"\", \"href\": \"https://ord.servers.api.rackspacecloud.com/v2/905546514/servers/370b0ff8-3f57-4e10-ac84-e9145ce00584\", \"rel\": \"cloudServersOpenStack\"}}",
+   "response":{
+      "records":[
+         {
+            "name":"www.foo4.bar.com",
+            "id":"PTR-7423317",
+            "type":"PTR",
+            "data":"127.1.1.1",
+            "updated":"2011-10-29T20:50:41.000+0000",
+            "ttl":3600,
+            "created":"2011-10-29T20:50:41.000+0000"
+         }
+      ]
+   },
+   "status":"COMPLETED",
+   "verb":"POST",
+   "jobId":"12345678-5739-43fb-8939-f3a2c4c0e99c",
+   "callbackUrl":"https://dns.api.rackspacecloud.com/v1.0/546514/status/12345678-5739-43fb-8939-f3a2c4c0e99c",
+   "requestUrl":"http://dns.api.rackspacecloud.com/v1.0/546514/rdns"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/994f7a41/libcloud/test/dns/fixtures/rackspace/delete_ptr_record_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/rackspace/delete_ptr_record_success.json b/libcloud/test/dns/fixtures/rackspace/delete_ptr_record_success.json
new file mode 100644
index 0000000..feea1d2
--- /dev/null
+++ b/libcloud/test/dns/fixtures/rackspace/delete_ptr_record_success.json
@@ -0,0 +1,8 @@
+{
+   "status":"COMPLETED",
+   "verb":"DELETE",
+   "jobId":"12345678-2e5d-490f-bb6e-fdc65d1118a9",
+   "callbackUrl":"https://dns.api.rackspacecloud.com/v1.0/11111/status/12345678-2e5d-490f-bb6e-fdc65d1118a9",
+   "requestUrl":"http://dns.api.rackspacecloud.com/v1.0/11111/rdns/cloudServersOpenStack"
+}
+

http://git-wip-us.apache.org/repos/asf/libcloud/blob/994f7a41/libcloud/test/dns/fixtures/rackspace/list_ptr_records_success.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/rackspace/list_ptr_records_success.json b/libcloud/test/dns/fixtures/rackspace/list_ptr_records_success.json
new file mode 100644
index 0000000..cadd1dc
--- /dev/null
+++ b/libcloud/test/dns/fixtures/rackspace/list_ptr_records_success.json
@@ -0,0 +1,14 @@
+{
+   "records":[
+      {
+         "name":"test3.foo4.bar.com",
+         "id":"PTR-7423034",
+         "type":"PTR",
+         "comment":"lulz",
+         "data":"127.7.7.7",
+         "updated":"2011-10-29T18:42:28.000+0000",
+         "ttl":777,
+         "created":"2011-10-29T15:29:29.000+0000"
+      }
+   ]
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/994f7a41/libcloud/test/dns/test_rackspace.py
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/test_rackspace.py b/libcloud/test/dns/test_rackspace.py
index 907fa8d..aa8ad10 100644
--- a/libcloud/test/dns/test_rackspace.py
+++ b/libcloud/test/dns/test_rackspace.py
@@ -18,15 +18,33 @@ import unittest
 from libcloud.utils.py3 import httplib
 
 from libcloud.common.types import LibcloudError
+from libcloud.compute.base import Node
 from libcloud.dns.types import RecordType, ZoneDoesNotExistError
 from libcloud.dns.types import RecordDoesNotExistError
+from libcloud.dns.drivers.rackspace import RackspacePTRRecord
 from libcloud.dns.drivers.rackspace import RackspaceUSDNSDriver
 from libcloud.dns.drivers.rackspace import RackspaceUKDNSDriver
+from libcloud.loadbalancer.base import LoadBalancer
 
 from libcloud.test import MockHttp
 from libcloud.test.file_fixtures import DNSFileFixtures
 from libcloud.test.secrets import DNS_PARAMS_RACKSPACE
 
+# only the 'extra' will be looked at, so pass in minimal data
+RDNS_NODE = Node('370b0ff8-3f57-4e10-ac84-e9145ce005841', 'server1',
+                 None, [], [], None,
+                 extra={'uri': 'https://ord.servers.api.rackspacecloud'
+                               '.com/v2/905546514/servers/370b0ff8-3f57'
+                               '-4e10-ac84-e9145ce00584',
+                        'service_name': 'cloudServersOpenStack'})
+RDNS_LB = LoadBalancer('370b0ff8-3f57-4e10-ac84-e9145ce005841', 'server1',
+                       None, None, None, None,
+                       extra={'uri': 'https://ord.loadbalancers.api.'
+                                     'rackspacecloud.com/v2/905546514/'
+                                     'loadbalancers/370b0ff8-3f57-4e10-'
+                                     'ac84-e9145ce00584',
+                              'service_name': 'cloudLoadbalancers'})
+
 
 class RackspaceUSTests(unittest.TestCase):
     klass = RackspaceUSDNSDriver
@@ -320,6 +338,70 @@ class RackspaceUSTests(unittest.TestCase):
                                                         name=name)
             self.assertEqual(value, expected_value)
 
+    def test_ex_create_ptr_success(self):
+        ip = '127.1.1.1'
+        domain = 'www.foo4.bar.com'
+        record = self.driver.ex_create_ptr_record(RDNS_NODE, ip, domain)
+        self.assertEqual(record.ip, ip)
+        self.assertEqual(record.domain, domain)
+        self.assertEqual(record.extra['uri'], RDNS_NODE.extra['uri'])
+        self.assertEqual(record.extra['service_name'],
+                         RDNS_NODE.extra['service_name'])
+
+        self.driver.ex_create_ptr_record(RDNS_LB, ip, domain)
+
+    def test_ex_list_ptr_success(self):
+        records = self.driver.ex_iterate_ptr_records(RDNS_NODE)
+        for record in records:
+            self.assertTrue(isinstance(record, RackspacePTRRecord))
+            self.assertEqual(record.type, RecordType.PTR)
+            self.assertEqual(record.extra['uri'], RDNS_NODE.extra['uri'])
+            self.assertEqual(record.extra['service_name'],
+                             RDNS_NODE.extra['service_name'])
+
+    def test_ex_list_ptr_not_found(self):
+        RackspaceMockHttp.type = 'RECORD_DOES_NOT_EXIST'
+
+        try:
+            records = self.driver.ex_iterate_ptr_records(RDNS_NODE)
+        except Exception as exc:
+            self.fail("PTR Records list 404 threw %s" % exc)
+
+        try:
+            next(records)
+            self.fail("PTR Records list 404 did not produce an empty list")
+        except StopIteration:
+            self.assertTrue(True, "Got empty list on 404")
+
+    def text_ex_get_ptr_success(self):
+        service_name = 'cloudServersOpenStack'
+        records = self.driver.ex_iterate_ptr_records(service_name)
+        original = next(records)
+        found = self.driver.ex_get_ptr_record(service_name, original.id)
+        for attr in dir(original):
+            self.assertEqual(getattr(found, attr), getattr(original, attr))
+
+    def text_update_ptr_success(self):
+        records = self.driver.ex_iterate_ptr_records(RDNS_NODE)
+        original = next(records)
+
+        updated = self.driver.ex_update_ptr_record(original,
+                                                   domain=original.domain)
+        self.assertEqual(original.id, updated.id)
+
+        extra_update = {'ttl': original.extra['ttl']}
+        updated = self.driver.ex_update_ptr_record(original,
+                                                   extra=extra_update)
+        self.assertEqual(original.id, updated.id)
+
+        updated = self.driver.ex_update_ptr_record(original, 'new-domain')
+        self.assertEqual(original.id, updated.id)
+
+    def test_ex_delete_ptr_success(self):
+        records = self.driver.ex_iterate_ptr_records(RDNS_NODE)
+        original = next(records)
+        self.assertTrue(self.driver.ex_delete_ptr_record(original))
+
 
 class RackspaceUKTests(RackspaceUSTests):
     klass = RackspaceUKDNSDriver
@@ -360,7 +442,7 @@ class RackspaceMockHttp(MockHttp):
             # Async - update_zone
             body = self.fixtures.load('update_zone_success.json')
         elif method == 'DELETE':
-            # Aync - delete_zone
+            # Async - delete_zone
             body = self.fixtures.load('delete_zone_success.json')
 
         return (httplib.OK, body, self.base_headers,
@@ -485,6 +567,41 @@ class RackspaceMockHttp(MockHttp):
         return (httplib.NOT_FOUND, body, self.base_headers,
                 httplib.responses[httplib.NOT_FOUND])
 
+    def _v1_0_11111_rdns_cloudServersOpenStack(self, method, url, body, headers):
+        if method == 'DELETE':
+            body = self.fixtures.load('delete_ptr_record_success.json')
+            return (httplib.OK, body, self.base_headers,
+                    httplib.responses[httplib.OK])
+        else:
+            body = self.fixtures.load('list_ptr_records_success.json')
+            return (httplib.OK, body, self.base_headers,
+                    httplib.responses[httplib.OK])
+
+    def _v1_0_11111_rdns_cloudServersOpenStack_RECORD_DOES_NOT_EXIST(self, method, url, body, headers):
+        body = self.fixtures.load('does_not_exist.json')
+        return (httplib.NOT_FOUND, body, self.base_headers,
+                httplib.responses[httplib.NOT_FOUND])
+
+    def _v1_0_11111_rdns_cloudServersOpenStack_PTR_7423034(self, method, url, body, headers):
+        body = self.fixtures.load('get_ptr_record_success.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _v1_0_11111_rdns(self, method, url, body, headers):
+        body = self.fixtures.load('create_ptr_record_success.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _v1_0_11111_status_12345678_5739_43fb_8939_f3a2c4c0e99c(self, method, url, body, headers):
+        body = self.fixtures.load('create_ptr_record_success.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
+    def _v1_0_11111_status_12345678_2e5d_490f_bb6e_fdc65d1118a9(self, method, url, body, headers):
+        body = self.fixtures.load('delete_ptr_record_success.json')
+        return (httplib.OK, body, self.base_headers,
+                httplib.responses[httplib.OK])
+
 
 if __name__ == '__main__':
     sys.exit(unittest.main())