Browse Source

Merge branch 'main' into yaml-includes

pull/1050/head
Ross McFarland 2 years ago
committed by GitHub
parent
commit
6f16412432
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 31 deletions
  1. +1
    -0
      README.md
  2. +10
    -9
      octodns/provider/yaml.py
  3. +11
    -6
      octodns/record/base.py
  4. +43
    -9
      octodns/zone.py
  5. +12
    -6
      tests/test_octodns_provider_yaml.py
  6. +10
    -0
      tests/test_octodns_record.py
  7. +78
    -1
      tests/test_octodns_zone.py

+ 1
- 0
README.md View File

@ -293,6 +293,7 @@ The table below lists the providers octoDNS supports. They are maintained in the
| [Rackspace](https://www.rackspace.com/library/what-is-dns) | [octodns_rackspace](https://github.com/octodns/octodns-rackspace/) | | | [Rackspace](https://www.rackspace.com/library/what-is-dns) | [octodns_rackspace](https://github.com/octodns/octodns-rackspace/) | |
| [Scaleway](https://www.scaleway.com/en/dns/) | [octodns_scaleway](https://github.com/scaleway/octodns-scaleway) | | | [Scaleway](https://www.scaleway.com/en/dns/) | [octodns_scaleway](https://github.com/scaleway/octodns-scaleway) | |
| [Selectel](https://selectel.ru/en/services/additional/dns/) | [octodns_selectel](https://github.com/octodns/octodns-selectel/) | | | [Selectel](https://selectel.ru/en/services/additional/dns/) | [octodns_selectel](https://github.com/octodns/octodns-selectel/) | |
| [SPF Value Management](https://github.com/octodns/octodns-spf) | [octodns_spf](https://github.com/octodns/octodns-spf/) | |
| [TransIP](https://www.transip.eu/knowledgebase/entry/155-dns-and-nameservers/) | [octodns_transip](https://github.com/octodns/octodns-transip/) | | | [TransIP](https://www.transip.eu/knowledgebase/entry/155-dns-and-nameservers/) | [octodns_transip](https://github.com/octodns/octodns-transip/) | |
| [UltraDNS](https://vercara.com/authoritative-dns) | [octodns_ultra](https://github.com/octodns/octodns-ultra/) | | | [UltraDNS](https://vercara.com/authoritative-dns) | [octodns_ultra](https://github.com/octodns/octodns-ultra/) | |
| [YamlProvider](/octodns/provider/yaml.py) | built-in | Supports all record types and core functionality | | [YamlProvider](/octodns/provider/yaml.py) | built-in | Supports all record types and core functionality |


+ 10
- 9
octodns/provider/yaml.py View File

@ -49,13 +49,14 @@ class YamlProvider(BaseProvider):
# When writing YAML records out to disk with split_extension enabled # When writing YAML records out to disk with split_extension enabled
# each record is written out into its own file with .yaml appended to # each record is written out into its own file with .yaml appended to
# the name of the record. This would result in files like `.yaml` for
# the apex and `*.yaml` for a wildcard. If your OS doesn't allow such
# filenames or you would prefer to avoid them you can enable
# split_catchall to instead write those records into a file named
# `$[zone.name].yaml`
# (optional, default False)
split_catchall: false
# the name of the record. The two exceptions are for the root and
# wildcard nodes. These records are written into a file named
# `$[zone.name].yaml`. If you would prefer this catchall file not be
# used `split_catchall` can be set to False to instead write those
# records out to `.yaml` and `*.yaml` respectively. Note that some
# operating systems may not allow files with those names.
# (optional, default True)
split_catchall: true
# Optional filename with record data to be included in all zones # Optional filename with record data to be included in all zones
# populated by this provider. Has no effect when used as a target. # populated by this provider. Has no effect when used as a target.
@ -80,7 +81,7 @@ class YamlProvider(BaseProvider):
zones/ zones/
github.com./ github.com./
.yaml
$github.com.yaml
www.yaml www.yaml
... ...
@ -175,7 +176,7 @@ class YamlProvider(BaseProvider):
populate_should_replace=False, populate_should_replace=False,
supports_root_ns=True, supports_root_ns=True,
split_extension=False, split_extension=False,
split_catchall=False,
split_catchall=True,
shared_filename=False, shared_filename=False,
disable_zonefile=False, disable_zonefile=False,
*args, *args,


+ 11
- 6
octodns/record/base.py View File

@ -5,6 +5,7 @@
from collections import defaultdict from collections import defaultdict
from logging import getLogger from logging import getLogger
from ..context import ContextDict
from ..equality import EqualityTupleMixin from ..equality import EqualityTupleMixin
from ..idna import IdnaError, idna_decode, idna_encode from ..idna import IdnaError, idna_decode, idna_encode
from .change import Update from .change import Update
@ -73,7 +74,7 @@ class Record(EqualityTupleMixin):
) )
else: else:
raise ValidationError(fqdn, reasons, context) raise ValidationError(fqdn, reasons, context)
return _class(zone, name, data, source=source)
return _class(zone, name, data, source=source, context=context)
@classmethod @classmethod
def validate(cls, name, fqdn, data): def validate(cls, name, fqdn, data):
@ -136,7 +137,7 @@ class Record(EqualityTupleMixin):
def parse_rdata_texts(cls, rdatas): def parse_rdata_texts(cls, rdatas):
return [cls._value_type.parse_rdata_text(r) for r in rdatas] return [cls._value_type.parse_rdata_text(r) for r in rdatas]
def __init__(self, zone, name, data, source=None):
def __init__(self, zone, name, data, source=None, context=None):
self.zone = zone self.zone = zone
if name: if name:
# internally everything is idna # internally everything is idna
@ -152,11 +153,14 @@ class Record(EqualityTupleMixin):
self.decoded_name, self.decoded_name,
) )
self.source = source self.source = source
self.context = context
self.ttl = int(data['ttl']) self.ttl = int(data['ttl'])
self._octodns = data.get('octodns', {}) self._octodns = data.get('octodns', {})
def _data(self): def _data(self):
if self.context:
return ContextDict({'ttl': self.ttl}, context=self.context)
return {'ttl': self.ttl} return {'ttl': self.ttl}
@property @property
@ -225,6 +229,7 @@ class Record(EqualityTupleMixin):
return Update(self, other) return Update(self, other)
def copy(self, zone=None): def copy(self, zone=None):
# data, via _data(), will preserve context
data = self.data data = self.data
data['type'] = self._type data['type'] = self._type
data['octodns'] = self._octodns data['octodns'] = self._octodns
@ -271,8 +276,8 @@ class ValuesMixin(object):
values = [cls._value_type.parse_rdata_text(rr.rdata) for rr in rrs] values = [cls._value_type.parse_rdata_text(rr.rdata) for rr in rrs]
return {'ttl': rr.ttl, 'type': rr._type, 'values': values} return {'ttl': rr.ttl, 'type': rr._type, 'values': values}
def __init__(self, zone, name, data, source=None):
super().__init__(zone, name, data, source=source)
def __init__(self, zone, name, data, source=None, context=None):
super().__init__(zone, name, data, source=source, context=context)
try: try:
values = data['values'] values = data['values']
except KeyError: except KeyError:
@ -333,8 +338,8 @@ class ValueMixin(object):
'value': cls._value_type.parse_rdata_text(rr.rdata), 'value': cls._value_type.parse_rdata_text(rr.rdata),
} }
def __init__(self, zone, name, data, source=None):
super().__init__(zone, name, data, source=source)
def __init__(self, zone, name, data, source=None, context=None):
super().__init__(zone, name, data, source=source, context=context)
self.value = self._value_type.process(data['value']) self.value = self._value_type.process(data['value'])
def changes(self, other, target): def changes(self, other, target):


