| @ -0,0 +1,383 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from __future__ import absolute_import, division, print_function, \ | |||||
| unicode_literals | |||||
| from requests import HTTPError, Session, post | |||||
| from collections import defaultdict | |||||
| import logging | |||||
| import string | |||||
| import time | |||||
| from ..record import Record | |||||
| from .base import BaseProvider | |||||
| def add_trailing_dot(s): | |||||
| assert s | |||||
| assert s[-1] != '.' | |||||
| return s + '.' | |||||
| def remove_trailing_dot(s): | |||||
| assert s | |||||
| assert s[-1] == '.' | |||||
| return s[:-1] | |||||
| def escape_semicolon(s): | |||||
| assert s | |||||
| return string.replace(s, ';', '\;') | |||||
| def unescape_semicolon(s): | |||||
| assert s | |||||
| return string.replace(s, '\;', ';') | |||||
| class RackspaceProvider(BaseProvider): | |||||
| SUPPORTS_GEO = False | |||||
| SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NS', 'PTR', 'SPF', | |||||
| 'TXT')) | |||||
| TIMEOUT = 5 | |||||
| def __init__(self, id, username, api_key, ratelimit_delay=0.0, *args, | |||||
| **kwargs): | |||||
| ''' | |||||
| Rackspace API v1 Provider | |||||
| rackspace: | |||||
| class: octodns.provider.rackspace.RackspaceProvider | |||||
| # The the username to authenticate with (required) | |||||
| username: username | |||||
| # The api key that grants access for that user (required) | |||||
| api_key: api-key | |||||
| ''' | |||||
| self.log = logging.getLogger('RackspaceProvider[{}]'.format(id)) | |||||
| super(RackspaceProvider, self).__init__(id, *args, **kwargs) | |||||
| auth_token, dns_endpoint = self._get_auth_token(username, api_key) | |||||
| self.dns_endpoint = dns_endpoint | |||||
| self.ratelimit_delay = float(ratelimit_delay) | |||||
| sess = Session() | |||||
| sess.headers.update({'X-Auth-Token': auth_token}) | |||||
| self._sess = sess | |||||
| # Map record type, name, and data to an id when populating so that | |||||
| # we can find the id for update and delete operations. | |||||
| self._id_map = {} | |||||
| def _get_auth_token(self, username, api_key): | |||||
| ret = post('https://identity.api.rackspacecloud.com/v2.0/tokens', | |||||
| json={"auth": { | |||||
| "RAX-KSKEY:apiKeyCredentials": {"username": username, | |||||
| "apiKey": api_key}}}, | |||||
| ) | |||||
| cloud_dns_endpoint = \ | |||||
| [x for x in ret.json()['access']['serviceCatalog'] if | |||||
| x['name'] == 'cloudDNS'][0]['endpoints'][0]['publicURL'] | |||||
| return ret.json()['access']['token']['id'], cloud_dns_endpoint | |||||
| def _get_zone_id_for(self, zone): | |||||
| ret = self._request('GET', 'domains', pagination_key='domains') | |||||
| return [x for x in ret if x['name'] == zone.name[:-1]][0]['id'] | |||||
| def _request(self, method, path, data=None, pagination_key=None): | |||||
| self.log.debug('_request: method=%s, path=%s', method, path) | |||||
| url = '{}/{}'.format(self.dns_endpoint, path) | |||||
| if pagination_key: | |||||
| resp = self._paginated_request_for_url(method, url, data, | |||||
| pagination_key) | |||||
| else: | |||||
| resp = self._request_for_url(method, url, data) | |||||
| time.sleep(self.ratelimit_delay) | |||||
| return resp | |||||
| def _request_for_url(self, method, url, data): | |||||
| resp = self._sess.request(method, url, json=data, timeout=self.TIMEOUT) | |||||
| self.log.debug('_request: status=%d', resp.status_code) | |||||
| resp.raise_for_status() | |||||
| return resp | |||||
| def _paginated_request_for_url(self, method, url, data, pagination_key): | |||||
| acc = [] | |||||
| resp = self._sess.request(method, url, json=data, timeout=self.TIMEOUT) | |||||
| self.log.debug('_request: status=%d', resp.status_code) | |||||
| resp.raise_for_status() | |||||
| acc.extend(resp.json()[pagination_key]) | |||||
| next_page = [x for x in resp.json().get('links', []) if | |||||
| x['rel'] == 'next'] | |||||
| if next_page: | |||||
| url = next_page[0]['href'] | |||||
| acc.extend(self._paginated_request_for_url(method, url, data, | |||||
| pagination_key)) | |||||
| return acc | |||||
| else: | |||||
| return acc | |||||
| def _post(self, path, data=None): | |||||
| return self._request('POST', path, data=data) | |||||
| def _put(self, path, data=None): | |||||
| return self._request('PUT', path, data=data) | |||||
| def _delete(self, path, data=None): | |||||
| return self._request('DELETE', path, data=data) | |||||
| @staticmethod | |||||
| def _as_unicode(s, codec): | |||||
| if not isinstance(s, unicode): | |||||
| return unicode(s, codec) | |||||
| return s | |||||
| @classmethod | |||||
| def _key_for_record(cls, rs_record): | |||||
| return cls._as_unicode(rs_record['type'], 'ascii'), \ | |||||
| cls._as_unicode(rs_record['name'], 'utf-8'), \ | |||||
| cls._as_unicode(rs_record['data'], 'utf-8') | |||||
| def _data_for_multiple(self, rrset): | |||||
| return { | |||||
| 'type': rrset[0]['type'], | |||||
| 'values': [r['data'] for r in rrset], | |||||
| 'ttl': rrset[0]['ttl'] | |||||
| } | |||||
| _data_for_A = _data_for_multiple | |||||
| _data_for_AAAA = _data_for_multiple | |||||
| def _data_for_NS(self, rrset): | |||||
| return { | |||||
| 'type': rrset[0]['type'], | |||||
| 'values': [add_trailing_dot(r['data']) for r in rrset], | |||||
| 'ttl': rrset[0]['ttl'] | |||||
| } | |||||
| def _data_for_single(self, record): | |||||
| return { | |||||
| 'type': record[0]['type'], | |||||
| 'value': add_trailing_dot(record[0]['data']), | |||||
| 'ttl': record[0]['ttl'] | |||||
| } | |||||
| _data_for_ALIAS = _data_for_single | |||||
| _data_for_CNAME = _data_for_single | |||||
| _data_for_PTR = _data_for_single | |||||
| def _data_for_textual(self, rrset): | |||||
| return { | |||||
| 'type': rrset[0]['type'], | |||||
| 'values': [escape_semicolon(r['data']) for r in rrset], | |||||
| 'ttl': rrset[0]['ttl'] | |||||
| } | |||||
| _data_for_SPF = _data_for_textual | |||||
| _data_for_TXT = _data_for_textual | |||||
| def _data_for_MX(self, rrset): | |||||
| values = [] | |||||
| for record in rrset: | |||||
| values.append({ | |||||
| 'priority': record['priority'], | |||||
| 'value': add_trailing_dot(record['data']), | |||||
| }) | |||||
| return { | |||||
| 'type': rrset[0]['type'], | |||||
| 'values': values, | |||||
| 'ttl': rrset[0]['ttl'] | |||||
| } | |||||
| def populate(self, zone, target=False, lenient=False): | |||||
| self.log.debug('populate: name=%s', zone.name) | |||||
| resp_data = None | |||||
| try: | |||||
| domain_id = self._get_zone_id_for(zone) | |||||
| resp_data = self._request('GET', | |||||
| 'domains/{}/records'.format(domain_id), | |||||
| pagination_key='records') | |||||
| self.log.debug('populate: loaded') | |||||
| except HTTPError as e: | |||||
| if e.response.status_code == 401: | |||||
| # Nicer error message for auth problems | |||||
| raise Exception('Rackspace request unauthorized') | |||||
| elif e.response.status_code == 404: | |||||
| # Zone not found leaves the zone empty instead of failing. | |||||
| return | |||||
| raise | |||||
| before = len(zone.records) | |||||
| if resp_data: | |||||
| records = self._group_records(resp_data) | |||||
| for record_type, records_of_type in records.items(): | |||||
| for raw_record_name, record_set in records_of_type.items(): | |||||
| data_for = getattr(self, | |||||
| '_data_for_{}'.format(record_type)) | |||||
| record_name = zone.hostname_from_fqdn(raw_record_name) | |||||
| record = Record.new(zone, record_name, | |||||
| data_for(record_set), | |||||
| source=self) | |||||
| zone.add_record(record) | |||||
| self.log.info('populate: found %s records', | |||||
| len(zone.records) - before) | |||||
| def _group_records(self, all_records): | |||||
| records = defaultdict(lambda: defaultdict(list)) | |||||
| for record in all_records: | |||||
| self._id_map[self._key_for_record(record)] = record['id'] | |||||
| records[record['type']][record['name']].append(record) | |||||
| return records | |||||
| @staticmethod | |||||
| def _record_for_single(record, value): | |||||
| return { | |||||
| 'name': remove_trailing_dot(record.fqdn), | |||||
| 'type': record._type, | |||||
| 'data': value, | |||||
| 'ttl': max(record.ttl, 300), | |||||
| } | |||||
| _record_for_A = _record_for_single | |||||
| _record_for_AAAA = _record_for_single | |||||
| @staticmethod | |||||
| def _record_for_named(record, value): | |||||
| return { | |||||
| 'name': remove_trailing_dot(record.fqdn), | |||||
| 'type': record._type, | |||||
| 'data': remove_trailing_dot(value), | |||||
| 'ttl': max(record.ttl, 300), | |||||
| } | |||||
| _record_for_NS = _record_for_named | |||||
| _record_for_ALIAS = _record_for_named | |||||
| _record_for_CNAME = _record_for_named | |||||
| _record_for_PTR = _record_for_named | |||||
| @staticmethod | |||||
| def _record_for_textual(record, value): | |||||
| return { | |||||
| 'name': remove_trailing_dot(record.fqdn), | |||||
| 'type': record._type, | |||||
| 'data': unescape_semicolon(value), | |||||
| 'ttl': max(record.ttl, 300), | |||||
| } | |||||
| _record_for_SPF = _record_for_textual | |||||
| _record_for_TXT = _record_for_textual | |||||
| @staticmethod | |||||
| def _record_for_MX(record, value): | |||||
| return { | |||||
| 'name': remove_trailing_dot(record.fqdn), | |||||
| 'type': record._type, | |||||
| 'data': remove_trailing_dot(value.exchange), | |||||
| 'ttl': max(record.ttl, 300), | |||||
| 'priority': value.preference | |||||
| } | |||||
| def _get_values(self, record): | |||||
| try: | |||||
| return record.values | |||||
| except AttributeError: | |||||
| return [record.value] | |||||
| def _mod_Create(self, change): | |||||
| return self._create_given_change_values(change, | |||||
| self._get_values(change.new)) | |||||
| def _create_given_change_values(self, change, values): | |||||
| transformer = getattr(self, "_record_for_{}".format(change.new._type)) | |||||
| return [transformer(change.new, v) for v in values] | |||||
| def _mod_Update(self, change): | |||||
| existing_values = self._get_values(change.existing) | |||||
| new_values = self._get_values(change.new) | |||||
| # A reduction in number of values in an update record needs | |||||
| # to get upgraded into a Delete change for the removed values. | |||||
| deleted_values = set(existing_values) - set(new_values) | |||||
| delete_out = self._delete_given_change_values(change, deleted_values) | |||||
| # An increase in number of values in an update record needs | |||||
| # to get upgraded into a Create change for the added values. | |||||
| create_values = set(new_values) - set(existing_values) | |||||
| create_out = self._create_given_change_values(change, create_values) | |||||
| update_out = [] | |||||
| update_values = set(new_values).intersection(set(existing_values)) | |||||
| for value in update_values: | |||||
| transformer = getattr(self, | |||||
| "_record_for_{}".format(change.new._type)) | |||||
| prior_rs_record = transformer(change.existing, value) | |||||
| prior_key = self._key_for_record(prior_rs_record) | |||||
| next_rs_record = transformer(change.new, value) | |||||
| next_key = self._key_for_record(next_rs_record) | |||||
| next_rs_record["id"] = self._id_map[prior_key] | |||||
| del next_rs_record["type"] | |||||
| update_out.append(next_rs_record) | |||||
| self._id_map[next_key] = self._id_map[prior_key] | |||||
| del self._id_map[prior_key] | |||||
| return create_out, update_out, delete_out | |||||
| def _mod_Delete(self, change): | |||||
| return self._delete_given_change_values(change, self._get_values( | |||||
| change.existing)) | |||||
| def _delete_given_change_values(self, change, values): | |||||
| transformer = getattr(self, "_record_for_{}".format( | |||||
| change.existing._type)) | |||||
| out = [] | |||||
| for value in values: | |||||
| rs_record = transformer(change.existing, value) | |||||
| key = self._key_for_record(rs_record) | |||||
| out.append('id=' + self._id_map[key]) | |||||
| del self._id_map[key] | |||||
| return out | |||||
| def _apply(self, plan): | |||||
| desired = plan.desired | |||||
| changes = plan.changes | |||||
| self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, | |||||
| len(changes)) | |||||
| # Creates, updates, and deletes are processed by different endpoints | |||||
| # and are broken out by record-set entries; pre-process everything | |||||
| # into these buckets in order to minimize the number of API calls. | |||||
| domain_id = self._get_zone_id_for(desired) | |||||
| creates = [] | |||||
| updates = [] | |||||
| deletes = [] | |||||
| for change in changes: | |||||
| if change.__class__.__name__ == 'Create': | |||||
| creates += self._mod_Create(change) | |||||
| elif change.__class__.__name__ == 'Update': | |||||
| add_creates, add_updates, add_deletes = self._mod_Update( | |||||
| change) | |||||
| creates += add_creates | |||||
| updates += add_updates | |||||
| deletes += add_deletes | |||||
| else: | |||||
| assert change.__class__.__name__ == 'Delete' | |||||
| deletes += self._mod_Delete(change) | |||||
| if deletes: | |||||
| params = "&".join(sorted(deletes)) | |||||
| self._delete('domains/{}/records?{}'.format(domain_id, params)) | |||||
| if updates: | |||||
| data = {"records": sorted(updates, key=lambda v: v['name'])} | |||||
| self._put('domains/{}/records'.format(domain_id), data=data) | |||||
| if creates: | |||||
| data = {"records": sorted(creates, key=lambda v: v['type'] + | |||||
| v['name'] + | |||||
| v.get('data', ''))} | |||||
| self._post('domains/{}/records'.format(domain_id), data=data) | |||||
| @ -0,0 +1,87 @@ | |||||
| { | |||||
| "access": { | |||||
| "token": { | |||||
| "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", | |||||
| "expires": "2014-11-24T22:05:39.115Z", | |||||
| "tenant": { | |||||
| "id": "110011", | |||||
| "name": "110011" | |||||
| }, | |||||
| "RAX-AUTH:authenticatedBy": [ | |||||
| "APIKEY" | |||||
| ] | |||||
| }, | |||||
| "serviceCatalog": [ | |||||
| { | |||||
| "name": "cloudDatabases", | |||||
| "endpoints": [ | |||||
| { | |||||
| "publicURL": "https://syd.databases.api.rackspacecloud.com/v1.0/110011", | |||||
| "region": "SYD", | |||||
| "tenantId": "110011" | |||||
| }, | |||||
| { | |||||
| "publicURL": "https://dfw.databases.api.rackspacecloud.com/v1.0/110011", | |||||
| "region": "DFW", | |||||
| "tenantId": "110011" | |||||
| }, | |||||
| { | |||||
| "publicURL": "https://ord.databases.api.rackspacecloud.com/v1.0/110011", | |||||
| "region": "ORD", | |||||
| "tenantId": "110011" | |||||
| }, | |||||
| { | |||||
| "publicURL": "https://iad.databases.api.rackspacecloud.com/v1.0/110011", | |||||
| "region": "IAD", | |||||
| "tenantId": "110011" | |||||
| }, | |||||
| { | |||||
| "publicURL": "https://hkg.databases.api.rackspacecloud.com/v1.0/110011", | |||||
| "region": "HKG", | |||||
| "tenantId": "110011" | |||||
| } | |||||
| ], | |||||
| "type": "rax:database" | |||||
| }, | |||||
| { | |||||
| "name": "cloudDNS", | |||||
| "endpoints": [ | |||||
| { | |||||
| "publicURL": "https://dns.api.rackspacecloud.com/v1.0/110011", | |||||
| "tenantId": "110011" | |||||
| } | |||||
| ], | |||||
| "type": "rax:dns" | |||||
| }, | |||||
| { | |||||
| "name": "rackCDN", | |||||
| "endpoints": [ | |||||
| { | |||||
| "internalURL": "https://global.cdn.api.rackspacecloud.com/v1.0/110011", | |||||
| "publicURL": "https://global.cdn.api.rackspacecloud.com/v1.0/110011", | |||||
| "tenantId": "110011" | |||||
| } | |||||
| ], | |||||
| "type": "rax:cdn" | |||||
| } | |||||
| ], | |||||
| "user": { | |||||
| "id": "123456", | |||||
| "roles": [ | |||||
| { | |||||
| "description": "A Role that allows a user access to keystone Service methods", | |||||
| "id": "6", | |||||
| "name": "compute:default", | |||||
| "tenantId": "110011" | |||||
| }, | |||||
| { | |||||
| "description": "User Admin Role.", | |||||
| "id": "3", | |||||
| "name": "identity:user-admin" | |||||
| } | |||||
| ], | |||||
| "name": "jsmith", | |||||
| "RAX-AUTH:defaultRegion": "ORD" | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,68 @@ | |||||
| { | |||||
| "totalEntries" : 10, | |||||
| "domains" : [ { | |||||
| "name" : "example.com", | |||||
| "id" : 2725233, | |||||
| "comment" : "Optional domain comment...", | |||||
| "updated" : "2011-06-24T01:23:15.000+0000", | |||||
| "accountId" : 1234, | |||||
| "emailAddress" : "sample@rackspace.com", | |||||
| "created" : "2011-06-24T01:12:51.000+0000" | |||||
| }, { | |||||
| "name" : "sub1.example.com", | |||||
| "id" : 2725257, | |||||
| "comment" : "1st sample subdomain", | |||||
| "updated" : "2011-06-23T03:09:34.000+0000", | |||||
| "accountId" : 1234, | |||||
| "emailAddress" : "sample@rackspace.com", | |||||
| "created" : "2011-06-23T03:09:33.000+0000" | |||||
| }, { | |||||
| "name" : "sub2.example.com", | |||||
| "id" : 2725258, | |||||
| "comment" : "1st sample subdomain", | |||||
| "updated" : "2011-06-23T03:52:55.000+0000", | |||||
| "accountId" : 1234, | |||||
| "emailAddress" : "sample@rackspace.com", | |||||
| "created" : "2011-06-23T03:52:55.000+0000" | |||||
| }, { | |||||
| "name" : "north.example.com", | |||||
| "id" : 2725260, | |||||
| "updated" : "2011-06-23T03:53:10.000+0000", | |||||
| "accountId" : 1234, | |||||
| "emailAddress" : "sample@rackspace.com", | |||||
| "created" : "2011-06-23T03:53:09.000+0000" | |||||
| }, { | |||||
| "name" : "south.example.com", | |||||
| "id" : 2725261, | |||||
| "comment" : "Final sample subdomain", | |||||
| "updated" : "2011-06-23T03:53:14.000+0000", | |||||
| "accountId" : 1234, | |||||
| "emailAddress" : "sample@rackspace.com", | |||||
| "created" : "2011-06-23T03:53:14.000+0000" | |||||
| }, { | |||||
| "name" : "region2.example.net", | |||||
| "id" : 2725352, | |||||
| "updated" : "2011-06-23T20:21:06.000+0000", | |||||
| "accountId" : 1234, | |||||
| "created" : "2011-06-23T19:24:27.000+0000" | |||||
| }, { | |||||
| "name" : "example.org", | |||||
| "id" : 2718984, | |||||
| "updated" : "2011-05-03T14:47:32.000+0000", | |||||
| "accountId" : 1234, | |||||
| "created" : "2011-05-03T14:47:30.000+0000" | |||||
| }, { | |||||
| "name" : "rackspace.example", | |||||
| "id" : 2722346, | |||||
| "updated" : "2011-06-21T15:54:31.000+0000", | |||||
| "accountId" : 1234, | |||||
| "created" : "2011-06-15T19:02:07.000+0000" | |||||
| }, { | |||||
| "name" : "unit.tests", | |||||
| "id" : 2722347, | |||||
| "comment" : "Sample comment", | |||||
| "updated" : "2011-06-21T15:54:31.000+0000", | |||||
| "accountId" : 1234, | |||||
| "created" : "2011-06-15T19:02:07.000+0000" | |||||
| } ] | |||||
| } | |||||
| @ -0,0 +1,29 @@ | |||||
| { | |||||
| "totalEntries" : 3, | |||||
| "records" : [{ | |||||
| "name" : "unit.tests.", | |||||
| "id" : "A-6822995", | |||||
| "type" : "A", | |||||
| "data" : "1.2.3.4", | |||||
| "updated" : "2011-06-24T01:12:53.000+0000", | |||||
| "ttl" : 600, | |||||
| "created" : "2011-06-24T01:12:53.000+0000" | |||||
| }, { | |||||
| "name" : "unit.tests.", | |||||
| "id" : "NS-454454", | |||||
| "type" : "NS", | |||||
| "data" : "ns1.example.com", | |||||
| "updated" : "2011-06-24T01:12:51.000+0000", | |||||
| "ttl" : 600, | |||||
| "created" : "2011-06-24T01:12:51.000+0000" | |||||
| }, { | |||||
| "name" : "unit.tests.", | |||||
| "id" : "NS-454455", | |||||
| "type" : "NS", | |||||
| "data" : "ns2.example.com", | |||||
| "updated" : "2011-06-24T01:12:52.000+0000", | |||||
| "ttl" : 600, | |||||
| "created" : "2011-06-24T01:12:52.000+0000" | |||||
| }], | |||||
| "links" : [] | |||||
| } | |||||
| @ -0,0 +1,33 @@ | |||||
| { | |||||
| "totalEntries" : 6, | |||||
| "records" : [ { | |||||
| "name" : "ftp.example.com", | |||||
| "id" : "A-6817754", | |||||
| "type" : "A", | |||||
| "data" : "192.0.2.8", | |||||
| "updated" : "2011-05-19T13:07:08.000+0000", | |||||
| "ttl" : 5771, | |||||
| "created" : "2011-05-18T19:53:09.000+0000" | |||||
| }, { | |||||
| "name" : "example.com", | |||||
| "id" : "A-6822994", | |||||
| "type" : "A", | |||||
| "data" : "192.0.2.17", | |||||
| "updated" : "2011-06-24T01:12:52.000+0000", | |||||
| "ttl" : 86400, | |||||
| "created" : "2011-06-24T01:12:52.000+0000" | |||||
| }, { | |||||
| "name" : "example.com", | |||||
| "id" : "NS-6251982", | |||||
| "type" : "NS", | |||||
| "data" : "ns.rackspace.com", | |||||
| "updated" : "2011-06-24T01:12:51.000+0000", | |||||
| "ttl" : 3600, | |||||
| "created" : "2011-06-24T01:12:51.000+0000" | |||||
| } ], | |||||
| "links" : [ { | |||||
| "content" : "", | |||||
| "href" : "https://localhost/v1.0/1234/domains/domain_id/records?limit=3&offset=3", | |||||
| "rel" : "next" | |||||
| } ] | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| { | |||||
| "totalEntries" : 6, | |||||
| "records" : [ { | |||||
| "name" : "example.com", | |||||
| "id" : "NS-6251983", | |||||
| "type" : "NS", | |||||
| "data" : "ns2.rackspace.com", | |||||
| "updated" : "2011-06-24T01:12:51.000+0000", | |||||
| "ttl" : 3600, | |||||
| "created" : "2011-06-24T01:12:51.000+0000" | |||||
| }, { | |||||
| "name" : "example.com", | |||||
| "priority" : 5, | |||||
| "id" : "MX-3151218", | |||||
| "type" : "MX", | |||||
| "data" : "mail.example.com", | |||||
| "updated" : "2011-06-24T01:12:53.000+0000", | |||||
| "ttl" : 3600, | |||||
| "created" : "2011-06-24T01:12:53.000+0000" | |||||
| }, { | |||||
| "name" : "www.example.com", | |||||
| "id" : "CNAME-9778009", | |||||
| "type" : "CNAME", | |||||
| "comment" : "This is a comment on the CNAME record", | |||||
| "data" : "example.com", | |||||
| "updated" : "2011-06-24T01:12:54.000+0000", | |||||
| "ttl" : 5400, | |||||
| "created" : "2011-06-24T01:12:54.000+0000" | |||||
| } ], | |||||
| "links" : [ { | |||||
| "content" : "", | |||||
| "href" : "https://dns.api.rackspacecloud.com/v1.0/1234/domains/domain_id/records?limit=3&offset=0", | |||||
| "rel" : "previous" | |||||
| }] | |||||
| } | |||||
| @ -0,0 +1,864 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from __future__ import absolute_import, division, print_function, \ | |||||
| unicode_literals | |||||
| import json | |||||
| import re | |||||
| from unittest import TestCase | |||||
| from urlparse import urlparse | |||||
| from requests import HTTPError | |||||
| from requests_mock import ANY, mock as requests_mock | |||||
| from octodns.provider.rackspace import RackspaceProvider | |||||
| from octodns.record import Record | |||||
| from octodns.zone import Zone | |||||
| EMPTY_TEXT = ''' | |||||
| { | |||||
| "totalEntries" : 0, | |||||
| "records" : [] | |||||
| } | |||||
| ''' | |||||
| with open('./tests/fixtures/rackspace-auth-response.json') as fh: | |||||
| AUTH_RESPONSE = fh.read() | |||||
| with open('./tests/fixtures/rackspace-list-domains-response.json') as fh: | |||||
| LIST_DOMAINS_RESPONSE = fh.read() | |||||
| with open('./tests/fixtures/rackspace-sample-recordset-page1.json') as fh: | |||||
| RECORDS_PAGE_1 = fh.read() | |||||
| with open('./tests/fixtures/rackspace-sample-recordset-page2.json') as fh: | |||||
| RECORDS_PAGE_2 = fh.read() | |||||
| class TestRackspaceProvider(TestCase): | |||||
| def setUp(self): | |||||
| self.maxDiff = 1000 | |||||
| with requests_mock() as mock: | |||||
| mock.post(ANY, status_code=200, text=AUTH_RESPONSE) | |||||
| self.provider = RackspaceProvider('identity', 'test', 'api-key', | |||||
| '0') | |||||
| self.assertTrue(mock.called_once) | |||||
| def test_bad_auth(self): | |||||
| with requests_mock() as mock: | |||||
| mock.get(ANY, status_code=401, text='Unauthorized') | |||||
| with self.assertRaises(Exception) as ctx: | |||||
| zone = Zone('unit.tests.', []) | |||||
| self.provider.populate(zone) | |||||
| self.assertTrue('unauthorized' in ctx.exception.message) | |||||
| self.assertTrue(mock.called_once) | |||||
| def test_server_error(self): | |||||
| with requests_mock() as mock: | |||||
| mock.get(ANY, status_code=502, text='Things caught fire') | |||||
| with self.assertRaises(HTTPError) as ctx: | |||||
| zone = Zone('unit.tests.', []) | |||||
| self.provider.populate(zone) | |||||
| self.assertEquals(502, ctx.exception.response.status_code) | |||||
| self.assertTrue(mock.called_once) | |||||
| def test_nonexistent_zone(self): | |||||
| # Non-existent zone doesn't populate anything | |||||
| with requests_mock() as mock: | |||||
| mock.get(ANY, status_code=404, | |||||
| json={'error': "Could not find domain 'unit.tests.'"}) | |||||
| zone = Zone('unit.tests.', []) | |||||
| self.provider.populate(zone) | |||||
| self.assertEquals(set(), zone.records) | |||||
| self.assertTrue(mock.called_once) | |||||
| def test_multipage_populate(self): | |||||
| with requests_mock() as mock: | |||||
| mock.get(re.compile('domains$'), status_code=200, | |||||
| text=LIST_DOMAINS_RESPONSE) | |||||
| mock.get(re.compile('records'), status_code=200, | |||||
| text=RECORDS_PAGE_1) | |||||
| mock.get(re.compile('records.*offset=3'), status_code=200, | |||||
| text=RECORDS_PAGE_2) | |||||
| zone = Zone('unit.tests.', []) | |||||
| self.provider.populate(zone) | |||||
| self.assertEquals(5, len(zone.records)) | |||||
| def test_plan_disappearing_ns_records(self): | |||||
| expected = Zone('unit.tests.', []) | |||||
| expected.add_record(Record.new(expected, '', { | |||||
| 'type': 'NS', | |||||
| 'ttl': 600, | |||||
| 'values': ['8.8.8.8.', '9.9.9.9.'] | |||||
| })) | |||||
| expected.add_record(Record.new(expected, 'sub', { | |||||
| 'type': 'NS', | |||||
| 'ttl': 600, | |||||
| 'values': ['8.8.8.8.', '9.9.9.9.'] | |||||
| })) | |||||
| with requests_mock() as mock: | |||||
| mock.get(re.compile('domains$'), status_code=200, | |||||
| text=LIST_DOMAINS_RESPONSE) | |||||
| mock.get(re.compile('records'), status_code=200, text=EMPTY_TEXT) | |||||
| plan = self.provider.plan(expected) | |||||
| self.assertTrue(mock.called) | |||||
| # OctoDNS does not propagate top-level NS records. | |||||
| self.assertEquals(1, len(plan.changes)) | |||||
| def test_fqdn_a_record(self): | |||||
| expected = Zone('example.com.', []) | |||||
| # expected.add_record(Record.new(expected, 'foo', '1.2.3.4')) | |||||
| with requests_mock() as list_mock: | |||||
| list_mock.get(re.compile('domains$'), status_code=200, | |||||
| text=LIST_DOMAINS_RESPONSE) | |||||
| list_mock.get(re.compile('records'), status_code=200, | |||||
| json={'records': [ | |||||
| {'type': 'A', | |||||
| 'name': 'foo.example.com', | |||||
| 'id': 'A-111111', | |||||
| 'data': '1.2.3.4', | |||||
| 'ttl': 300}]}) | |||||
| plan = self.provider.plan(expected) | |||||
| self.assertTrue(list_mock.called) | |||||
| self.assertEqual(1, len(plan.changes)) | |||||
| self.assertTrue( | |||||
| plan.changes[0].existing.fqdn == 'foo.example.com.') | |||||
| with requests_mock() as mock: | |||||
| def _assert_deleting(request, context): | |||||
| parts = urlparse(request.url) | |||||
| self.assertEqual('id=A-111111', parts.query) | |||||
| mock.get(re.compile('domains$'), status_code=200, | |||||
| text=LIST_DOMAINS_RESPONSE) | |||||
| mock.delete(re.compile('domains/.*/records?.*'), status_code=202, | |||||
| text=_assert_deleting) | |||||
| self.provider.apply(plan) | |||||
| self.assertTrue(mock.called) | |||||
| def _test_apply_with_data(self, data): | |||||
| expected = Zone('unit.tests.', []) | |||||
| for record in data.OtherRecords: | |||||
| expected.add_record( | |||||
| Record.new(expected, record['subdomain'], record['data'])) | |||||
| with requests_mock() as list_mock: | |||||
| list_mock.get(re.compile('domains$'), status_code=200, | |||||
| text=LIST_DOMAINS_RESPONSE) | |||||
| list_mock.get(re.compile('records'), status_code=200, | |||||
| json=data.OwnRecords) | |||||
| plan = self.provider.plan(expected) | |||||
| self.assertTrue(list_mock.called) | |||||
| if not data.ExpectChanges: | |||||
| self.assertFalse(plan) | |||||
| return | |||||
| with requests_mock() as mock: | |||||
| called = set() | |||||
| def make_assert_sending_right_body(expected): | |||||
| def _assert_sending_right_body(request, _context): | |||||
| called.add(request.method) | |||||
| if request.method != 'DELETE': | |||||
| self.assertEqual(request.headers['content-type'], | |||||
| 'application/json') | |||||
| self.assertDictEqual(expected, | |||||
| json.loads(request.body)) | |||||
| else: | |||||
| parts = urlparse(request.url) | |||||
| self.assertEqual(expected, parts.query) | |||||
| return '' | |||||
| return _assert_sending_right_body | |||||
| mock.get(re.compile('domains$'), status_code=200, | |||||
| text=LIST_DOMAINS_RESPONSE) | |||||
| mock.post(re.compile('domains/.*/records$'), status_code=202, | |||||
| text=make_assert_sending_right_body( | |||||
| data.ExpectedAdditions)) | |||||
| mock.delete(re.compile('domains/.*/records?.*'), status_code=202, | |||||
| text=make_assert_sending_right_body( | |||||
| data.ExpectedDeletions)) | |||||
| mock.put(re.compile('domains/.*/records$'), status_code=202, | |||||
| text=make_assert_sending_right_body(data.ExpectedUpdates)) | |||||
| self.provider.apply(plan) | |||||
| self.assertTrue(data.ExpectedAdditions is None or "POST" in called) | |||||
| self.assertTrue( | |||||
| data.ExpectedDeletions is None or "DELETE" in called) | |||||
| self.assertTrue(data.ExpectedUpdates is None or "PUT" in called) | |||||
| def test_apply_no_change_empty(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [] | |||||
| OwnRecords = { | |||||
| "totalEntries": 0, | |||||
| "records": [] | |||||
| } | |||||
| ExpectChanges = False | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_no_change_a_records(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'values': ['1.2.3.4', '1.2.3.5', '1.2.3.6'] | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 3, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-222222", | |||||
| "type": "A", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-333333", | |||||
| "type": "A", | |||||
| "data": "1.2.3.6", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = False | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_no_change_a_records_cross_zone(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| }, | |||||
| { | |||||
| "subdomain": 'bar', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 3, | |||||
| "records": [{ | |||||
| "name": "foo.unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "bar.unit.tests", | |||||
| "id": "A-222222", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = False | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_one_addition(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| }, | |||||
| { | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'NS', | |||||
| 'ttl': 300, | |||||
| 'value': 'ns.example.com.' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 0, | |||||
| "records": [] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "type": "NS", | |||||
| "data": "ns.example.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_create_MX(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'MX', | |||||
| 'ttl': 300, | |||||
| 'value': { | |||||
| 'value': 'mail1.example.com.', | |||||
| 'priority': 1, | |||||
| } | |||||
| } | |||||
| }, | |||||
| { | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'MX', | |||||
| 'ttl': 300, | |||||
| 'value': { | |||||
| 'value': 'mail2.example.com.', | |||||
| 'priority': 2 | |||||
| } | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 0, | |||||
| "records": [] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = { | |||||
| "records": [{ | |||||
| "name": "foo.unit.tests", | |||||
| "type": "MX", | |||||
| "data": "mail2.example.com", | |||||
| "priority": 2, | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "type": "MX", | |||||
| "data": "mail1.example.com", | |||||
| "priority": 1, | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_multiple_additions_splatting(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'values': ['1.2.3.4', '1.2.3.5', '1.2.3.6'] | |||||
| } | |||||
| }, | |||||
| { | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'NS', | |||||
| 'ttl': 300, | |||||
| 'values': ['ns1.example.com.', 'ns2.example.com.'] | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 0, | |||||
| "records": [] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "type": "A", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "type": "A", | |||||
| "data": "1.2.3.6", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "type": "NS", | |||||
| "data": "ns1.example.com", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "type": "NS", | |||||
| "data": "ns2.example.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_multiple_additions_namespaced(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [{ | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| }, { | |||||
| "subdomain": 'bar', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| }, { | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'NS', | |||||
| 'ttl': 300, | |||||
| 'value': 'ns.example.com.' | |||||
| } | |||||
| }] | |||||
| OwnRecords = { | |||||
| "totalEntries": 0, | |||||
| "records": [] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = { | |||||
| "records": [{ | |||||
| "name": "bar.unit.tests", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "type": "NS", | |||||
| "data": "ns.example.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_single_deletion(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [] | |||||
| OwnRecords = { | |||||
| "totalEntries": 1, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "id": "NS-111111", | |||||
| "type": "NS", | |||||
| "data": "ns.example.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = "id=A-111111&id=NS-111111" | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_multiple_deletions(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.5' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 3, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-222222", | |||||
| "type": "A", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-333333", | |||||
| "type": "A", | |||||
| "data": "1.2.3.6", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "id": "NS-111111", | |||||
| "type": "NS", | |||||
| "data": "ns.example.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = "id=A-111111&id=A-333333&id=NS-111111" | |||||
| ExpectedUpdates = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-222222", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_multiple_deletions_cross_zone(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 300, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 3, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "id": "A-222222", | |||||
| "type": "A", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "bar.unit.tests", | |||||
| "id": "A-333333", | |||||
| "type": "A", | |||||
| "data": "1.2.3.6", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = "id=A-222222&id=A-333333" | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_delete_cname(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [] | |||||
| OwnRecords = { | |||||
| "totalEntries": 3, | |||||
| "records": [{ | |||||
| "name": "foo.unit.tests", | |||||
| "id": "CNAME-111111", | |||||
| "type": "CNAME", | |||||
| "data": "a.example.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = "id=CNAME-111111" | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_single_update(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 3600, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 1, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 3600 | |||||
| }] | |||||
| } | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_update_TXT(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'TXT', | |||||
| 'ttl': 300, | |||||
| 'value': 'othervalue' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 1, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "TXT-111111", | |||||
| "type": "TXT", | |||||
| "data": "somevalue", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "type": "TXT", | |||||
| "data": "othervalue", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectedDeletions = 'id=TXT-111111' | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_update_MX(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'MX', | |||||
| 'ttl': 300, | |||||
| 'value': {u'priority': 50, u'value': 'mx.test.com.'} | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 1, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "MX-111111", | |||||
| "type": "MX", | |||||
| "priority": 20, | |||||
| "data": "mx.test.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "type": "MX", | |||||
| "priority": 50, | |||||
| "data": "mx.test.com", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectedDeletions = 'id=MX-111111' | |||||
| ExpectedUpdates = None | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_multiple_updates(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": '', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 3600, | |||||
| 'values': ['1.2.3.4', '1.2.3.5', '1.2.3.6'] | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 3, | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-222222", | |||||
| "type": "A", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-333333", | |||||
| "type": "A", | |||||
| "data": "1.2.3.6", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = { | |||||
| "records": [{ | |||||
| "name": "unit.tests", | |||||
| "id": "A-222222", | |||||
| "data": "1.2.3.5", | |||||
| "ttl": 3600 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-111111", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 3600 | |||||
| }, { | |||||
| "name": "unit.tests", | |||||
| "id": "A-333333", | |||||
| "data": "1.2.3.6", | |||||
| "ttl": 3600 | |||||
| }] | |||||
| } | |||||
| return self._test_apply_with_data(TestData) | |||||
| def test_apply_multiple_updates_cross_zone(self): | |||||
| class TestData(object): | |||||
| OtherRecords = [ | |||||
| { | |||||
| "subdomain": 'foo', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 3600, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| }, | |||||
| { | |||||
| "subdomain": 'bar', | |||||
| "data": { | |||||
| 'type': 'A', | |||||
| 'ttl': 3600, | |||||
| 'value': '1.2.3.4' | |||||
| } | |||||
| } | |||||
| ] | |||||
| OwnRecords = { | |||||
| "totalEntries": 2, | |||||
| "records": [{ | |||||
| "name": "foo.unit.tests", | |||||
| "id": "A-111111", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }, { | |||||
| "name": "bar.unit.tests", | |||||
| "id": "A-222222", | |||||
| "type": "A", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 300 | |||||
| }] | |||||
| } | |||||
| ExpectChanges = True | |||||
| ExpectedAdditions = None | |||||
| ExpectedDeletions = None | |||||
| ExpectedUpdates = { | |||||
| "records": [{ | |||||
| "name": "bar.unit.tests", | |||||
| "id": "A-222222", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 3600 | |||||
| }, { | |||||
| "name": "foo.unit.tests", | |||||
| "id": "A-111111", | |||||
| "data": "1.2.3.4", | |||||
| "ttl": 3600 | |||||
| }] | |||||
| } | |||||
| return self._test_apply_with_data(TestData) | |||||