diff --git a/octodns/provider/googlecloud.py b/octodns/provider/googlecloud.py new file mode 100644 index 0000000..dff11fc --- /dev/null +++ b/octodns/provider/googlecloud.py @@ -0,0 +1,363 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import re +import shlex +import time +from logging import getLogger + +from google.cloud import dns + +from .base import BaseProvider +from ..record import Record + + +class _GoogleCloudRecordSetMaker(object): + """Wrapper to make google cloud client resource record sets from OctoDNS + Records. + + googlecloud.py: + class: octodns.provider.googlecloud._GoogleCloudRecordSetMaker + An _GoogleCloudRecordSetMaker creates google cloued client resource + records which can be used to update the Google Cloud DNS zones. + """ + + def __init__(self, gcloud_zone, record): + self.gcloud_zone = gcloud_zone + self.record = record + + self._record_set_func = getattr( + self, '_record_set_from_{}'.format(record._type)) + + def get_record_set(self): + return self._record_set_func(self.record) + + def _record_set_from_A(self, record): + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, record.values) + + _record_set_from_AAAA = _record_set_from_A + + def _record_set_from_CAA(self, record): + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, [ + '{flags} {tag} {value}'.format(**record.data['value'])]) + + def _record_set_from_CNAME(self, record): + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, [record.value]) + + def _record_set_from_MX(self, record): + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, [ + '{preference} {exchange}'.format(**v.data) + for v in record.values]) + + def _record_set_from_NAPTR(self, record): + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, [ + '{order} {preference} "{flags}" "{service}" ' + '"{regexp}" {replacement}' + .format(**v.data) for v in record.values]) + + _record_set_from_NS = _record_set_from_A + + _record_set_from_PTR = _record_set_from_CNAME + + _record_set_from_SPF = _record_set_from_A + + def _record_set_from_SRV(self, record): + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, [ + '{priority} {weight} {port} {target}' + .format(**v.data) for v in record.values]) + + def _record_set_from_TXT(self, record): + if 'values' in record.data: + val = record.data['values'] + else: + val = [record.data['value']] + + return self.gcloud_zone.resource_record_set( + record.fqdn, record._type, record.ttl, val) + + +class GoogleCloudProvider(BaseProvider): + """ + Google Cloud DNS provider + + google_cloud: + class: octodns.provider.googlecloud.GoogleCloudProvider + # Credentials file for a service_account or other account can be + # specified with the GOOGLE_APPLICATION_CREDENTIALS environment + # variable. (https://console.cloud.google.com/apis/credentials) + # + # The project to work on (not required) + # project: foobar + """ + + SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', + 'NS', 'PTR', 'SPF', 'SRV', 'TXT')) + SUPPORTS_GEO = False + + def __init__(self, id, project=None, *args, **kwargs): + + # Logger + self.log = getLogger('GoogleCloudProvider[{}]'.format(id)) + self.id = id + + super(GoogleCloudProvider, self).__init__(id, *args, **kwargs) + self.gcloud_client = dns.Client(project=project) + + def _apply(self, plan): + """Required function of manager.py to actually apply a record change. + + :param plan: Contains the zones and changes to be made + :type plan: octodns.provider.base.Plan + + :type return: void + """ + desired = plan.desired + changes = plan.changes + + self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + + # Get gcloud zone, or create one if none existed before. + gcloud_zone = self._get_gcloud_zone(desired.name, create=True) + + gcloud_changes = gcloud_zone.changes() + + for change in changes: + class_name = change.__class__.__name__ + if class_name in 'Create': + gcloud_changes.add_record_set( + self._record_to_record_set(gcloud_zone, change.record)) + elif class_name == 'Delete': + gcloud_changes.delete_record_set( + self._record_to_record_set(gcloud_zone, change.record)) + elif class_name == 'Update': + gcloud_changes.delete_record_set( + self._record_to_record_set(gcloud_zone, change.existing)) + gcloud_changes.add_record_set( + self._record_to_record_set(gcloud_zone, change.new)) + else: + raise RuntimeError('Change type "{}" for change "{!s}" ' + 'is none of "Create", "Delete" or "Update' + .format(class_name, change)) + + gcloud_changes.create() + i = 1 + while gcloud_changes.status != 'done': + self.log.debug("Waiting for changes to complete") + time.sleep(i) + gcloud_changes.reload() + if i < 30: + i += 2 + + def _create_gcloud_zone(self, dns_name): + """Creates a google cloud ManagedZone with dns_name, and zone named + derived from it. calls .create() method and returns it. + + :param dns_name: fqdn of zone to create + :type dns_name: str + + :type return: new google.cloud.dns.ManagedZone + """ + # Zone name must begin with a letter, end with a letter or digit, + # and only contain lowercase letters, digits or dashes + zone_name = re.sub("[^a-z0-9-]", "", + dns_name[:-1].replace('.', "-")) + # make sure that the end result did not end up wo leading letter + if re.match('[^a-z]', zone_name[0]): + # I cannot think of a situation where a zone name derived from + # a domain name would'nt start with leading letter and thereby + # violate the constraint, however if such a situation is + # encountered, add a leading "a" here. + zone_name = "a%s" % zone_name + + gcloud_zone = self.gcloud_client.zone( + name=zone_name, + dns_name=dns_name + ) + gcloud_zone.create(client=self.gcloud_client) + + self.log.info("Created zone %s. Fqdn %s." % + (zone_name, dns_name)) + + return gcloud_zone + + def _get_gcloud_records(self, gcloud_zone, page_token=None): + """ Generator function which yields ResourceRecordSet for the managed + gcloud zone, until there are no more records to pull. + + :param gcloud_zone: zone to pull records from + :type gcloud_zone: google.cloud.dns.ManagedZone + :param page_token: page token for the page to get + + :return: a resource record set + :type return: google.cloud.dns.ResourceRecordSet + """ + gcloud_iterator = gcloud_zone.list_resource_record_sets( + page_token=page_token) + for gcloud_record in gcloud_iterator: + yield gcloud_record + # This is to get results which may be on a "paged" page. + # (if more than max_results) entries. + if gcloud_iterator.next_page_token: + for gcloud_record in self._get_gcloud_records( + gcloud_zone, gcloud_iterator.next_page_token): + # yield from is in python 3 only. + yield gcloud_record + + def _get_gcloud_zone(self, dns_name, page_token=None, create=False): + """Return the ManagedZone which has has the matching dns_name, or + None if no such zone exist, unless create=True, then create a new + one and return it. + + :param dns_name: fqdn of dns name for zone to get. + :type dns_name: str + :param page_token: page token for the page to get + :type page_token: str + :param create: if true, create ManagedZone if it does not exist + already + + :type return: new google.cloud.dns.ManagedZone + """ + # Find the google name for the incoming zone + gcloud_zones = self.gcloud_client.list_zones(page_token=page_token) + for gcloud_zone in gcloud_zones: + if gcloud_zone.dns_name == dns_name: + return gcloud_zone + else: + # Zone not found. Check if there are more results which could be + # retrieved by checking "next_page_token". + if gcloud_zones.next_page_token: + return self._get_gcloud_zone(dns_name, + gcloud_zones.next_page_token) + else: + # Nothing found, either return None or else create zone and + # return that one (if create=True) + self.log.debug('_get_gcloud_zone: zone name=%s, ' + 'was not found by %s.', + dns_name, self.gcloud_client) + if create: + return self._create_gcloud_zone(dns_name) + + @staticmethod + def _record_to_record_set(gcloud_zone, record): + """create google.cloud.dns.ResourceRecordSet from ocdodns.Record + + :param record: a record object + :type record: ocdodns.Record + :param gcloud_zone: a google gcloud zone + :type gcloud_zone: google.cloud.dns.ManagedZone + :type return: google.cloud.dns.ResourceRecordSet + """ + grm = _GoogleCloudRecordSetMaker(gcloud_zone, record) + + return grm.get_record_set() + + def populate(self, zone, target=False, lenient=False): + """Required function of manager.py to collect records from zone. + + :param zone: A dns zone + :type zone: octodns.zone.Zone + :param target: Unused. + :type target: bool + :param lenient: Unused. Check octodns.manager for usage. + :type lenient: bool + + :type return: void + """ + + self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, + target, lenient) + before = len(zone.records) + + gcloud_zone = self._get_gcloud_zone(zone.name) + + _records = set() + if gcloud_zone: + for gcloud_record in self._get_gcloud_records(gcloud_zone): + if gcloud_record.record_type.upper() in self.SUPPORTS: + _records.add(gcloud_record) + for gcloud_record in _records: + record_name = gcloud_record.name + if record_name.endswith(zone.name): + # google cloud always return fqdn. Make relative record + # here. "root" records will then get the '' record_name, + # which is also the way dyn likes it. + record_name = record_name[:-(len(zone.name) + 1)] + typ = gcloud_record.record_type.upper() + data = getattr(self, '_data_for_{}'.format(typ)) + data = data(gcloud_record) + data['type'] = typ + data['ttl'] = gcloud_record.ttl + self.log.debug('populate: adding record {} records: {!s}' + .format(record_name, data)) + record = Record.new(zone, record_name, data, source=self) + zone.add_record(record) + + self.log.info('populate: found %s records', len(zone.records) - before) + + def _data_for_A(self, gcloud_record): + return { + 'values': gcloud_record.rrdatas + } + + _data_for_AAAA = _data_for_A + + def _data_for_CAA(self, gcloud_record): + return { + 'values': [{ + 'flags': v[0], + 'tag': v[1], + 'value': v[2]} + for v in [shlex.split(g) for g in gcloud_record.rrdatas]]} + + def _data_for_CNAME(self, gcloud_record): + return { + 'value': gcloud_record.rrdatas[0] + } + + def _data_for_MX(self, gcloud_record): + return {'values': [{ + "preference": v[0], + "exchange": v[1]} + for v in [shlex.split(g) for g in gcloud_record.rrdatas]]} + + def _data_for_NAPTR(self, gcloud_record): + return {'values': [{ + 'order': v[0], + 'preference': v[1], + 'flags': v[2], + 'service': v[3], + 'regexp': v[4], + 'replacement': v[5]} + for v in [shlex.split(g) for g in gcloud_record.rrdatas]]} + + _data_for_NS = _data_for_A + + _data_for_PTR = _data_for_CNAME + + _data_for_SPF = _data_for_A + + def _data_for_SRV(self, gcloud_record): + return {'values': [{ + 'priority': v[0], + 'weight': v[1], + 'port': v[2], + 'target': v[3]} + for v in [shlex.split(g) for g in gcloud_record.rrdatas]]} + + def _data_for_TXT(self, gcloud_record): + if len(gcloud_record.rrdatas) > 1: + return { + 'values': gcloud_record.rrdatas} + return { + 'value': gcloud_record.rrdatas[0]} diff --git a/requirements.txt b/requirements.txt index a7d1d94..80fbe1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ dnspython==1.15.0 docutils==0.14 dyn==1.8.0 futures==3.1.1 +google-cloud==0.27.0 incf.countryutils==1.0 ipaddress==1.0.18 jmespath==0.9.3 diff --git a/script/test b/script/test index 3ee6e9c..41edfd8 100755 --- a/script/test +++ b/script/test @@ -24,5 +24,6 @@ export DNSIMPLE_TOKEN= export DYN_CUSTOMER= export DYN_PASSWORD= export DYN_USERNAME= +export GOOGLE_APPLICATION_CREDENTIALS= nosetests "$@" diff --git a/tests/test_octodns_provider_googlecloud.py b/tests/test_octodns_provider_googlecloud.py new file mode 100644 index 0000000..568ac4f --- /dev/null +++ b/tests/test_octodns_provider_googlecloud.py @@ -0,0 +1,421 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from octodns.record import Create, Delete, Update, Record +from octodns.provider.googlecloud import GoogleCloudProvider, \ + _GoogleCloudRecordSetMaker + +from octodns.zone import Zone +from octodns.provider.base import Plan + +from unittest import TestCase +from mock import Mock, patch, PropertyMock + +zone = Zone(name='unit.tests.', sub_zones=[]) +octo_records = [] +octo_records.append(Record.new(zone, '', { + 'ttl': 0, + 'type': 'A', + 'values': ['1.2.3.4', '10.10.10.10']})) +octo_records.append(Record.new(zone, 'a', { + 'ttl': 1, + 'type': 'A', + 'values': ['1.2.3.4', '1.1.1.1']})) +octo_records.append(Record.new(zone, 'aa', { + 'ttl': 9001, + 'type': 'A', + 'values': ['1.2.4.3']})) +octo_records.append(Record.new(zone, 'aaa', { + 'ttl': 2, + 'type': 'A', + 'values': ['1.1.1.3']})) +octo_records.append(Record.new(zone, 'cname', { + 'ttl': 3, + 'type': 'CNAME', + 'value': 'a.unit.tests.'})) +octo_records.append(Record.new(zone, 'mx1', { + 'ttl': 3, + 'type': 'MX', + 'values': [{ + 'priority': 10, + 'value': 'mx1.unit.tests.', + }, { + 'priority': 20, + 'value': 'mx2.unit.tests.', + }]})) +octo_records.append(Record.new(zone, 'mx2', { + 'ttl': 3, + 'type': 'MX', + 'values': [{ + 'priority': 10, + 'value': 'mx1.unit.tests.', + }]})) +octo_records.append(Record.new(zone, '', { + 'ttl': 4, + 'type': 'NS', + 'values': ['ns1.unit.tests.', 'ns2.unit.tests.']})) +octo_records.append(Record.new(zone, 'foo', { + 'ttl': 5, + 'type': 'NS', + 'value': 'ns1.unit.tests.'})) +octo_records.append(Record.new(zone, '_srv._tcp', { + 'ttl': 6, + 'type': 'SRV', + 'values': [{ + 'priority': 10, + 'weight': 20, + 'port': 30, + 'target': 'foo-1.unit.tests.', + }, { + 'priority': 12, + 'weight': 30, + 'port': 30, + 'target': 'foo-2.unit.tests.', + }]})) +octo_records.append(Record.new(zone, '_srv2._tcp', { + 'ttl': 7, + 'type': 'SRV', + 'values': [{ + 'priority': 12, + 'weight': 17, + 'port': 1, + 'target': 'srvfoo.unit.tests.', + }]})) +octo_records.append(Record.new(zone, 'txt1', { + 'ttl': 8, + 'type': 'TXT', + 'value': 'txt singleton test'})) +octo_records.append(Record.new(zone, 'txt2', { + 'ttl': 9, + 'type': 'TXT', + 'values': ['txt multiple test', 'txt multiple test 2']})) +octo_records.append(Record.new(zone, 'naptr', { + 'ttl': 9, + 'type': 'NAPTR', + 'values': [{ + 'order': 100, + 'preference': 10, + 'flags': 'S', + 'service': 'SIP+D2U', + 'regexp': "!^.*$!sip:customer-service@unit.tests!", + 'replacement': '_sip._udp.unit.tests.' + }]})) +octo_records.append(Record.new(zone, 'caa', { + 'ttl': 9, + 'type': 'CAA', + 'value': { + 'flags': 0, + 'tag': 'issue', + 'value': 'ca.unit.tests', + }})) +for record in octo_records: + zone.add_record(record) + +# This is the format which the google API likes. +resource_record_sets = [ + ('unit.tests.', u'A', 0, [u'1.2.3.4', u'10.10.10.10']), + (u'a.unit.tests.', u'A', 1, [u'1.1.1.1', u'1.2.3.4']), + (u'aa.unit.tests.', u'A', 9001, [u'1.2.4.3']), + (u'aaa.unit.tests.', u'A', 2, [u'1.1.1.3']), + (u'cname.unit.tests.', u'CNAME', 3, [u'a.unit.tests.']), + (u'mx1.unit.tests.', u'MX', 3, + [u'10 mx1.unit.tests.', u'20 mx2.unit.tests.']), + (u'mx2.unit.tests.', u'MX', 3, [u'10 mx1.unit.tests.']), + ('unit.tests.', u'NS', 4, [u'ns1.unit.tests.', u'ns2.unit.tests.']), + (u'foo.unit.tests.', u'NS', 5, [u'ns1.unit.tests.']), + (u'_srv._tcp.unit.tests.', u'SRV', 6, + [u'10 20 30 foo-1.unit.tests.', u'12 30 30 foo-2.unit.tests.']), + (u'_srv2._tcp.unit.tests.', u'SRV', 7, [u'12 17 1 srvfoo.unit.tests.']), + (u'txt1.unit.tests.', u'TXT', 8, [u'txt singleton test']), + (u'txt2.unit.tests.', u'TXT', 9, + [u'txt multiple test', u'txt multiple test 2']), + (u'naptr.unit.tests.', u'NAPTR', 9, [ + u'100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@unit.tests!"' + u' _sip._udp.unit.tests.']), + (u'caa.unit.tests.', u'CAA', 9, [u'0 issue ca.unit.tests']) +] + + +class DummyResourceRecordSet: + def __init__(self, record_name, record_type, ttl, rrdatas): + self.name = record_name + self.record_type = record_type + self.ttl = ttl + self.rrdatas = rrdatas + + def __eq__(self, other): + try: + return self.name == other.name \ + and self.record_type == other.record_type \ + and self.ttl == other.ttl \ + and sorted(self.rrdatas) == sorted(other.rrdatas) + except: + return False + + def __repr__(self): + return "{} {} {} {!s}"\ + .format(self.name, self.record_type, self.ttl, self.rrdatas) + + def __hash__(self): + return hash(repr(self)) + + +class DummyGoogleCloudZone: + def __init__(self, dns_name): + self.dns_name = dns_name + + def resource_record_set(self, *args): + return DummyResourceRecordSet(*args) + + def list_resource_record_sets(self, *args): + pass + + +class DummyIterator: + """Returns a mock DummyIterator object to use in testing. + This is because API calls for google cloud DNS, if paged, contains a + "next_page_token", which can be used to grab a subsequent + iterator with more results. + + :type return: DummyIterator + """ + def __init__(self, list_of_stuff, page_token=None): + self.iterable = iter(list_of_stuff) + self.next_page_token = page_token + + def __iter__(self): + return self + + def next(self): + return self.iterable.next() + + +class TestGoogleCloudRecordSetMaker(TestCase): + def test_get_record_set(self): + mz = DummyGoogleCloudZone('unit.tests.') + record_sets = [] + for record in octo_records: + mm = _GoogleCloudRecordSetMaker(mz, record) + record_sets.append(mm.get_record_set()) + + self.assertEqual( + len(octo_records), + len(record_sets)) + + +class TestGoogleCloudProvider(TestCase): + @patch('octodns.provider.googlecloud.dns') + def _get_provider(*args): + '''Returns a mock GoogleCloudProvider object to use in testing. + + :type return: GoogleCloudProvider + ''' + return GoogleCloudProvider(id=1, project="mock") + + @patch('octodns.provider.googlecloud.time.sleep') + @patch('octodns.provider.googlecloud.dns') + def test__apply(self, *_): + class DummyDesired: + def __init__(self, name, changes): + self.name = name + self.changes = changes + + apply_z = Zone("unit.tests.", []) + create_r = Record.new(apply_z, '', { + 'ttl': 0, + 'type': 'A', + 'values': ['1.2.3.4', '10.10.10.10']}) + delete_r = Record.new(apply_z, 'a', { + 'ttl': 1, + 'type': 'A', + 'values': ['1.2.3.4', '1.1.1.1']}) + update_existing_r = Record.new(apply_z, 'aa', { + 'ttl': 9001, + 'type': 'A', + 'values': ['1.2.4.3']}) + update_new_r = Record.new(apply_z, 'aa', { + 'ttl': 666, + 'type': 'A', + 'values': ['1.4.3.2']}) + + gcloud_zone_mock = DummyGoogleCloudZone("unit.tests.") + status_mock = Mock() + return_values_for_status = iter( + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', 'done']) + type(status_mock).status = PropertyMock( + side_effect=return_values_for_status.next) + gcloud_zone_mock.changes = Mock(return_value=status_mock) + + provider = self._get_provider() + provider.gcloud_client = Mock() + provider._get_gcloud_zone = Mock( + return_value=gcloud_zone_mock) + desired = Mock() + desired.name = Mock(return_value="unit.tests.") + changes = [] + changes.append(Create(create_r)) + changes.append(Delete(delete_r)) + changes.append(Update(existing=update_existing_r, new=update_new_r)) + + provider.apply(Plan( + existing=[update_existing_r, delete_r], + desired=desired, + changes=changes + )) + + calls_mock = gcloud_zone_mock.changes.return_value + mocked_calls = [] + for mock_call in calls_mock.add_record_set.mock_calls: + mocked_calls.append(mock_call[1][0]) + + self.assertEqual(mocked_calls, [ + DummyResourceRecordSet( + 'unit.tests.', 'A', 0, ['1.2.3.4', '10.10.10.10']), + DummyResourceRecordSet( + 'aa.unit.tests.', 'A', 666, ['1.4.3.2']) + ]) + + mocked_calls2 = [] + for mock_call in calls_mock.delete_record_set.mock_calls: + mocked_calls2.append(mock_call[1][0]) + + self.assertEqual(mocked_calls2, [ + DummyResourceRecordSet( + 'a.unit.tests.', 'A', 1, ['1.2.3.4', '1.1.1.1']), + DummyResourceRecordSet( + 'aa.unit.tests.', 'A', 9001, ['1.2.4.3']) + ]) + + unsupported_change = Mock() + unsupported_change.__len__ = Mock(return_value=1) + mock_plan = Mock() + type(mock_plan).desired = PropertyMock(return_value=DummyDesired( + "dummy name", [])) + type(mock_plan).changes = [unsupported_change] + + with self.assertRaises(RuntimeError): + provider.apply(mock_plan) + + def test__record_to_record_set(self): + provider = self._get_provider() + gcloud_zone = DummyGoogleCloudZone('unit.tests.') + for record in octo_records: + self.assertIsNotNone(provider._record_to_record_set( + gcloud_zone, record)) + + def test__get_gcloud_client(self): + provider = self._get_provider() + + self.assertIsInstance(provider, GoogleCloudProvider) + + @patch('octodns.provider.googlecloud.dns') + def test_populate(self, _): + def _get_mock_zones(page_token=None): + if not page_token: + return DummyIterator([ + DummyGoogleCloudZone('example.com.'), + DummyGoogleCloudZone('example2.com.'), + ], page_token="DUMMY_PAGE_TOKEN") + + return DummyIterator([ + google_cloud_zone + ]) + + def _get_mock_record_sets(page_token=None): + if not page_token: + return DummyIterator( + [DummyResourceRecordSet(*v) for v in + resource_record_sets[:5]], page_token="DUMMY_PAGE_TOKEN") + return DummyIterator( + [DummyResourceRecordSet(*v) for v in resource_record_sets[5:]]) + + google_cloud_zone = DummyGoogleCloudZone('unit.tests.') + + provider = self._get_provider() + provider.gcloud_client.list_zones = Mock(side_effect=_get_mock_zones) + google_cloud_zone.list_resource_record_sets = Mock( + side_effect=_get_mock_record_sets) + + self.assertEqual(provider._get_gcloud_zone("unit.tests.").dns_name, + "unit.tests.") + + test_zone = Zone('unit.tests.', []) + provider.populate(test_zone) + + # test_zone gets fed the same records as zone does, except it's in + # the format returned by google API, so after populate they should look + # excactly the same. + self.assertEqual(test_zone.records, zone.records) + + test_zone2 = Zone('nonexistant.zone.', []) + provider.populate(test_zone2, False, False) + + self.assertEqual(len(test_zone2.records), 0, + msg="Zone should not get records from wrong domain") + + provider.SUPPORTS = set() + test_zone3 = Zone('unit.tests.', []) + provider.populate(test_zone3) + self.assertEqual(len(test_zone3.records), 0) + + @patch('octodns.provider.googlecloud.dns') + def test_populate_corner_cases(self, _): + provider = self._get_provider() + test_zone = Zone('unit.tests.', []) + not_same_fqdn = DummyResourceRecordSet( + 'unit.tests.gr', u'A', 0, [u'1.2.3.4']), + + provider._get_gcloud_records = Mock( + side_effect=[not_same_fqdn]) + provider._get_gcloud_zone = Mock(return_value=DummyGoogleCloudZone( + dns_name="unit.tests.")) + + provider.populate(test_zone) + + self.assertEqual(len(test_zone.records), 1) + + self.assertEqual(test_zone.records.pop().fqdn, + u'unit.tests.gr.unit.tests.') + + def test__get_gcloud_zone(self): + provider = self._get_provider() + + provider.gcloud_client = Mock() + provider.gcloud_client.list_zones = Mock( + return_value=DummyIterator([])) + + self.assertIsNone(provider._get_gcloud_zone("nonexistant.xone"), + msg="Check that nonexistant zones return None when" + "there's no create=True flag") + + def test__create_zone(self): + provider = self._get_provider() + + provider.gcloud_client = Mock() + provider.gcloud_client.list_zones = Mock( + return_value=DummyIterator([])) + + mock_zone = provider._get_gcloud_zone( + 'nonexistant.zone.mock', create=True) + + mock_zone.create.assert_called() + provider.gcloud_client.zone.assert_called() + provider.gcloud_client.zone.assert_called_once_with( + dns_name=u'nonexistant.zone.mock', name=u'nonexistant-zone-moc') + + def test__create_zone_with_numbers_in_name(self): + provider = self._get_provider() + + provider.gcloud_client = Mock() + provider.gcloud_client.list_zones = Mock( + return_value=DummyIterator([])) + + provider._get_gcloud_zone( + '111.', create=True) + provider.gcloud_client.zone.assert_called_once_with( + dns_name=u'111.', name=u'a111')