mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Add proposed google cloud provider.
Proposed google cloud provider for #23
This commit is contained in:
363
octodns/provider/googlecloud.py
Normal file
363
octodns/provider/googlecloud.py
Normal file
@@ -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]}
|
||||||
@@ -9,6 +9,7 @@ dnspython==1.15.0
|
|||||||
docutils==0.14
|
docutils==0.14
|
||||||
dyn==1.8.0
|
dyn==1.8.0
|
||||||
futures==3.1.1
|
futures==3.1.1
|
||||||
|
google-cloud==0.27.0
|
||||||
incf.countryutils==1.0
|
incf.countryutils==1.0
|
||||||
ipaddress==1.0.18
|
ipaddress==1.0.18
|
||||||
jmespath==0.9.3
|
jmespath==0.9.3
|
||||||
|
|||||||
@@ -24,5 +24,6 @@ export DNSIMPLE_TOKEN=
|
|||||||
export DYN_CUSTOMER=
|
export DYN_CUSTOMER=
|
||||||
export DYN_PASSWORD=
|
export DYN_PASSWORD=
|
||||||
export DYN_USERNAME=
|
export DYN_USERNAME=
|
||||||
|
export GOOGLE_APPLICATION_CREDENTIALS=
|
||||||
|
|
||||||
nosetests "$@"
|
nosetests "$@"
|
||||||
|
|||||||
421
tests/test_octodns_provider_googlecloud.py
Normal file
421
tests/test_octodns_provider_googlecloud.py
Normal file
@@ -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')
|
||||||
Reference in New Issue
Block a user