mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
committed by
GitHub
co-authored by
GitHub
commit
a7c538dcd6
@@ -158,6 +158,12 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| [TinyDNSSource](/octodns/source/tinydns.py) | A, CNAME, MX, NS, PTR | No | read-only |
|
||||
| [YamlProvider](/octodns/provider/yaml.py) | All | Yes | config |
|
||||
|
||||
#### Notes
|
||||
|
||||
* ALIAS support varies a lot fromm provider to provider care should be taken to verify that your needs are met in detail.
|
||||
* Dyn's UI doesn't allow editing or view of TTL, but the API accepts and stores the value provided, this value does not appear to be used when served
|
||||
* Dnsimple's uses the configured TTL when serving things through the ALIAS, there's also a secondary TXT record created alongside the ALIAS that octoDNS ignores
|
||||
|
||||
## Custom Sources and Providers
|
||||
|
||||
You can check out the [source](/octodns/source/) and [provider](/octodns/provider/) directory to see what's currently supported. Sources act as a source of record information. TinyDnsProvider is currently the only OSS source, though we have several others internally that are specific to our environment. These include something to pull host data from [gPanel](https://githubengineering.com/githubs-metal-cloud/) and a similar provider that sources information about our network gear to create both `A` & `PTR` records for their interfaces. Things that might make good OSS sources might include an `ElbSource` that pulls information about [AWS Elastic Load Balancers](https://aws.amazon.com/elasticloadbalancing/) and dynamically creates `CNAME`s for them, or `Ec2Source` that pulls instance information so that records can be created for hosts similar to how our `GPanelProvider` works. An `AxfrSource` could be really interesting as well. Another case where a source may make sense is if you'd like to export data from a legacy service that you have no plans to push changes back into.
|
||||
|
||||
@@ -120,6 +120,8 @@ class DnsimpleProvider(BaseProvider):
|
||||
'value': '{}.'.format(record['content'])
|
||||
}
|
||||
|
||||
_data_for_ALIAS = _data_for_CNAME
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
@@ -238,6 +240,10 @@ class DnsimpleProvider(BaseProvider):
|
||||
_type = record['type']
|
||||
if _type == 'SOA':
|
||||
continue
|
||||
elif _type == 'TXT' and record['content'].startswith('ALIAS for'):
|
||||
# ALIAS has a "ride along" TXT record with 'ALIAS for XXXX',
|
||||
# we're ignoring it
|
||||
continue
|
||||
values[record['name']][record['type']].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
@@ -273,6 +279,7 @@ class DnsimpleProvider(BaseProvider):
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
_params_for_ALIAS = _params_for_single
|
||||
_params_for_CNAME = _params_for_single
|
||||
_params_for_PTR = _params_for_single
|
||||
|
||||
|
||||
+21
-13
@@ -109,6 +109,7 @@ class DynProvider(BaseProvider):
|
||||
RECORDS_TO_TYPE = {
|
||||
'a_records': 'A',
|
||||
'aaaa_records': 'AAAA',
|
||||
'alias_records': 'ALIAS',
|
||||
'cname_records': 'CNAME',
|
||||
'mx_records': 'MX',
|
||||
'naptr_records': 'NAPTR',
|
||||
@@ -119,19 +120,7 @@ class DynProvider(BaseProvider):
|
||||
'srv_records': 'SRV',
|
||||
'txt_records': 'TXT',
|
||||
}
|
||||
TYPE_TO_RECORDS = {
|
||||
'A': 'a_records',
|
||||
'AAAA': 'aaaa_records',
|
||||
'CNAME': 'cname_records',
|
||||
'MX': 'mx_records',
|
||||
'NAPTR': 'naptr_records',
|
||||
'NS': 'ns_records',
|
||||
'PTR': 'ptr_records',
|
||||
'SSHFP': 'sshfp_records',
|
||||
'SPF': 'spf_records',
|
||||
'SRV': 'srv_records',
|
||||
'TXT': 'txt_records',
|
||||
}
|
||||
TYPE_TO_RECORDS = {v: k for k, v in RECORDS_TO_TYPE.items()}
|
||||
|
||||
# https://help.dyn.com/predefined-geotm-regions-groups/
|
||||
REGION_CODES = {
|
||||
@@ -194,6 +183,15 @@ class DynProvider(BaseProvider):
|
||||
|
||||
_data_for_AAAA = _data_for_A
|
||||
|
||||
def _data_for_ALIAS(self, _type, records):
|
||||
# See note on ttl in _kwargs_for_ALIAS
|
||||
record = records[0]
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': record.ttl,
|
||||
'value': record.alias
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
@@ -385,6 +383,16 @@ class DynProvider(BaseProvider):
|
||||
'ttl': record.ttl,
|
||||
}]
|
||||
|
||||
def _kwargs_for_ALIAS(self, record):
|
||||
# NOTE: Dyn's UI doesn't allow editing of ALIAS ttl, but the API seems
|
||||
# to accept and store the values we send it just fine. No clue if they
|
||||
# do anything with them. I'd assume they just obey the TTL of the
|
||||
# record that we're pointed at which makes sense.
|
||||
return [{
|
||||
'alias': record.value,
|
||||
'ttl': record.ttl,
|
||||
}]
|
||||
|
||||
def _kwargs_for_MX(self, record):
|
||||
return [{
|
||||
'preference': v.priority,
|
||||
|
||||
@@ -51,6 +51,7 @@ class Ns1Provider(BaseProvider):
|
||||
'value': record['short_answers'][0],
|
||||
}
|
||||
|
||||
_data_for_ALIAS = _data_for_CNAME
|
||||
_data_for_PTR = _data_for_CNAME
|
||||
|
||||
def _data_for_MX(self, _type, record):
|
||||
@@ -143,6 +144,7 @@ class Ns1Provider(BaseProvider):
|
||||
def _params_for_CNAME(self, record):
|
||||
return {'answers': [record.value], 'ttl': record.ttl}
|
||||
|
||||
_params_for_ALIAS = _params_for_CNAME
|
||||
_params_for_PTR = _params_for_CNAME
|
||||
|
||||
def _params_for_MX(self, record):
|
||||
|
||||
@@ -64,6 +64,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
'ttl': rrset['ttl']
|
||||
}
|
||||
|
||||
_data_for_ALIAS = _data_for_single
|
||||
_data_for_CNAME = _data_for_single
|
||||
_data_for_PTR = _data_for_single
|
||||
|
||||
@@ -191,6 +192,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
def _records_for_single(self, record):
|
||||
return [{'content': record.value, 'disabled': False}]
|
||||
|
||||
_records_for_ALIAS = _records_for_single
|
||||
_records_for_CNAME = _records_for_single
|
||||
_records_for_PTR = _records_for_single
|
||||
|
||||
|
||||
+18
-6
@@ -71,7 +71,7 @@ class Record(object):
|
||||
_type = {
|
||||
'A': ARecord,
|
||||
'AAAA': AaaaRecord,
|
||||
# alias
|
||||
'ALIAS': AliasRecord,
|
||||
# cert
|
||||
'CNAME': CnameRecord,
|
||||
# dhcid
|
||||
@@ -188,13 +188,14 @@ class _ValuesMixin(object):
|
||||
def __init__(self, zone, name, data, source=None):
|
||||
super(_ValuesMixin, self).__init__(zone, name, data, source=source)
|
||||
try:
|
||||
self.values = sorted(self._process_values(data['values']))
|
||||
values = data['values']
|
||||
except KeyError:
|
||||
try:
|
||||
self.values = self._process_values([data['value']])
|
||||
values = [data['value']]
|
||||
except KeyError:
|
||||
raise Exception('Invalid record {}, missing value(s)'
|
||||
.format(self.fqdn))
|
||||
self.values = sorted(self._process_values(values))
|
||||
|
||||
def changes(self, other, target):
|
||||
if self.values != other.values:
|
||||
@@ -293,10 +294,11 @@ class _ValueMixin(object):
|
||||
def __init__(self, zone, name, data, source=None):
|
||||
super(_ValueMixin, self).__init__(zone, name, data, source=source)
|
||||
try:
|
||||
self.value = self._process_value(data['value'])
|
||||
value = data['value']
|
||||
except KeyError:
|
||||
raise Exception('Invalid record {}, missing value'
|
||||
.format(self.fqdn))
|
||||
self.value = self._process_value(value)
|
||||
|
||||
def changes(self, other, target):
|
||||
if self.value != other.value:
|
||||
@@ -314,12 +316,22 @@ class _ValueMixin(object):
|
||||
self.fqdn, self.value)
|
||||
|
||||
|
||||
class AliasRecord(_ValueMixin, Record):
|
||||
_type = 'ALIAS'
|
||||
|
||||
def _process_value(self, value):
|
||||
if not value.endswith('.'):
|
||||
raise Exception('Invalid record {}, value ({}) missing trailing .'
|
||||
.format(self.fqdn, value))
|
||||
return value
|
||||
|
||||
|
||||
class CnameRecord(_ValueMixin, Record):
|
||||
_type = 'CNAME'
|
||||
|
||||
def _process_value(self, value):
|
||||
if not value.endswith('.'):
|
||||
raise Exception('Invalid record {}, value {} missing trailing .'
|
||||
raise Exception('Invalid record {}, value ({}) missing trailing .'
|
||||
.format(self.fqdn, value))
|
||||
return value.lower()
|
||||
|
||||
@@ -437,7 +449,7 @@ class PtrRecord(_ValueMixin, Record):
|
||||
|
||||
def _process_value(self, value):
|
||||
if not value.endswith('.'):
|
||||
raise Exception('Invalid record {}, value {} missing trailing .'
|
||||
raise Exception('Invalid record {}, value ({}) missing trailing .'
|
||||
.format(self.fqdn, value))
|
||||
return value.lower()
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -308,7 +308,7 @@
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"per_page": 20,
|
||||
"total_entries": 28,
|
||||
"total_entries": 29,
|
||||
"total_pages": 2
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+17
-1
@@ -143,12 +143,28 @@
|
||||
"system_record": false,
|
||||
"created_at": "2017-03-09T15:55:09Z",
|
||||
"updated_at": "2017-03-09T15:55:09Z"
|
||||
},
|
||||
{
|
||||
"id": 11188802,
|
||||
"zone_id": "unit.tests",
|
||||
"parent_id": null,
|
||||
"name": "txt",
|
||||
"content": "ALIAS for www.unit.tests.",
|
||||
"ttl": 600,
|
||||
"priority": null,
|
||||
"type": "TXT",
|
||||
"regions": [
|
||||
"global"
|
||||
],
|
||||
"system_record": false,
|
||||
"created_at": "2017-03-09T15:55:09Z",
|
||||
"updated_at": "2017-03-09T15:55:09Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 2,
|
||||
"per_page": 20,
|
||||
"total_entries": 28,
|
||||
"total_entries": 29,
|
||||
"total_pages": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,3 +1154,109 @@ class TestDynProviderGeo(TestCase):
|
||||
# old ruleset ruleset should be deleted, it's pool will have been
|
||||
# reused
|
||||
ruleset_mock.delete.assert_called_once()
|
||||
|
||||
|
||||
class TestDynProviderAlias(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
for name, data in (
|
||||
('', {
|
||||
'type': 'ALIAS',
|
||||
'ttl': 300,
|
||||
'value': 'www.unit.tests.'
|
||||
}),
|
||||
('www', {
|
||||
'type': 'A',
|
||||
'ttl': 300,
|
||||
'values': ['1.2.3.4']
|
||||
})):
|
||||
expected.add_record(Record.new(expected, name, data))
|
||||
|
||||
def setUp(self):
|
||||
# Flush our zone to ensure we start fresh
|
||||
_CachingDynZone.flush_zone(self.expected.name[:-1])
|
||||
|
||||
@patch('dyn.core.SessionEngine.execute')
|
||||
def test_populate(self, execute_mock):
|
||||
provider = DynProvider('test', 'cust', 'user', 'pass')
|
||||
|
||||
# Test Zone create
|
||||
execute_mock.side_effect = [
|
||||
# get Zone
|
||||
{'data': {}},
|
||||
# get_all_records
|
||||
{'data': {
|
||||
'a_records': [{
|
||||
'fqdn': 'www.unit.tests',
|
||||
'rdata': {'address': '1.2.3.4'},
|
||||
'record_id': 1,
|
||||
'record_type': 'A',
|
||||
'ttl': 300,
|
||||
'zone': 'unit.tests',
|
||||
}],
|
||||
'alias_records': [{
|
||||
'fqdn': 'unit.tests',
|
||||
'rdata': {'alias': 'www.unit.tests.'},
|
||||
'record_id': 2,
|
||||
'record_type': 'ALIAS',
|
||||
'ttl': 300,
|
||||
'zone': 'unit.tests',
|
||||
}],
|
||||
}}
|
||||
]
|
||||
got = Zone('unit.tests.', [])
|
||||
provider.populate(got)
|
||||
execute_mock.assert_has_calls([
|
||||
call('/Zone/unit.tests/', 'GET', {}),
|
||||
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
|
||||
])
|
||||
changes = self.expected.changes(got, SimpleProvider())
|
||||
self.assertEquals([], changes)
|
||||
|
||||
@patch('dyn.core.SessionEngine.execute')
|
||||
def test_sync(self, execute_mock):
|
||||
provider = DynProvider('test', 'cust', 'user', 'pass')
|
||||
|
||||
# Test Zone create
|
||||
execute_mock.side_effect = [
|
||||
# No such zone, during populate
|
||||
DynectGetError('foo'),
|
||||
# No such zone, during sync
|
||||
DynectGetError('foo'),
|
||||
# get empty Zone
|
||||
{'data': {}},
|
||||
# get zone we can modify & delete with
|
||||
{'data': {
|
||||
# A top-level to delete
|
||||
'a_records': [{
|
||||
'fqdn': 'www.unit.tests',
|
||||
'rdata': {'address': '1.2.3.4'},
|
||||
'record_id': 1,
|
||||
'record_type': 'A',
|
||||
'ttl': 300,
|
||||
'zone': 'unit.tests',
|
||||
}],
|
||||
# A node to delete
|
||||
'alias_records': [{
|
||||
'fqdn': 'unit.tests',
|
||||
'rdata': {'alias': 'www.unit.tests.'},
|
||||
'record_id': 2,
|
||||
'record_type': 'ALIAS',
|
||||
'ttl': 300,
|
||||
'zone': 'unit.tests',
|
||||
}],
|
||||
}}
|
||||
]
|
||||
|
||||
# No existing records, create all
|
||||
with patch('dyn.tm.zones.Zone.add_record') as add_mock:
|
||||
with patch('dyn.tm.zones.Zone._update') as update_mock:
|
||||
plan = provider.plan(self.expected)
|
||||
update_mock.assert_not_called()
|
||||
provider.apply(plan)
|
||||
update_mock.assert_called()
|
||||
add_mock.assert_called()
|
||||
# Once for each dyn record
|
||||
self.assertEquals(2, len(add_mock.call_args_list))
|
||||
execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
|
||||
call('/Zone/unit.tests/', 'GET', {})])
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
|
||||
@@ -7,9 +7,9 @@ from __future__ import absolute_import, division, print_function, \
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import ARecord, AaaaRecord, CnameRecord, Create, Delete, \
|
||||
GeoValue, MxRecord, NaptrRecord, NaptrValue, NsRecord, PtrRecord, Record, \
|
||||
SshfpRecord, SpfRecord, SrvRecord, TxtRecord, Update
|
||||
from octodns.record import ARecord, AaaaRecord, AliasRecord, CnameRecord, \
|
||||
Create, Delete, GeoValue, MxRecord, NaptrRecord, NaptrValue, NsRecord, \
|
||||
PtrRecord, Record, SshfpRecord, SpfRecord, SrvRecord, TxtRecord, Update
|
||||
from octodns.zone import Zone
|
||||
|
||||
from helpers import GeoProvider, SimpleProvider
|
||||
@@ -242,6 +242,37 @@ class TestRecord(TestCase):
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
def test_alias(self):
|
||||
a_data = {'ttl': 0, 'value': 'www.unit.tests.'}
|
||||
a = AliasRecord(self.zone, '', a_data)
|
||||
self.assertEquals('', a.name)
|
||||
self.assertEquals('unit.tests.', a.fqdn)
|
||||
self.assertEquals(0, a.ttl)
|
||||
self.assertEquals(a_data['value'], a.value)
|
||||
self.assertEquals(a_data, a.data)
|
||||
|
||||
# missing value
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
AliasRecord(self.zone, None, {'ttl': 0})
|
||||
self.assertTrue('missing value' in ctx.exception.message)
|
||||
# bad name
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
AliasRecord(self.zone, None, {'ttl': 0, 'value': 'www.unit.tests'})
|
||||
self.assertTrue('missing trailing .' in ctx.exception.message)
|
||||
|
||||
target = SimpleProvider()
|
||||
# No changes with self
|
||||
self.assertFalse(a.changes(a, target))
|
||||
# Diff in value causes change
|
||||
other = AliasRecord(self.zone, 'a', a_data)
|
||||
other.value = 'foo.unit.tests.'
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
def test_cname(self):
|
||||
self.assertSingleValue(CnameRecord, 'target.foo.com.',
|
||||
'other.foo.com.')
|
||||
|
||||
Reference in New Issue
Block a user