+ 43
- 9
octodns/zone.py View File

@ -11,15 +11,45 @@ from .record import Create, Delete
class SubzoneRecordException(Exception): class SubzoneRecordException(Exception):
pass
def __init__(self, msg, record):
self.record = record
if record.context:
msg += f', {record.context}'
super().__init__(msg)
class DuplicateRecordException(Exception): class DuplicateRecordException(Exception):
pass
def __init__(self, msg, existing, new):
self.existing = existing
self.new = new
if existing.context:
if new.context:
# both have context
msg += f'\n existing: {existing.context}\n new: {new.context}'
else:
# only existing has context
msg += (
f'\n existing: {existing.context}\n new: [UNKNOWN]'
)
elif new.context:
# only new has context
msg += f'\n existing: [UNKNOWN]\n new: {new.context}'
# else no one has context
super().__init__(msg)
class InvalidNodeException(Exception): class InvalidNodeException(Exception):
pass
def __init__(self, msg, record):
self.record = record
if record.context:
msg += f', {record.context}'
super().__init__(msg)
class Zone(object): class Zone(object):
@ -113,7 +143,8 @@ class Zone(object):
if not record._type == 'NS': if not record._type == 'NS':
# and not a NS record, this should be in the sub # and not a NS record, this should be in the sub
raise SubzoneRecordException( raise SubzoneRecordException(
f'Record {record.fqdn} is a managed sub-zone and not of type NS'
f'Record {record.fqdn} is a managed sub-zone and not of type NS',
record,
) )
else: else:
# It's not an exact match so there has to be a `.` before the # It's not an exact match so there has to be a `.` before the
@ -122,7 +153,8 @@ class Zone(object):
if name.endswith(f'.{sub_zone}'): if name.endswith(f'.{sub_zone}'):
# this should be in a sub # this should be in a sub
raise SubzoneRecordException( raise SubzoneRecordException(
f'Record {record.fqdn} is under a managed subzone'
f'Record {record.fqdn} is under a managed subzone',
record,
) )
if replace: if replace:
@ -132,8 +164,11 @@ class Zone(object):
node = self._records[name] node = self._records[name]
if record in node: if record in node:
# We already have a record at this node of this type # We already have a record at this node of this type
existing = [c for c in node if c == record][0]
raise DuplicateRecordException( raise DuplicateRecordException(
f'Duplicate record {record.fqdn}, ' f'type {record._type}'
f'Duplicate record {record.fqdn}, type {record._type}',
existing,
record,
) )
elif not lenient and ( elif not lenient and (
(record._type == 'CNAME' and len(node) > 0) (record._type == 'CNAME' and len(node) > 0)
@ -142,9 +177,8 @@ class Zone(object):
# We're adding a CNAME to existing records or adding to an existing # We're adding a CNAME to existing records or adding to an existing
# CNAME # CNAME
raise InvalidNodeException( raise InvalidNodeException(
'Invalid state, CNAME at '
f'{record.fqdn} cannot coexist with '
'other records'
f'Invalid state, CNAME at {record.fqdn} cannot coexist with other records',
record,
) )
if record._type == 'NS' and record.name == '': if record._type == 'NS' and record.name == '':


+ 12
- 6
tests/test_octodns_provider_yaml.py View File

@ -260,10 +260,13 @@ xn--dj-kia8a:
zone = Zone('unit.tests.', ['sub']) zone = Zone('unit.tests.', ['sub'])
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
source.populate(zone) source.populate(zone)
self.assertEqual(
'Record www.sub.unit.tests. is under a managed subzone',
str(ctx.exception),
msg = str(ctx.exception)
self.assertTrue(
msg.startswith(
'Record www.sub.unit.tests. is under a managed subzone'
)
) )
self.assertTrue(msg.endswith('unit.tests.yaml, line 201, column 3'))
def test_SUPPORTS(self): def test_SUPPORTS(self):
source = YamlProvider('test', join(dirname(__file__), 'config')) source = YamlProvider('test', join(dirname(__file__), 'config'))
@ -694,10 +697,13 @@ class TestSplitYamlProvider(TestCase):
zone = Zone('unit.tests.', ['sub']) zone = Zone('unit.tests.', ['sub'])
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
source.populate(zone) source.populate(zone)
self.assertEqual(
'Record www.sub.unit.tests. is under a managed subzone',
str(ctx.exception),
msg = str(ctx.exception)
self.assertTrue(
msg.startswith(
'Record www.sub.unit.tests. is under a managed subzone'
)
) )
self.assertTrue(msg.endswith('www.sub.yaml, line 3, column 3'))
def test_copy(self): def test_copy(self):
# going to put some sentinal values in here to ensure, these aren't # going to put some sentinal values in here to ensure, these aren't


+ 10
- 0
tests/test_octodns_record.py View File

@ -628,3 +628,13 @@ class TestRecordValidation(TestCase):
ContextDict({'ttl': 42, 'value': '1.2.3.4'}, context='needle'), ContextDict({'ttl': 42, 'value': '1.2.3.4'}, context='needle'),
) )
self.assertTrue('needle' in str(ctx.exception)) self.assertTrue('needle' in str(ctx.exception))
def test_context_copied_to_record(self):
record = Record.new(
self.zone,
'www',
ContextDict(
{'ttl': 42, 'type': 'A', 'value': '1.2.3.4'}, context='needle'
),
)
self.assertEqual('needle', record.context)

