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