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 <anthony.p.s...@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 <greg.h...@rackspace.com> Authored: Thu Dec 3 10:20:06 2015 -0600 Committer: Anthony Shaw <anthony.p.s...@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())