+ 78
- 1
tests/test_octodns_zone.py View File

@ -6,6 +6,7 @@ from unittest import TestCase
from helpers import SimpleProvider from helpers import SimpleProvider
from octodns.context import ContextDict
from octodns.idna import idna_encode from octodns.idna import idna_encode
from octodns.record import ( from octodns.record import (
AaaaRecord, AaaaRecord,
@ -106,6 +107,62 @@ class TestZone(TestCase):
zone.add_record(b) zone.add_record(b)
self.assertEqual(zone.records, set([a, b])) self.assertEqual(zone.records, set([a, b]))
def test_duplicate_context_handling(self):
zone = Zone('unit.tests.', [])
# these will be ==, but one has context and the other doesn't
no_context = ARecord(zone, 'a', {'ttl': 42, 'value': '1.1.1.1'})
has_context = ARecord(
zone, 'a', {'ttl': 42, 'value': '1.1.1.1'}, context='hello world'
)
# both have context
zone.add_record(has_context)
with self.assertRaises(DuplicateRecordException) as ctx:
zone.add_record(has_context)
self.assertEqual(has_context, ctx.exception.existing)
self.assertEqual(has_context, ctx.exception.new)
zone.remove_record(has_context)
self.assertEqual(
[
'Duplicate record a.unit.tests., type A',
' existing: hello world',
' new: hello world',
],
str(ctx.exception).split('\n'),
)
# new has context
zone.add_record(no_context)
with self.assertRaises(DuplicateRecordException) as ctx:
zone.add_record(has_context)
self.assertEqual(no_context, ctx.exception.existing)
self.assertEqual(has_context, ctx.exception.new)
zone.remove_record(no_context)
self.assertEqual(
[
'Duplicate record a.unit.tests., type A',
' existing: [UNKNOWN]',
' new: hello world',
],
str(ctx.exception).split('\n'),
)
# existing has context
zone.add_record(has_context)
with self.assertRaises(DuplicateRecordException) as ctx:
zone.add_record(no_context)
self.assertEqual(has_context, ctx.exception.existing)
self.assertEqual(no_context, ctx.exception.new)
self.assertEqual(
[
'Duplicate record a.unit.tests., type A',
' existing: hello world',
' new: [UNKNOWN]',
],
str(ctx.exception).split('\n'),
)
def test_changes(self): def test_changes(self):
before = Zone('unit.tests.', []) before = Zone('unit.tests.', [])
a = ARecord(before, 'a', {'ttl': 42, 'value': '1.1.1.1'}) a = ARecord(before, 'a', {'ttl': 42, 'value': '1.1.1.1'})
@ -242,9 +299,11 @@ class TestZone(TestCase):
'sub', 'sub',
{'ttl': 3600, 'type': 'A', 'values': ['1.2.3.4', '2.3.4.5']}, {'ttl': 3600, 'type': 'A', 'values': ['1.2.3.4', '2.3.4.5']},
) )
record.context = 'added context'
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
zone.add_record(record) zone.add_record(record)
self.assertTrue('not of type NS', str(ctx.exception)) self.assertTrue('not of type NS', str(ctx.exception))
self.assertTrue(', added context' in str(ctx.exception))
# Can add it w/lenient # Can add it w/lenient
zone.add_record(record, lenient=True) zone.add_record(record, lenient=True)
self.assertEqual(set([record]), zone.records) self.assertEqual(set([record]), zone.records)
@ -328,11 +387,13 @@ class TestZone(TestCase):
cname = Record.new( cname = Record.new(
zone, 'www', {'ttl': 60, 'type': 'CNAME', 'value': 'foo.bar.com.'} zone, 'www', {'ttl': 60, 'type': 'CNAME', 'value': 'foo.bar.com.'}
) )
cname.context = 'has some context'
# add cname to a # add cname to a
zone.add_record(a) zone.add_record(a)
with self.assertRaises(InvalidNodeException):
with self.assertRaises(InvalidNodeException) as ctx:
zone.add_record(cname) zone.add_record(cname)
self.assertTrue(', has some context' in str(ctx.exception))
self.assertEqual(set([a]), zone.records) self.assertEqual(set([a]), zone.records)
zone.add_record(cname, lenient=True) zone.add_record(cname, lenient=True)
self.assertEqual(set([a, cname]), zone.records) self.assertEqual(set([a, cname]), zone.records)
@ -501,6 +562,22 @@ class TestZone(TestCase):
# Doesn't the second # Doesn't the second
self.assertFalse(copy.hydrate()) self.assertFalse(copy.hydrate())
def test_copy_context(self):
zone = Zone('unit.tests.', [])
no_context = Record.new(
zone, 'a', {'ttl': 42, 'type': 'A', 'value': '1.1.1.1'}
)
self.assertFalse(no_context.context)
self.assertFalse(no_context.copy().context)
data = ContextDict(
{'ttl': 42, 'type': 'A', 'value': '1.1.1.1'}, context='hello world'
)
has_context = Record.new(zone, 'a', data)
self.assertTrue(has_context.context)
self.assertTrue(has_context.copy().context)
def test_root_ns(self): def test_root_ns(self):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])


Loading…
Cancel
Save