From b402a43665ea6a2025ed7f2ab0977df2ef5530f6 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 6 Sep 2022 14:55:24 -0700 Subject: [PATCH 01/14] All record value(s) are first-class objects, as compatible as possible with previous str/dict --- octodns/record/__init__.py | 603 ++++++++++++++++++++++++++--------- tests/test_octodns_record.py | 288 ++++++++++------- 2 files changed, 618 insertions(+), 273 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 851574d..f2ee1a5 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -9,7 +9,7 @@ from __future__ import ( unicode_literals, ) -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address as _IPv4Address, IPv6Address as _IPv6Address from logging import getLogger import re @@ -326,7 +326,7 @@ class ValuesMixin(object): def __init__(self, zone, name, data, source=None): super(ValuesMixin, self).__init__(zone, name, data, source=source) try: - values = data['values'] + values = [v for v in data['values']] except KeyError: values = [data['value']] self.values = sorted(self._value_type.process(values)) @@ -770,7 +770,38 @@ class _DynamicMixin(object): return super(_DynamicMixin, self).__repr__() -class _IpList(object): +class _TargetValue(str): + @classmethod + def validate(cls, data, _type): + reasons = [] + if data == '': + reasons.append('empty value') + elif not data: + reasons.append('missing value') + # NOTE: FQDN complains if the data it receives isn't a str, it doesn't + # allow unicode... This is likely specific to 2.7 + elif not FQDN(str(data), allow_underscores=True).is_valid: + reasons.append(f'{_type} value "{data}" is not a valid FQDN') + elif not data.endswith('.'): + reasons.append(f'{_type} value "{data}" missing trailing .') + return reasons + + @classmethod + def process(self, value): + if value: + return value.lower() + return value + + +class CnameValue(_TargetValue): + pass + + +class DnameValue(_TargetValue): + pass + + +class _IpAddress(str): @classmethod def validate(cls, data, _type): if not isinstance(data, (list, tuple)): @@ -795,64 +826,38 @@ class _IpList(object): def process(cls, values): # Translating None into '' so that the list will be sortable in # python3, get everything to str first - values = [str(v) if v is not None else '' for v in values] + values = [v if v is not None else '' for v in values] # Now round trip all non-'' through the address type and back to a str # to normalize the address representation. - return [str(cls._address_type(v)) if v != '' else '' for v in values] - - -class Ipv4List(_IpList): - _address_name = 'IPv4' - _address_type = IPv4Address - - -class Ipv6List(_IpList): - _address_name = 'IPv6' - _address_type = IPv6Address - - -class _TargetValue(object): - @classmethod - def validate(cls, data, _type): - reasons = [] - if data == '': - reasons.append('empty value') - elif not data: - reasons.append('missing value') - # NOTE: FQDN complains if the data it receives isn't a str, it doesn't - # allow unicode... This is likely specific to 2.7 - elif not FQDN(str(data), allow_underscores=True).is_valid: - reasons.append(f'{_type} value "{data}" is not a valid FQDN') - elif not data.endswith('.'): - reasons.append(f'{_type} value "{data}" missing trailing .') - return reasons - - @classmethod - def process(self, value): - if value: - return value.lower() - return value + return [cls(v) if v != '' else '' for v in values] - -class CnameValue(_TargetValue): - pass + def __new__(cls, v): + if v: + v = str(cls._address_type(v)) + return super().__new__(cls, v) -class DnameValue(_TargetValue): - pass +class Ipv4Address(_IpAddress): + _address_type = _IPv4Address + _address_name = 'IPv4' class ARecord(_DynamicMixin, _GeoMixin, Record): _type = 'A' - _value_type = Ipv4List + _value_type = Ipv4Address Record.register_type(ARecord) +class Ipv6Address(_IpAddress): + _address_type = _IPv6Address + _address_name = 'IPv6' + + class AaaaRecord(_DynamicMixin, _GeoMixin, Record): _type = 'AAAA' - _value_type = Ipv6List + _value_type = Ipv6Address Record.register_type(AaaaRecord) @@ -878,7 +883,7 @@ class AliasRecord(ValueMixin, Record): Record.register_type(AliasRecord) -class CaaValue(EqualityTupleMixin): +class CaaValue(EqualityTupleMixin, dict): # https://tools.ietf.org/html/rfc6844#page-5 @classmethod @@ -905,13 +910,41 @@ class CaaValue(EqualityTupleMixin): return [CaaValue(v) for v in values] def __init__(self, value): - self.flags = int(value.get('flags', 0)) - self.tag = value['tag'] - self.value = value['value'] + super().__init__( + { + 'flags': int(value.get('flags', 0)), + 'tag': value['tag'], + 'value': value['value'], + } + ) + + @property + def flags(self): + return self['flags'] + + @flags.setter + def flags(self, value): + self['flags'] = value + + @property + def tag(self): + return self['tag'] + + @tag.setter + def tag(self, value): + self['tag'] = value + + @property + def value(self): + return self['value'] + + @value.setter + def value(self, value): + self['value'] = value @property def data(self): - return {'flags': self.flags, 'tag': self.tag, 'value': self.value} + return self def _equality_tuple(self): return (self.flags, self.tag, self.value) @@ -952,7 +985,7 @@ class DnameRecord(_DynamicMixin, ValueMixin, Record): Record.register_type(DnameRecord) -class LocValue(EqualityTupleMixin): +class LocValue(EqualityTupleMixin, dict): # TODO: work out how to do defaults per RFC @classmethod @@ -1051,35 +1084,122 @@ class LocValue(EqualityTupleMixin): return [LocValue(v) for v in values] def __init__(self, value): - self.lat_degrees = int(value['lat_degrees']) - self.lat_minutes = int(value['lat_minutes']) - self.lat_seconds = float(value['lat_seconds']) - self.lat_direction = value['lat_direction'].upper() - self.long_degrees = int(value['long_degrees']) - self.long_minutes = int(value['long_minutes']) - self.long_seconds = float(value['long_seconds']) - self.long_direction = value['long_direction'].upper() - self.altitude = float(value['altitude']) - self.size = float(value['size']) - self.precision_horz = float(value['precision_horz']) - self.precision_vert = float(value['precision_vert']) + super().__init__( + { + 'lat_degrees': int(value['lat_degrees']), + 'lat_minutes': int(value['lat_minutes']), + 'lat_seconds': float(value['lat_seconds']), + 'lat_direction': value['lat_direction'].upper(), + 'long_degrees': int(value['long_degrees']), + 'long_minutes': int(value['long_minutes']), + 'long_seconds': float(value['long_seconds']), + 'long_direction': value['long_direction'].upper(), + 'altitude': float(value['altitude']), + 'size': float(value['size']), + 'precision_horz': float(value['precision_horz']), + 'precision_vert': float(value['precision_vert']), + } + ) + + @property + def lat_degrees(self): + return self['lat_degrees'] + + @lat_degrees.setter + def lat_degrees(self, value): + self['lat_degrees'] = value + + @property + def lat_minutes(self): + return self['lat_minutes'] + + @lat_minutes.setter + def lat_minutes(self, value): + self['lat_minutes'] = value + + @property + def lat_seconds(self): + return self['lat_seconds'] + + @lat_seconds.setter + def lat_seconds(self, value): + self['lat_seconds'] = value + + @property + def lat_direction(self): + return self['lat_direction'] + + @lat_direction.setter + def lat_direction(self, value): + self['lat_direction'] = value + + @property + def long_degrees(self): + return self['long_degrees'] + + @long_degrees.setter + def long_degrees(self, value): + self['long_degrees'] = value + + @property + def long_minutes(self): + return self['long_minutes'] + + @long_minutes.setter + def long_minutes(self, value): + self['long_minutes'] = value + + @property + def long_seconds(self): + return self['long_seconds'] + + @long_seconds.setter + def long_seconds(self, value): + self['long_seconds'] = value + + @property + def long_direction(self): + return self['long_direction'] + + @long_direction.setter + def long_direction(self, value): + self['long_direction'] = value + + @property + def altitude(self): + return self['altitude'] + + @altitude.setter + def altitude(self, value): + self['altitude'] = value + + @property + def size(self): + return self['size'] + + @size.setter + def size(self, value): + self['size'] = value + + @property + def precision_horz(self): + return self['precision_horz'] + + @precision_horz.setter + def precision_horz(self, value): + self['precision_horz'] = value + + @property + def precision_vert(self): + return self['precision_vert'] + + @precision_vert.setter + def precision_vert(self, value): + self['precision_vert'] = value @property def data(self): - return { - 'lat_degrees': self.lat_degrees, - 'lat_minutes': self.lat_minutes, - 'lat_seconds': self.lat_seconds, - 'lat_direction': self.lat_direction, - 'long_degrees': self.long_degrees, - 'long_minutes': self.long_minutes, - 'long_seconds': self.long_seconds, - 'long_direction': self.long_direction, - 'altitude': self.altitude, - 'size': self.size, - 'precision_horz': self.precision_horz, - 'precision_vert': self.precision_vert, - } + return self def __hash__(self): return hash( @@ -1134,7 +1254,7 @@ class LocRecord(ValuesMixin, Record): Record.register_type(LocRecord) -class MxValue(EqualityTupleMixin): +class MxValue(EqualityTupleMixin, dict): @classmethod def validate(cls, data, _type): if not isinstance(data, (list, tuple)): @@ -1177,17 +1297,34 @@ class MxValue(EqualityTupleMixin): preference = value['preference'] except KeyError: preference = value['priority'] - self.preference = int(preference) # UNTIL 1.0 remove value fallback try: exchange = value['exchange'] except KeyError: exchange = value['value'] - self.exchange = exchange.lower() + super().__init__( + {'preference': int(preference), 'exchange': exchange.lower()} + ) + + @property + def preference(self): + return self['preference'] + + @preference.setter + def preference(self, value): + self['preference'] = value + + @property + def exchange(self): + return self['exchange'] + + @exchange.setter + def exchange(self, value): + self['exchange'] = value @property def data(self): - return {'preference': self.preference, 'exchange': self.exchange} + return self def __hash__(self): return hash((self.preference, self.exchange)) @@ -1207,7 +1344,7 @@ class MxRecord(ValuesMixin, Record): Record.register_type(MxRecord) -class NaptrValue(EqualityTupleMixin): +class NaptrValue(EqualityTupleMixin, dict): VALID_FLAGS = ('S', 'A', 'U', 'P') @classmethod @@ -1247,23 +1384,68 @@ class NaptrValue(EqualityTupleMixin): return [NaptrValue(v) for v in values] def __init__(self, value): - self.order = int(value['order']) - self.preference = int(value['preference']) - self.flags = value['flags'] - self.service = value['service'] - self.regexp = value['regexp'] - self.replacement = value['replacement'] + super().__init__( + { + 'order': int(value['order']), + 'preference': int(value['preference']), + 'flags': value['flags'], + 'service': value['service'], + 'regexp': value['regexp'], + 'replacement': value['replacement'], + } + ) + + @property + def order(self): + return self['order'] + + @order.setter + def order(self, value): + self['order'] = value + + @property + def preference(self): + return self['preference'] + + @preference.setter + def preference(self, value): + self['preference'] = value + + @property + def flags(self): + return self['flags'] + + @flags.setter + def flags(self, value): + self['flags'] = value + + @property + def service(self): + return self['service'] + + @service.setter + def service(self, value): + self['service'] = value + + @property + def regexp(self): + return self['regexp'] + + @regexp.setter + def regexp(self, value): + self['regexp'] = value + + @property + def replacement(self): + return self['replacement'] + + @replacement.setter + def replacement(self, value): + self['replacement'] = value @property def data(self): - return { - 'order': self.order, - 'preference': self.preference, - 'flags': self.flags, - 'service': self.service, - 'regexp': self.regexp, - 'replacement': self.replacement, - } + return self def __hash__(self): return hash(self.__repr__()) @@ -1296,7 +1478,7 @@ class NaptrRecord(ValuesMixin, Record): Record.register_type(NaptrRecord) -class _NsValue(object): +class _NsValue(str): @classmethod def validate(cls, data, _type): if not data: @@ -1361,7 +1543,7 @@ class PtrRecord(ValuesMixin, Record): Record.register_type(PtrRecord) -class SshfpValue(EqualityTupleMixin): +class SshfpValue(EqualityTupleMixin, dict): VALID_ALGORITHMS = (1, 2, 3, 4) VALID_FINGERPRINT_TYPES = (1, 2) @@ -1400,17 +1582,41 @@ class SshfpValue(EqualityTupleMixin): return [SshfpValue(v) for v in values] def __init__(self, value): - self.algorithm = int(value['algorithm']) - self.fingerprint_type = int(value['fingerprint_type']) - self.fingerprint = value['fingerprint'] + super().__init__( + { + 'algorithm': int(value['algorithm']), + 'fingerprint_type': int(value['fingerprint_type']), + 'fingerprint': value['fingerprint'], + } + ) + + @property + def algorithm(self): + return self['algorithm'] + + @algorithm.setter + def algorithm(self, value): + self['algorithm'] = value + + @property + def fingerprint_type(self): + return self['fingerprint_type'] + + @fingerprint_type.setter + def fingerprint_type(self, value): + self['fingerprint_type'] = value + + @property + def fingerprint(self): + return self['fingerprint'] + + @fingerprint.setter + def fingerprint(self, value): + self['fingerprint'] = value @property def data(self): - return { - 'algorithm': self.algorithm, - 'fingerprint_type': self.fingerprint_type, - 'fingerprint': self.fingerprint, - } + return self def __hash__(self): return hash(self.__repr__()) @@ -1451,7 +1657,7 @@ class _ChunkedValuesMixin(ValuesMixin): return values -class _ChunkedValue(object): +class _ChunkedValue(str): _unescaped_semicolon_re = re.compile(r'\w;') @classmethod @@ -1472,7 +1678,7 @@ class _ChunkedValue(object): for v in values: if v and v[0] == '"': v = v[1:-1] - ret.append(v.replace('" "', '')) + ret.append(cls(v.replace('" "', ''))) return ret @@ -1484,7 +1690,7 @@ class SpfRecord(_ChunkedValuesMixin, Record): Record.register_type(SpfRecord) -class SrvValue(EqualityTupleMixin): +class SrvValue(EqualityTupleMixin, dict): @classmethod def validate(cls, data, _type): if not isinstance(data, (list, tuple)): @@ -1530,19 +1736,50 @@ class SrvValue(EqualityTupleMixin): return [SrvValue(v) for v in values] def __init__(self, value): - self.priority = int(value['priority']) - self.weight = int(value['weight']) - self.port = int(value['port']) - self.target = value['target'].lower() + super().__init__( + { + 'priority': int(value['priority']), + 'weight': int(value['weight']), + 'port': int(value['port']), + 'target': value['target'].lower(), + } + ) + + @property + def priority(self): + return self['priority'] + + @priority.setter + def priority(self, value): + self['priority'] = value + + @property + def weight(self): + return self['weight'] + + @weight.setter + def weight(self, value): + self['weight'] = value + + @property + def port(self): + return self['port'] + + @port.setter + def port(self, value): + self['port'] = value + + @property + def target(self): + return self['target'] + + @target.setter + def target(self, value): + self['target'] = value @property def data(self): - return { - 'priority': self.priority, - 'weight': self.weight, - 'port': self.port, - 'target': self.target, - } + return self def __hash__(self): return hash(self.__repr__()) @@ -1571,7 +1808,7 @@ class SrvRecord(ValuesMixin, Record): Record.register_type(SrvRecord) -class TlsaValue(EqualityTupleMixin): +class TlsaValue(EqualityTupleMixin, dict): @classmethod def validate(cls, data, _type): if not isinstance(data, (list, tuple)): @@ -1621,21 +1858,48 @@ class TlsaValue(EqualityTupleMixin): return [TlsaValue(v) for v in values] def __init__(self, value): - self.certificate_usage = int(value.get('certificate_usage', 0)) - self.selector = int(value.get('selector', 0)) - self.matching_type = int(value.get('matching_type', 0)) - self.certificate_association_data = value[ - 'certificate_association_data' - ] + super().__init__( + { + 'certificate_usage': int(value.get('certificate_usage', 0)), + 'selector': int(value.get('selector', 0)), + 'matching_type': int(value.get('matching_type', 0)), + 'certificate_association_data': value[ + 'certificate_association_data' + ], + } + ) @property - def data(self): - return { - 'certificate_usage': self.certificate_usage, - 'selector': self.selector, - 'matching_type': self.matching_type, - 'certificate_association_data': self.certificate_association_data, - } + def certificate_usage(self): + return self['certificate_usage'] + + @certificate_usage.setter + def certificate_usage(self, value): + self['certificate_usage'] = value + + @property + def selector(self): + return self['selector'] + + @selector.setter + def selector(self, value): + self['selector'] = value + + @property + def matching_type(self): + return self['matching_type'] + + @matching_type.setter + def matching_type(self, value): + self['matching_type'] = value + + @property + def certificate_association_data(self): + return self['certificate_association_data'] + + @certificate_association_data.setter + def certificate_association_data(self, value): + self['certificate_association_data'] = value def _equality_tuple(self): return ( @@ -1672,7 +1936,7 @@ class TxtRecord(_ChunkedValuesMixin, Record): Record.register_type(TxtRecord) -class UrlfwdValue(EqualityTupleMixin): +class UrlfwdValue(EqualityTupleMixin, dict): VALID_CODES = (301, 302) VALID_MASKS = (0, 1, 2) VALID_QUERY = (0, 1) @@ -1717,34 +1981,67 @@ class UrlfwdValue(EqualityTupleMixin): return [UrlfwdValue(v) for v in values] def __init__(self, value): - self.path = value['path'] - self.target = value['target'] - self.code = int(value['code']) - self.masking = int(value['masking']) - self.query = int(value['query']) + super().__init__( + { + 'path': value['path'], + 'target': value['target'], + 'code': int(value['code']), + 'masking': int(value['masking']), + 'query': int(value['query']), + } + ) @property - def data(self): - return { - 'path': self.path, - 'target': self.target, - 'code': self.code, - 'masking': self.masking, - 'query': self.query, - } + def path(self): + return self['path'] - def __hash__(self): - return hash(self.__repr__()) + @path.setter + def path(self, value): + self['path'] = value + + @property + def target(self): + return self['target'] + + @target.setter + def target(self, value): + self['target'] = value + + @property + def code(self): + return self['code'] + + @code.setter + def code(self, value): + self['code'] = value + + @property + def masking(self): + return self['masking'] + + @masking.setter + def masking(self, value): + self['masking'] = value + + @property + def query(self): + return self['query'] + + @query.setter + def query(self, value): + self['query'] = value def _equality_tuple(self): return (self.path, self.target, self.code, self.masking, self.query) - def __repr__(self): - return ( - f'"{self.path}" "{self.target}" {self.code} ' - f'{self.masking} {self.query}' + def __hash__(self): + return hash( + (self.path, self.target, self.code, self.masking, self.query) ) + def __repr__(self): + return f'"{self.path}" "{self.target}" {self.code} {self.masking} {self.query}' + class UrlfwdRecord(ValuesMixin, Record): _type = 'URLFWD' diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 7e41a48..a862efc 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -38,6 +38,7 @@ from octodns.record import ( SrvRecord, SrvValue, TlsaRecord, + TlsaValue, TxtRecord, Update, UrlfwdRecord, @@ -380,12 +381,14 @@ class TestRecord(TestCase): def test_caa(self): a_values = [ - {'flags': 0, 'tag': 'issue', 'value': 'ca.example.net'}, - { - 'flags': 128, - 'tag': 'iodef', - 'value': 'mailto:security@example.com', - }, + CaaValue({'flags': 0, 'tag': 'issue', 'value': 'ca.example.net'}), + CaaValue( + { + 'flags': 128, + 'tag': 'iodef', + 'value': 'mailto:security@example.com', + } + ), ] a_data = {'ttl': 30, 'values': a_values} a = CaaRecord(self.zone, 'a', a_data) @@ -400,7 +403,9 @@ class TestRecord(TestCase): self.assertEqual(a_values[1]['value'], a.values[1].value) self.assertEqual(a_data, a.data) - b_value = {'tag': 'iodef', 'value': 'http://iodef.example.com/'} + b_value = CaaValue( + {'tag': 'iodef', 'value': 'http://iodef.example.com/'} + ) b_data = {'ttl': 30, 'value': b_value} b = CaaRecord(self.zone, 'b', b_data) self.assertEqual(0, b.values[0].flags) @@ -442,20 +447,22 @@ class TestRecord(TestCase): def test_loc(self): a_values = [ - { - 'lat_degrees': 31, - 'lat_minutes': 58, - 'lat_seconds': 52.1, - 'lat_direction': 'S', - 'long_degrees': 115, - 'long_minutes': 49, - 'long_seconds': 11.7, - 'long_direction': 'E', - 'altitude': 20, - 'size': 10, - 'precision_horz': 10, - 'precision_vert': 2, - } + LocValue( + { + 'lat_degrees': 31, + 'lat_minutes': 58, + 'lat_seconds': 52.1, + 'lat_direction': 'S', + 'long_degrees': 115, + 'long_minutes': 49, + 'long_seconds': 11.7, + 'long_direction': 'E', + 'altitude': 20, + 'size': 10, + 'precision_horz': 10, + 'precision_vert': 2, + } + ) ] a_data = {'ttl': 30, 'values': a_values} a = LocRecord(self.zone, 'a', a_data) @@ -483,20 +490,22 @@ class TestRecord(TestCase): a_values[0]['precision_vert'], a.values[0].precision_vert ) - b_value = { - 'lat_degrees': 32, - 'lat_minutes': 7, - 'lat_seconds': 19, - 'lat_direction': 'S', - 'long_degrees': 116, - 'long_minutes': 2, - 'long_seconds': 25, - 'long_direction': 'E', - 'altitude': 10, - 'size': 1, - 'precision_horz': 10000, - 'precision_vert': 10, - } + b_value = LocValue( + { + 'lat_degrees': 32, + 'lat_minutes': 7, + 'lat_seconds': 19, + 'lat_direction': 'S', + 'long_degrees': 116, + 'long_minutes': 2, + 'long_seconds': 25, + 'long_direction': 'E', + 'altitude': 10, + 'size': 1, + 'precision_horz': 10000, + 'precision_vert': 10, + } + ) b_data = {'ttl': 30, 'value': b_value} b = LocRecord(self.zone, 'b', b_data) self.assertEqual(b_value['lat_degrees'], b.values[0].lat_degrees) @@ -534,8 +543,8 @@ class TestRecord(TestCase): def test_mx(self): a_values = [ - {'preference': 10, 'exchange': 'smtp1.'}, - {'priority': 20, 'value': 'smtp2.'}, + MxValue({'preference': 10, 'exchange': 'smtp1.'}), + MxValue({'priority': 20, 'value': 'smtp2.'}), ] a_data = {'ttl': 30, 'values': a_values} a = MxRecord(self.zone, 'a', a_data) @@ -544,12 +553,12 @@ class TestRecord(TestCase): self.assertEqual(30, a.ttl) self.assertEqual(a_values[0]['preference'], a.values[0].preference) self.assertEqual(a_values[0]['exchange'], a.values[0].exchange) - self.assertEqual(a_values[1]['priority'], a.values[1].preference) - self.assertEqual(a_values[1]['value'], a.values[1].exchange) - a_data['values'][1] = {'preference': 20, 'exchange': 'smtp2.'} + self.assertEqual(a_values[1]['preference'], a.values[1].preference) + self.assertEqual(a_values[1]['exchange'], a.values[1].exchange) + a_data['values'][1] = MxValue({'preference': 20, 'exchange': 'smtp2.'}) self.assertEqual(a_data, a.data) - b_value = {'preference': 0, 'exchange': 'smtp3.'} + b_value = MxValue({'preference': 0, 'exchange': 'smtp3.'}) b_data = {'ttl': 30, 'value': b_value} b = MxRecord(self.zone, 'b', b_data) self.assertEqual(b_value['preference'], b.values[0].preference) @@ -585,22 +594,26 @@ class TestRecord(TestCase): def test_naptr(self): a_values = [ - { - 'order': 10, - 'preference': 11, - 'flags': 'X', - 'service': 'Y', - 'regexp': 'Z', - 'replacement': '.', - }, - { - 'order': 20, - 'preference': 21, - 'flags': 'A', - 'service': 'B', - 'regexp': 'C', - 'replacement': 'foo.com', - }, + NaptrValue( + { + 'order': 10, + 'preference': 11, + 'flags': 'X', + 'service': 'Y', + 'regexp': 'Z', + 'replacement': '.', + } + ), + NaptrValue( + { + 'order': 20, + 'preference': 21, + 'flags': 'A', + 'service': 'B', + 'regexp': 'C', + 'replacement': 'foo.com', + } + ), ] a_data = {'ttl': 30, 'values': a_values} a = NaptrRecord(self.zone, 'a', a_data) @@ -612,14 +625,16 @@ class TestRecord(TestCase): self.assertEqual(a_values[i][k], getattr(a.values[i], k)) self.assertEqual(a_data, a.data) - b_value = { - 'order': 30, - 'preference': 31, - 'flags': 'M', - 'service': 'N', - 'regexp': 'O', - 'replacement': 'x', - } + b_value = NaptrValue( + { + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + } + ) b_data = {'ttl': 30, 'value': b_value} b = NaptrRecord(self.zone, 'b', b_data) for k in a_values[0].keys(): @@ -861,8 +876,20 @@ class TestRecord(TestCase): def test_sshfp(self): a_values = [ - {'algorithm': 10, 'fingerprint_type': 11, 'fingerprint': 'abc123'}, - {'algorithm': 20, 'fingerprint_type': 21, 'fingerprint': 'def456'}, + SshfpValue( + { + 'algorithm': 10, + 'fingerprint_type': 11, + 'fingerprint': 'abc123', + } + ), + SshfpValue( + { + 'algorithm': 20, + 'fingerprint_type': 21, + 'fingerprint': 'def456', + } + ), ] a_data = {'ttl': 30, 'values': a_values} a = SshfpRecord(self.zone, 'a', a_data) @@ -876,11 +903,9 @@ class TestRecord(TestCase): self.assertEqual(a_values[0]['fingerprint'], a.values[0].fingerprint) self.assertEqual(a_data, a.data) - b_value = { - 'algorithm': 30, - 'fingerprint_type': 31, - 'fingerprint': 'ghi789', - } + b_value = SshfpValue( + {'algorithm': 30, 'fingerprint_type': 31, 'fingerprint': 'ghi789'} + ) b_data = {'ttl': 30, 'value': b_value} b = SshfpRecord(self.zone, 'b', b_data) self.assertEqual(b_value['algorithm'], b.values[0].algorithm) @@ -924,8 +949,12 @@ class TestRecord(TestCase): def test_srv(self): a_values = [ - {'priority': 10, 'weight': 11, 'port': 12, 'target': 'server1'}, - {'priority': 20, 'weight': 21, 'port': 22, 'target': 'server2'}, + SrvValue( + {'priority': 10, 'weight': 11, 'port': 12, 'target': 'server1'} + ), + SrvValue( + {'priority': 20, 'weight': 21, 'port': 22, 'target': 'server2'} + ), ] a_data = {'ttl': 30, 'values': a_values} a = SrvRecord(self.zone, '_a._tcp', a_data) @@ -936,14 +965,21 @@ class TestRecord(TestCase): self.assertEqual(a_values[0]['weight'], a.values[0].weight) self.assertEqual(a_values[0]['port'], a.values[0].port) self.assertEqual(a_values[0]['target'], a.values[0].target) + from pprint import pprint + + pprint( + { + 'a_values': a_values, + 'self': a_data, + 'other': a.data, + 'a.values': a.values, + } + ) self.assertEqual(a_data, a.data) - b_value = { - 'priority': 30, - 'weight': 31, - 'port': 32, - 'target': 'server3', - } + b_value = SrvValue( + {'priority': 30, 'weight': 31, 'port': 32, 'target': 'server3'} + ) b_data = {'ttl': 30, 'value': b_value} b = SrvRecord(self.zone, '_b._tcp', b_data) self.assertEqual(b_value['priority'], b.values[0].priority) @@ -987,18 +1023,22 @@ class TestRecord(TestCase): def test_tlsa(self): a_values = [ - { - 'certificate_usage': 1, - 'selector': 1, - 'matching_type': 1, - 'certificate_association_data': 'ABABABABABABABABAB', - }, - { - 'certificate_usage': 2, - 'selector': 0, - 'matching_type': 2, - 'certificate_association_data': 'ABABABABABABABABAC', - }, + TlsaValue( + { + 'certificate_usage': 1, + 'selector': 1, + 'matching_type': 1, + 'certificate_association_data': 'ABABABABABABABABAB', + } + ), + TlsaValue( + { + 'certificate_usage': 2, + 'selector': 0, + 'matching_type': 2, + 'certificate_association_data': 'ABABABABABABABABAC', + } + ), ] a_data = {'ttl': 30, 'values': a_values} a = TlsaRecord(self.zone, 'a', a_data) @@ -1030,12 +1070,14 @@ class TestRecord(TestCase): ) self.assertEqual(a_data, a.data) - b_value = { - 'certificate_usage': 0, - 'selector': 0, - 'matching_type': 0, - 'certificate_association_data': 'AAAAAAAAAAAAAAA', - } + b_value = TlsaValue( + { + 'certificate_usage': 0, + 'selector': 0, + 'matching_type': 0, + 'certificate_association_data': 'AAAAAAAAAAAAAAA', + } + ) b_data = {'ttl': 30, 'value': b_value} b = TlsaRecord(self.zone, 'b', b_data) self.assertEqual( @@ -1087,20 +1129,24 @@ class TestRecord(TestCase): def test_urlfwd(self): a_values = [ - { - 'path': '/', - 'target': 'http://foo', - 'code': 301, - 'masking': 2, - 'query': 0, - }, - { - 'path': '/target', - 'target': 'http://target', - 'code': 302, - 'masking': 2, - 'query': 0, - }, + UrlfwdValue( + { + 'path': '/', + 'target': 'http://foo', + 'code': 301, + 'masking': 2, + 'query': 0, + } + ), + UrlfwdValue( + { + 'path': '/target', + 'target': 'http://target', + 'code': 302, + 'masking': 2, + 'query': 0, + } + ), ] a_data = {'ttl': 30, 'values': a_values} a = UrlfwdRecord(self.zone, 'a', a_data) @@ -1119,13 +1165,15 @@ class TestRecord(TestCase): self.assertEqual(a_values[1]['query'], a.values[1].query) self.assertEqual(a_data, a.data) - b_value = { - 'path': '/', - 'target': 'http://location', - 'code': 301, - 'masking': 2, - 'query': 0, - } + b_value = UrlfwdValue( + { + 'path': '/', + 'target': 'http://location', + 'code': 301, + 'masking': 2, + 'query': 0, + } + ) b_data = {'ttl': 30, 'value': b_value} b = UrlfwdRecord(self.zone, 'b', b_data) self.assertEqual(b_value['path'], b.values[0].path) @@ -2987,7 +3035,7 @@ class TestRecordValidation(TestCase): ) self.assertEqual('.', record.values[0].exchange) - def test_NXPTR(self): + def test_NAPTR(self): # doesn't blow up Record.new( self.zone, From 3557aac1d8cf5df140667c146005223cafdb1ddf Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 6 Sep 2022 15:17:58 -0700 Subject: [PATCH 02/14] Allow our custom yaml config to dump value types --- octodns/yaml.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/octodns/yaml.py b/octodns/yaml.py index 5b02431..bfbc3a3 100644 --- a/octodns/yaml.py +++ b/octodns/yaml.py @@ -11,6 +11,7 @@ from __future__ import ( from natsort import natsort_keygen from yaml import SafeDumper, SafeLoader, load, dump +from yaml.representer import SafeRepresenter from yaml.constructor import ConstructorError @@ -61,6 +62,10 @@ class SortingDumper(SafeDumper): SortingDumper.add_representer(dict, SortingDumper._representer) +# This should handle all the record value types which are ultimately either str +# or dict at some point in their inheritance hierarchy +SortingDumper.add_multi_representer(str, SafeRepresenter.represent_str) +SortingDumper.add_multi_representer(dict, SortingDumper._representer) def safe_dump(data, fh, **options): From 9da2c15328f9bf00df27794743145137e4b009b9 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 6 Sep 2022 15:30:00 -0700 Subject: [PATCH 03/14] full testing for missing value properties --- tests/test_octodns_record.py | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index a862efc..5521584 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -858,6 +858,30 @@ class TestRecord(TestCase): values.add(o) self.assertTrue(o in values) + self.assertEqual(30, o.order) + o.order = o.order + 1 + self.assertEqual(31, o.order) + + self.assertEqual(32, o.preference) + o.preference = o.preference + 1 + self.assertEqual(33, o.preference) + + self.assertEqual('M', o.flags) + o.flags = 'P' + self.assertEqual('P', o.flags) + + self.assertEqual('N', o.service) + o.service = 'Q' + self.assertEqual('Q', o.service) + + self.assertEqual('O', o.regexp) + o.regexp = 'R' + self.assertEqual('R', o.regexp) + + self.assertEqual('z', o.replacement) + o.replacement = '1' + self.assertEqual('1', o.replacement) + def test_ns(self): a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.'] a_data = {'ttl': 30, 'values': a_values} @@ -1695,6 +1719,54 @@ class TestRecord(TestCase): self.assertTrue(c >= c) self.assertTrue(c <= c) + self.assertEqual(31, a.lat_degrees) + a.lat_degrees = a.lat_degrees + 1 + self.assertEqual(32, a.lat_degrees) + + self.assertEqual(58, a.lat_minutes) + a.lat_minutes = a.lat_minutes + 1 + self.assertEqual(59, a.lat_minutes) + + self.assertEqual(52.1, a.lat_seconds) + a.lat_seconds = a.lat_seconds + 1 + self.assertEqual(53.1, a.lat_seconds) + + self.assertEqual('S', a.lat_direction) + a.lat_direction = 'N' + self.assertEqual('N', a.lat_direction) + + self.assertEqual(115, a.long_degrees) + a.long_degrees = a.long_degrees + 1 + self.assertEqual(116, a.long_degrees) + + self.assertEqual(49, a.long_minutes) + a.long_minutes = a.long_minutes + 1 + self.assertEqual(50, a.long_minutes) + + self.assertEqual(11.7, a.long_seconds) + a.long_seconds = a.long_seconds + 1 + self.assertEqual(12.7, a.long_seconds) + + self.assertEqual('E', a.long_direction) + a.long_direction = 'W' + self.assertEqual('W', a.long_direction) + + self.assertEqual(20, a.altitude) + a.altitude = a.altitude + 1 + self.assertEqual(21, a.altitude) + + self.assertEqual(10, a.size) + a.size = a.size + 1 + self.assertEqual(11, a.size) + + self.assertEqual(10, a.precision_horz) + a.precision_horz = a.precision_horz + 1 + self.assertEqual(11, a.precision_horz) + + self.assertEqual(2, a.precision_vert) + a.precision_vert = a.precision_vert + 1 + self.assertEqual(3, a.precision_vert) + # Hash values = set() values.add(a) From e875ca030428d9e92d0266a1f541d62abb09f60b Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 6 Sep 2022 15:30:20 -0700 Subject: [PATCH 04/14] Remove uneeded , case it was handling is now down in the cstor --- octodns/record/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index f2ee1a5..d236d55 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -832,8 +832,7 @@ class _IpAddress(str): return [cls(v) if v != '' else '' for v in values] def __new__(cls, v): - if v: - v = str(cls._address_type(v)) + v = str(cls._address_type(v)) return super().__new__(cls, v) From 5d2ef0239097c1f8ed26ccfe236b2fc16f33d3cd Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 6 Sep 2022 18:58:50 -0700 Subject: [PATCH 05/14] the *Value.data properties are no longer needed --- octodns/record/__init__.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index d236d55..c19ef7e 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -339,6 +339,10 @@ class ValuesMixin(object): def _data(self): ret = super(ValuesMixin, self)._data() if len(self.values) > 1: + # we still look for a .data here for backwards compat reasons, any + # 3rd party value types may still rely on it (they should be + # transitioned, perhaps we should deprecate, warn if it exists and + # remove it in 1.0 values = [getattr(v, 'data', v) for v in self.values if v] if len(values) > 1: ret['values'] = values @@ -347,6 +351,7 @@ class ValuesMixin(object): elif len(self.values) == 1: v = self.values[0] if v: + # see getattr note above ret['value'] = getattr(v, 'data', v) return ret @@ -431,6 +436,7 @@ class ValueMixin(object): def _data(self): ret = super(ValueMixin, self)._data() if self.value: + # see getattr note above ret['value'] = getattr(self.value, 'data', self.value) return ret @@ -941,10 +947,6 @@ class CaaValue(EqualityTupleMixin, dict): def value(self, value): self['value'] = value - @property - def data(self): - return self - def _equality_tuple(self): return (self.flags, self.tag, self.value) @@ -1196,10 +1198,6 @@ class LocValue(EqualityTupleMixin, dict): def precision_vert(self, value): self['precision_vert'] = value - @property - def data(self): - return self - def __hash__(self): return hash( ( @@ -1321,10 +1319,6 @@ class MxValue(EqualityTupleMixin, dict): def exchange(self, value): self['exchange'] = value - @property - def data(self): - return self - def __hash__(self): return hash((self.preference, self.exchange)) @@ -1442,10 +1436,6 @@ class NaptrValue(EqualityTupleMixin, dict): def replacement(self, value): self['replacement'] = value - @property - def data(self): - return self - def __hash__(self): return hash(self.__repr__()) @@ -1613,10 +1603,6 @@ class SshfpValue(EqualityTupleMixin, dict): def fingerprint(self, value): self['fingerprint'] = value - @property - def data(self): - return self - def __hash__(self): return hash(self.__repr__()) @@ -1776,10 +1762,6 @@ class SrvValue(EqualityTupleMixin, dict): def target(self, value): self['target'] = value - @property - def data(self): - return self - def __hash__(self): return hash(self.__repr__()) From d439e70419aa71bdbe701340cff4e067d07f9ce9 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 6 Sep 2022 19:07:44 -0700 Subject: [PATCH 06/14] Revert "the *Value.data properties are no longer needed" The mythicbeasts module (at least) is relying on them This reverts commit 5d2ef0239097c1f8ed26ccfe236b2fc16f33d3cd. --- octodns/record/__init__.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index c19ef7e..d236d55 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -339,10 +339,6 @@ class ValuesMixin(object): def _data(self): ret = super(ValuesMixin, self)._data() if len(self.values) > 1: - # we still look for a .data here for backwards compat reasons, any - # 3rd party value types may still rely on it (they should be - # transitioned, perhaps we should deprecate, warn if it exists and - # remove it in 1.0 values = [getattr(v, 'data', v) for v in self.values if v] if len(values) > 1: ret['values'] = values @@ -351,7 +347,6 @@ class ValuesMixin(object): elif len(self.values) == 1: v = self.values[0] if v: - # see getattr note above ret['value'] = getattr(v, 'data', v) return ret @@ -436,7 +431,6 @@ class ValueMixin(object): def _data(self): ret = super(ValueMixin, self)._data() if self.value: - # see getattr note above ret['value'] = getattr(self.value, 'data', self.value) return ret @@ -947,6 +941,10 @@ class CaaValue(EqualityTupleMixin, dict): def value(self, value): self['value'] = value + @property + def data(self): + return self + def _equality_tuple(self): return (self.flags, self.tag, self.value) @@ -1198,6 +1196,10 @@ class LocValue(EqualityTupleMixin, dict): def precision_vert(self, value): self['precision_vert'] = value + @property + def data(self): + return self + def __hash__(self): return hash( ( @@ -1319,6 +1321,10 @@ class MxValue(EqualityTupleMixin, dict): def exchange(self, value): self['exchange'] = value + @property + def data(self): + return self + def __hash__(self): return hash((self.preference, self.exchange)) @@ -1436,6 +1442,10 @@ class NaptrValue(EqualityTupleMixin, dict): def replacement(self, value): self['replacement'] = value + @property + def data(self): + return self + def __hash__(self): return hash(self.__repr__()) @@ -1603,6 +1613,10 @@ class SshfpValue(EqualityTupleMixin, dict): def fingerprint(self, value): self['fingerprint'] = value + @property + def data(self): + return self + def __hash__(self): return hash(self.__repr__()) @@ -1762,6 +1776,10 @@ class SrvValue(EqualityTupleMixin, dict): def target(self, value): self['target'] = value + @property + def data(self): + return self + def __hash__(self): return hash(self.__repr__()) From a6046e3e9d7b6f417d6ba612313faaa543828b06 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 7 Sep 2022 13:13:17 -0700 Subject: [PATCH 07/14] Remove uneeded/noop list comprehension --- octodns/record/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index d236d55..973a03a 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -326,7 +326,7 @@ class ValuesMixin(object): def __init__(self, zone, name, data, source=None): super(ValuesMixin, self).__init__(zone, name, data, source=source) try: - values = [v for v in data['values']] + values = data['values'] except KeyError: values = [data['value']] self.values = sorted(self._value_type.process(values)) From c3a5795452fc0ba4b8e71cbab071707898387ea0 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 7 Sep 2022 13:33:56 -0700 Subject: [PATCH 08/14] Clean up process funcs, always use cls --- octodns/record/__init__.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 973a03a..58169d5 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -787,10 +787,10 @@ class _TargetValue(str): return reasons @classmethod - def process(self, value): + def process(cls, value): if value: - return value.lower() - return value + return cls(value.lower()) + return None class CnameValue(_TargetValue): @@ -906,7 +906,7 @@ class CaaValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [CaaValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( @@ -1080,7 +1080,7 @@ class LocValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [LocValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( @@ -1288,7 +1288,7 @@ class MxValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [MxValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): # RFC1035 says preference, half the providers use priority @@ -1380,7 +1380,7 @@ class NaptrValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [NaptrValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( @@ -1496,7 +1496,7 @@ class _NsValue(str): @classmethod def process(cls, values): - return values + return [cls(v) for v in values] class NsRecord(ValuesMixin, Record): @@ -1525,7 +1525,8 @@ class PtrValue(_TargetValue): @classmethod def process(cls, values): - return [super(PtrValue, cls).process(v) for v in values] + supr = super() + return [supr.process(v) for v in values] class PtrRecord(ValuesMixin, Record): @@ -1578,7 +1579,7 @@ class SshfpValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [SshfpValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( @@ -1732,7 +1733,7 @@ class SrvValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [SrvValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( @@ -1854,7 +1855,7 @@ class TlsaValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [TlsaValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( From d9d3209ef6e23458fa86b4778c96ab600f799588 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 7 Sep 2022 13:51:40 -0700 Subject: [PATCH 09/14] Update octodns/record/__init__.py --- octodns/record/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 58169d5..287ebe2 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -1978,7 +1978,7 @@ class UrlfwdValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [UrlfwdValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( From faf277ca01b10c3d49b64b871213f066b972bb55 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 14 Sep 2022 13:56:27 -0700 Subject: [PATCH 10/14] IDNA support for Record values holding fqdns --- octodns/record/__init__.py | 47 ++++++--- tests/test_octodns_record.py | 184 ++++++++++++++++++++++++++++++++--- 2 files changed, 201 insertions(+), 30 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index a0c83bb..1893629 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -463,12 +463,12 @@ class ValueMixin(object): class _DynamicPool(object): log = getLogger('_DynamicPool') - def __init__(self, _id, data): + def __init__(self, _id, data, value_type): self._id = _id values = [ { - 'value': d['value'], + 'value': value_type(d['value']), 'weight': d.get('weight', 1), 'status': d.get('status', 'obey'), } @@ -745,7 +745,7 @@ class _DynamicMixin(object): pools = {} for _id, pool in sorted(pools.items()): - pools[_id] = _DynamicPool(_id, pool) + pools[_id] = _DynamicPool(_id, pool, self._value_type) # rules try: @@ -799,20 +799,24 @@ class _TargetValue(str): reasons.append('empty value') elif not data: reasons.append('missing value') - # NOTE: FQDN complains if the data it receives isn't a str, it doesn't - # allow unicode... This is likely specific to 2.7 - elif not FQDN(str(data), allow_underscores=True).is_valid: - reasons.append(f'{_type} value "{data}" is not a valid FQDN') - elif not data.endswith('.'): - reasons.append(f'{_type} value "{data}" missing trailing .') + else: + data = idna_encode(data) + if not FQDN(str(data), allow_underscores=True).is_valid: + reasons.append(f'{_type} value "{data}" is not a valid FQDN') + elif not data.endswith('.'): + reasons.append(f'{_type} value "{data}" missing trailing .') return reasons @classmethod def process(cls, value): if value: - return cls(value.lower()) + return cls(value) return None + def __new__(cls, v): + v = idna_encode(v) + return super().__new__(cls, v) + class CnameValue(_TargetValue): pass @@ -1292,7 +1296,11 @@ class MxValue(EqualityTupleMixin, dict): reasons.append(f'invalid preference "{value["preference"]}"') exchange = None try: - exchange = str(value.get('exchange', None) or value['value']) + exchange = value.get('exchange', None) or value['value'] + if not exchange: + reasons.append('missing exchange') + continue + exchange = idna_encode(exchange) if ( exchange != '.' and not FQDN(exchange, allow_underscores=True).is_valid @@ -1323,7 +1331,7 @@ class MxValue(EqualityTupleMixin, dict): except KeyError: exchange = value['value'] super().__init__( - {'preference': int(preference), 'exchange': exchange.lower()} + {'preference': int(preference), 'exchange': idna_encode(exchange)} ) @property @@ -1507,7 +1515,8 @@ class _NsValue(str): data = (data,) reasons = [] for value in data: - if not FQDN(str(value), allow_underscores=True).is_valid: + value = idna_encode(value) + if not FQDN(value, allow_underscores=True).is_valid: reasons.append( f'Invalid NS value "{value}" is not a valid FQDN.' ) @@ -1519,6 +1528,10 @@ class _NsValue(str): def process(cls, values): return [cls(v) for v in values] + def __new__(cls, v): + v = idna_encode(v) + return super().__new__(cls, v) + class NsRecord(ValuesMixin, Record): _type = 'NS' @@ -1739,11 +1752,15 @@ class SrvValue(EqualityTupleMixin, dict): reasons.append(f'invalid port "{value["port"]}"') try: target = value['target'] + if not target: + reasons.append('missing target') + continue + target = idna_encode(target) if not target.endswith('.'): reasons.append(f'SRV value "{target}" missing trailing .') if ( target != '.' - and not FQDN(str(target), allow_underscores=True).is_valid + and not FQDN(target, allow_underscores=True).is_valid ): reasons.append( f'Invalid SRV target "{target}" is not a valid FQDN.' @@ -1762,7 +1779,7 @@ class SrvValue(EqualityTupleMixin, dict): 'priority': int(value['priority']), 'weight': int(value['weight']), 'port': int(value['port']), - 'target': value['target'].lower(), + 'target': idna_encode(value['target']), } ) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 37c0180..180089a 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -23,6 +23,7 @@ from octodns.record import ( Create, Delete, GeoValue, + Ipv4Address, LocRecord, LocValue, MxRecord, @@ -101,6 +102,81 @@ class TestRecord(TestCase): self.assertTrue(f'{encoded}.{zone.name}', record.fqdn) self.assertTrue(f'{utf8}.{zone.decoded_name}', record.decoded_fqdn) + def test_utf8_values(self): + zone = Zone('unit.tests.', []) + utf8 = 'гэрбүл.mn.' + encoded = idna_encode(utf8) + + # ALIAS + record = Record.new( + zone, '', {'type': 'ALIAS', 'ttl': 300, 'value': utf8} + ) + self.assertEqual(encoded, record.value) + + # CNAME + record = Record.new( + zone, 'cname', {'type': 'CNAME', 'ttl': 300, 'value': utf8} + ) + self.assertEqual(encoded, record.value) + + # DNAME + record = Record.new( + zone, 'dname', {'type': 'DNAME', 'ttl': 300, 'value': utf8} + ) + self.assertEqual(encoded, record.value) + + # MX + record = Record.new( + zone, + 'mx', + { + 'type': 'MX', + 'ttl': 300, + 'value': {'preference': 10, 'exchange': utf8}, + }, + ) + self.assertEqual( + MxValue({'preference': 10, 'exchange': encoded}), record.values[0] + ) + + # NS + record = Record.new( + zone, 'ns', {'type': 'NS', 'ttl': 300, 'value': utf8} + ) + self.assertEqual(encoded, record.values[0]) + + # PTR + another_utf8 = 'niño.mx.' + another_encoded = idna_encode(another_utf8) + record = Record.new( + zone, + 'ptr', + {'type': 'PTR', 'ttl': 300, 'values': [utf8, another_utf8]}, + ) + self.assertEqual([encoded, another_encoded], record.values) + + # SRV + record = Record.new( + zone, + '_srv._tcp', + { + 'type': 'SRV', + 'ttl': 300, + 'value': { + 'priority': 0, + 'weight': 10, + 'port': 80, + 'target': utf8, + }, + }, + ) + self.assertEqual( + SrvValue( + {'priority': 0, 'weight': 10, 'port': 80, 'target': encoded} + ), + record.values[0], + ) + def test_alias_lowering_value(self): upper_record = AliasRecord( self.zone, @@ -1002,16 +1078,6 @@ class TestRecord(TestCase): self.assertEqual(a_values[0]['weight'], a.values[0].weight) self.assertEqual(a_values[0]['port'], a.values[0].port) self.assertEqual(a_values[0]['target'], a.values[0].target) - from pprint import pprint - - pprint( - { - 'a_values': a_values, - 'self': a_data, - 'other': a.data, - 'a.values': a.values, - } - ) self.assertEqual(a_data, a.data) b_value = SrvValue( @@ -2604,7 +2670,7 @@ class TestRecordValidation(TestCase): ) self.assertEqual(['missing value'], ctx.exception.reasons) - def test_CNAME(self): + def test_cname_validation(self): # doesn't blow up Record.new( self.zone, @@ -3153,6 +3219,19 @@ class TestRecordValidation(TestCase): ctx.exception.reasons, ) + # if exchange doesn't exist value can not be None/falsey + with self.assertRaises(ValidationError) as ctx: + Record.new( + self.zone, + '', + { + 'type': 'MX', + 'ttl': 600, + 'value': {'preference': 10, 'value': ''}, + }, + ) + self.assertEqual(['missing exchange'], ctx.exception.reasons) + # exchange can be a single `.` record = Record.new( self.zone, @@ -3648,6 +3727,24 @@ class TestRecordValidation(TestCase): ctx.exception.reasons, ) + # falsey target + with self.assertRaises(ValidationError) as ctx: + Record.new( + self.zone, + '_srv._tcp', + { + 'type': 'SRV', + 'ttl': 600, + 'value': { + 'priority': 1, + 'weight': 2, + 'port': 3, + 'target': '', + }, + }, + ) + self.assertEqual(['missing target'], ctx.exception.reasons) + # target must be a valid FQDN with self.assertRaises(ValidationError) as ctx: Record.new( @@ -5333,7 +5430,6 @@ class TestDynamicRecords(TestCase): 'pools': { 'one': {'values': [{'value': '3.3.3.3'}]}, 'two': { - # Testing out of order value sorting here 'values': [{'value': '5.5.5.5'}, {'value': '4.4.4.4'}] }, 'three': { @@ -5361,9 +5457,12 @@ class TestDynamicRecords(TestCase): ) def test_dynamic_eqs(self): - - pool_one = _DynamicPool('one', {'values': [{'value': '1.2.3.4'}]}) - pool_two = _DynamicPool('two', {'values': [{'value': '1.2.3.5'}]}) + pool_one = _DynamicPool( + 'one', {'values': [{'value': '1.2.3.4'}]}, Ipv4Address + ) + pool_two = _DynamicPool( + 'two', {'values': [{'value': '1.2.3.5'}]}, Ipv4Address + ) self.assertEqual(pool_one, pool_one) self.assertNotEqual(pool_one, pool_two) self.assertNotEqual(pool_one, 42) @@ -5382,6 +5481,61 @@ class TestDynamicRecords(TestCase): self.assertNotEqual(dynamic, other) self.assertNotEqual(dynamic, 42) + def test_dynamic_cname_idna(self): + a_utf8 = 'natación.mx.' + a_encoded = idna_encode(a_utf8) + b_utf8 = 'гэрбүл.mn.' + b_encoded = idna_encode(b_utf8) + cname_data = { + 'dynamic': { + 'pools': { + 'one': { + # Testing out of order value sorting here + 'values': [ + {'value': 'b.unit.tests.'}, + {'value': 'a.unit.tests.'}, + ] + }, + 'two': { + 'values': [ + # some utf8 values we expect to be idna encoded + {'weight': 10, 'value': a_utf8}, + {'weight': 12, 'value': b_utf8}, + ] + }, + }, + 'rules': [ + {'geos': ['NA-US-CA'], 'pool': 'two'}, + {'pool': 'one'}, + ], + }, + 'type': 'CNAME', + 'ttl': 60, + 'value': a_utf8, + } + cname = Record.new(self.zone, 'cname', cname_data) + self.assertEqual(a_encoded, cname.value) + self.assertEqual( + { + 'fallback': None, + 'values': [ + {'weight': 1, 'value': 'a.unit.tests.', 'status': 'obey'}, + {'weight': 1, 'value': 'b.unit.tests.', 'status': 'obey'}, + ], + }, + cname.dynamic.pools['one'].data, + ) + self.assertEqual( + { + 'fallback': None, + 'values': [ + {'weight': 12, 'value': b_encoded, 'status': 'obey'}, + {'weight': 10, 'value': a_encoded, 'status': 'obey'}, + ], + }, + cname.dynamic.pools['two'].data, + ) + class TestChanges(TestCase): zone = Zone('unit.tests.', []) From 247cef2701e18ef4e1db6afe3df28828da29652e Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 14 Sep 2022 14:06:20 -0700 Subject: [PATCH 11/14] CHANGELOG entry for IDNA values --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9b785..12c9bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ decoded form. Both forms should be accepted in command line arguments. Providers may need to be updated to display the decoded form in their logs, until then they'd display the IDNA version. +* IDNA value support for Record types that hold FQDNs: ALIAS, CNAME, DNAME, PTR, + MX, NS, and SRV. * Support for configuring global processors that apply to all zones with `manager.processors` From 66debc0b806de823e5fa2fdb2640a5ccf248050c Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 15 Sep 2022 14:25:47 -0700 Subject: [PATCH 12/14] Use super() now that we require python3, less error prone --- octodns/cmds/args.py | 4 +-- octodns/cmds/report.py | 6 ++-- octodns/processor/acme.py | 2 +- octodns/processor/ownership.py | 2 +- octodns/provider/base.py | 2 +- octodns/provider/plan.py | 2 +- octodns/provider/yaml.py | 4 +-- octodns/record/__init__.py | 50 ++++++++++++++--------------- octodns/source/axfr.py | 14 ++++---- octodns/source/envvar.py | 6 ++-- octodns/source/tinydns.py | 4 +-- tests/helpers.py | 4 +-- tests/test_octodns_provider_base.py | 4 +-- 13 files changed, 49 insertions(+), 55 deletions(-) diff --git a/octodns/cmds/args.py b/octodns/cmds/args.py index ac00079..170c852 100644 --- a/octodns/cmds/args.py +++ b/octodns/cmds/args.py @@ -18,7 +18,7 @@ class ArgumentParser(_Base): ''' def __init__(self, *args, **kwargs): - super(ArgumentParser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def parse_args(self, default_log_level=INFO): version = f'octoDNS {__VERSION__}' @@ -50,7 +50,7 @@ class ArgumentParser(_Base): '--debug', action='store_true', default=False, help=_help ) - args = super(ArgumentParser, self).parse_args() + args = super().parse_args() self._setup_logging(args, default_log_level) return args diff --git a/octodns/cmds/report.py b/octodns/cmds/report.py index 2bc5fa3..88bf71f 100755 --- a/octodns/cmds/report.py +++ b/octodns/cmds/report.py @@ -16,13 +16,11 @@ from octodns.manager import Manager class AsyncResolver(Resolver): def __init__(self, num_workers, *args, **kwargs): - super(AsyncResolver, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.executor = ThreadPoolExecutor(max_workers=num_workers) def query(self, *args, **kwargs): - return self.executor.submit( - super(AsyncResolver, self).query, *args, **kwargs - ) + return self.executor.submit(super().query, *args, **kwargs) def main(): diff --git a/octodns/processor/acme.py b/octodns/processor/acme.py index cab3f16..793f95a 100644 --- a/octodns/processor/acme.py +++ b/octodns/processor/acme.py @@ -25,7 +25,7 @@ class AcmeMangingProcessor(BaseProcessor): - acme ... ''' - super(AcmeMangingProcessor, self).__init__(name) + super().__init__(name) self._owned = set() diff --git a/octodns/processor/ownership.py b/octodns/processor/ownership.py index 083a583..1abbbea 100644 --- a/octodns/processor/ownership.py +++ b/octodns/processor/ownership.py @@ -15,7 +15,7 @@ from .base import BaseProcessor # and thus "own" them going forward. class OwnershipProcessor(BaseProcessor): def __init__(self, name, txt_name='_owner', txt_value='*octodns*'): - super(OwnershipProcessor, self).__init__(name) + super().__init__(name) self.txt_name = txt_name self.txt_value = txt_value self._txt_values = [txt_value] diff --git a/octodns/provider/base.py b/octodns/provider/base.py index ae9c018..65e156b 100644 --- a/octodns/provider/base.py +++ b/octodns/provider/base.py @@ -17,7 +17,7 @@ class BaseProvider(BaseSource): delete_pcent_threshold=Plan.MAX_SAFE_DELETE_PCENT, strict_supports=False, ): - super(BaseProvider, self).__init__(id) + super().__init__(id) self.log.debug( '__init__: id=%s, apply_disabled=%s, ' 'update_pcent_threshold=%.2f, ' diff --git a/octodns/provider/plan.py b/octodns/provider/plan.py index ceacb25..5e38749 100644 --- a/octodns/provider/plan.py +++ b/octodns/provider/plan.py @@ -136,7 +136,7 @@ class _PlanOutput(object): class PlanLogger(_PlanOutput): def __init__(self, name, level='info'): - super(PlanLogger, self).__init__(name) + super().__init__(name) try: self.level = { 'debug': DEBUG, diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index 3a3252b..c2e8b3f 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -128,7 +128,7 @@ class YamlProvider(BaseProvider): enforce_order, populate_should_replace, ) - super(YamlProvider, self).__init__(id, *args, **kwargs) + super().__init__(id, *args, **kwargs) self.directory = directory self.default_ttl = default_ttl self.enforce_order = enforce_order @@ -311,7 +311,7 @@ class SplitYamlProvider(YamlProvider): CATCHALL_RECORD_NAMES = ('*', '') def __init__(self, id, directory, extension='.', *args, **kwargs): - super(SplitYamlProvider, self).__init__(id, directory, *args, **kwargs) + super().__init__(id, directory, *args, **kwargs) self.extension = extension def _zone_directory(self, zone): diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index b3663fd..c4a4422 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -31,7 +31,7 @@ class Create(Change): CLASS_ORDERING = 1 def __init__(self, new): - super(Create, self).__init__(None, new) + super().__init__(None, new) def __repr__(self, leader=''): source = self.new.source.id if self.new.source else '' @@ -57,7 +57,7 @@ class Delete(Change): CLASS_ORDERING = 0 def __init__(self, existing): - super(Delete, self).__init__(existing, None) + super().__init__(existing, None) def __repr__(self, leader=''): return f'Delete {self.existing}' @@ -74,7 +74,7 @@ class ValidationError(RecordException): return f'Invalid record {idna_decode(fqdn)}\n - {reasons}' def __init__(self, fqdn, reasons): - super(Exception, self).__init__(self.build_message(fqdn, reasons)) + super().__init__(self.build_message(fqdn, reasons)) self.fqdn = fqdn self.reasons = reasons @@ -329,7 +329,7 @@ class GeoValue(EqualityTupleMixin): class ValuesMixin(object): @classmethod def validate(cls, name, fqdn, data): - reasons = super(ValuesMixin, cls).validate(name, fqdn, data) + reasons = super().validate(name, fqdn, data) values = data.get('values', data.get('value', [])) @@ -338,7 +338,7 @@ class ValuesMixin(object): return reasons def __init__(self, zone, name, data, source=None): - super(ValuesMixin, self).__init__(zone, name, data, source=source) + super().__init__(zone, name, data, source=source) try: values = data['values'] except KeyError: @@ -348,10 +348,10 @@ class ValuesMixin(object): def changes(self, other, target): if self.values != other.values: return Update(self, other) - return super(ValuesMixin, self).changes(other, target) + return super().changes(other, target) def _data(self): - ret = super(ValuesMixin, self)._data() + ret = super()._data() if len(self.values) > 1: values = [getattr(v, 'data', v) for v in self.values if v] if len(values) > 1: @@ -380,7 +380,7 @@ class _GeoMixin(ValuesMixin): @classmethod def validate(cls, name, fqdn, data): - reasons = super(_GeoMixin, cls).validate(name, fqdn, data) + reasons = super().validate(name, fqdn, data) try: geo = dict(data['geo']) for code, values in geo.items(): @@ -391,7 +391,7 @@ class _GeoMixin(ValuesMixin): return reasons def __init__(self, zone, name, data, *args, **kwargs): - super(_GeoMixin, self).__init__(zone, name, data, *args, **kwargs) + super().__init__(zone, name, data, *args, **kwargs) try: self.geo = dict(data['geo']) except KeyError: @@ -400,7 +400,7 @@ class _GeoMixin(ValuesMixin): self.geo[code] = GeoValue(code, values) def _data(self): - ret = super(_GeoMixin, self)._data() + ret = super()._data() if self.geo: geo = {} for code, value in self.geo.items(): @@ -412,7 +412,7 @@ class _GeoMixin(ValuesMixin): if target.SUPPORTS_GEO: if self.geo != other.geo: return Update(self, other) - return super(_GeoMixin, self).changes(other, target) + return super().changes(other, target) def __repr__(self): if self.geo: @@ -421,29 +421,29 @@ class _GeoMixin(ValuesMixin): f'<{klass} {self._type} {self.ttl}, {self.decoded_fqdn}, ' f'{self.values}, {self.geo}>' ) - return super(_GeoMixin, self).__repr__() + return super().__repr__() class ValueMixin(object): @classmethod def validate(cls, name, fqdn, data): - reasons = super(ValueMixin, cls).validate(name, fqdn, data) + reasons = super().validate(name, fqdn, data) reasons.extend( cls._value_type.validate(data.get('value', None), cls._type) ) return reasons def __init__(self, zone, name, data, source=None): - super(ValueMixin, self).__init__(zone, name, data, source=source) + super().__init__(zone, name, data, source=source) self.value = self._value_type.process(data['value']) def changes(self, other, target): if self.value != other.value: return Update(self, other) - return super(ValueMixin, self).changes(other, target) + return super().changes(other, target) def _data(self): - ret = super(ValueMixin, self)._data() + ret = super()._data() if self.value: ret['value'] = getattr(self.value, 'data', self.value) return ret @@ -565,7 +565,7 @@ class _DynamicMixin(object): @classmethod def validate(cls, name, fqdn, data): - reasons = super(_DynamicMixin, cls).validate(name, fqdn, data) + reasons = super().validate(name, fqdn, data) if 'dynamic' not in data: return reasons @@ -724,7 +724,7 @@ class _DynamicMixin(object): return reasons def __init__(self, zone, name, data, *args, **kwargs): - super(_DynamicMixin, self).__init__(zone, name, data, *args, **kwargs) + super().__init__(zone, name, data, *args, **kwargs) self.dynamic = {} @@ -754,7 +754,7 @@ class _DynamicMixin(object): self.dynamic = _Dynamic(pools, parsed) def _data(self): - ret = super(_DynamicMixin, self)._data() + ret = super()._data() if self.dynamic: ret['dynamic'] = self.dynamic._data() return ret @@ -763,7 +763,7 @@ class _DynamicMixin(object): if target.SUPPORTS_DYNAMIC: if self.dynamic != other.dynamic: return Update(self, other) - return super(_DynamicMixin, self).changes(other, target) + return super().changes(other, target) def __repr__(self): # TODO: improve this whole thing, we need multi-line... @@ -781,7 +781,7 @@ class _DynamicMixin(object): f'<{klass} {self._type} {self.ttl}, {self.decoded_fqdn}, ' f'{values}, {self.dynamic}>' ) - return super(_DynamicMixin, self).__repr__() + return super().__repr__() class _IpList(object): @@ -885,7 +885,7 @@ class AliasRecord(ValueMixin, Record): reasons = [] if name != '': reasons.append('non-root ALIAS not allowed') - reasons.extend(super(AliasRecord, cls).validate(name, fqdn, data)) + reasons.extend(super().validate(name, fqdn, data)) return reasons @@ -951,7 +951,7 @@ class CnameRecord(_DynamicMixin, ValueMixin, Record): reasons = [] if name == '': reasons.append('root CNAME not allowed') - reasons.extend(super(CnameRecord, cls).validate(name, fqdn, data)) + reasons.extend(super().validate(name, fqdn, data)) return reasons @@ -1352,7 +1352,7 @@ class PtrValue(_TargetValue): reasons.append('missing values') for value in values: - reasons.extend(super(PtrValue, cls).validate(value, _type)) + reasons.extend(super().validate(value, _type)) return reasons @@ -1578,7 +1578,7 @@ class SrvRecord(ValuesMixin, Record): reasons = [] if not cls._name_re.match(name): reasons.append('invalid name for SRV record') - reasons.extend(super(SrvRecord, cls).validate(name, fqdn, data)) + reasons.extend(super().validate(name, fqdn, data)) return reasons diff --git a/octodns/source/axfr.py b/octodns/source/axfr.py index 164a466..e40d7a9 100644 --- a/octodns/source/axfr.py +++ b/octodns/source/axfr.py @@ -40,7 +40,7 @@ class AxfrBaseSource(BaseSource): ) def __init__(self, id): - super(AxfrBaseSource, self).__init__(id) + super().__init__(id) def _data_for_multiple(self, _type, records): return { @@ -186,9 +186,7 @@ class AxfrSourceException(Exception): class AxfrSourceZoneTransferFailed(AxfrSourceException): def __init__(self): - super(AxfrSourceZoneTransferFailed, self).__init__( - 'Unable to Perform Zone Transfer' - ) + super().__init__('Unable to Perform Zone Transfer') class AxfrSource(AxfrBaseSource): @@ -204,7 +202,7 @@ class AxfrSource(AxfrBaseSource): def __init__(self, id, master): self.log = logging.getLogger(f'AxfrSource[{id}]') self.log.debug('__init__: id=%s, master=%s', id, master) - super(AxfrSource, self).__init__(id) + super().__init__(id) self.master = master def zone_records(self, zone): @@ -238,12 +236,12 @@ class ZoneFileSourceException(Exception): class ZoneFileSourceNotFound(ZoneFileSourceException): def __init__(self): - super(ZoneFileSourceNotFound, self).__init__('Zone file not found') + super().__init__('Zone file not found') class ZoneFileSourceLoadFailure(ZoneFileSourceException): def __init__(self, error): - super(ZoneFileSourceLoadFailure, self).__init__(str(error)) + super().__init__(str(error)) class ZoneFileSource(AxfrBaseSource): @@ -275,7 +273,7 @@ class ZoneFileSource(AxfrBaseSource): file_extension, check_origin, ) - super(ZoneFileSource, self).__init__(id) + super().__init__(id) self.directory = directory self.file_extension = file_extension self.check_origin = check_origin diff --git a/octodns/source/envvar.py b/octodns/source/envvar.py index d13c33b..6ca80df 100644 --- a/octodns/source/envvar.py +++ b/octodns/source/envvar.py @@ -11,9 +11,7 @@ class EnvVarSourceException(Exception): class EnvironmentVariableNotFoundException(EnvVarSourceException): def __init__(self, data): - super(EnvironmentVariableNotFoundException, self).__init__( - f'Unknown environment variable {data}' - ) + super().__init__(f'Unknown environment variable {data}') class EnvVarSource(BaseSource): @@ -73,7 +71,7 @@ class EnvVarSource(BaseSource): name, ttl, ) - super(EnvVarSource, self).__init__(id) + super().__init__(id) self.envvar = variable self.name = name self.ttl = ttl diff --git a/octodns/source/tinydns.py b/octodns/source/tinydns.py index 30189f7..20a03ea 100755 --- a/octodns/source/tinydns.py +++ b/octodns/source/tinydns.py @@ -23,7 +23,7 @@ class TinyDnsBaseSource(BaseSource): split_re = re.compile(r':+') def __init__(self, id, default_ttl=3600): - super(TinyDnsBaseSource, self).__init__(id) + super().__init__(id) self.default_ttl = default_ttl def _data_for_A(self, _type, records): @@ -239,7 +239,7 @@ class TinyDnsFileSource(TinyDnsBaseSource): directory, default_ttl, ) - super(TinyDnsFileSource, self).__init__(id, default_ttl) + super().__init__(id, default_ttl) self.directory = directory self._cache = None diff --git a/tests/helpers.py b/tests/helpers.py index 6efd604..ecdc2cc 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -95,7 +95,7 @@ class TemporaryDirectory(object): class WantsConfigProcessor(BaseProcessor): def __init__(self, name, some_config): - super(WantsConfigProcessor, self).__init__(name) + super().__init__(name) class PlannableProvider(BaseProvider): @@ -106,7 +106,7 @@ class PlannableProvider(BaseProvider): SUPPORTS = set(('A', 'AAAA', 'TXT')) def __init__(self, *args, **kwargs): - super(PlannableProvider, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def populate(self, zone, source=False, target=False, lenient=False): pass diff --git a/tests/test_octodns_provider_base.py b/tests/test_octodns_provider_base.py index 0a78ff7..ab3b945 100644 --- a/tests/test_octodns_provider_base.py +++ b/tests/test_octodns_provider_base.py @@ -53,7 +53,7 @@ class HelperProvider(BaseProvider): class TrickyProcessor(BaseProcessor): def __init__(self, name, add_during_process_target_zone): - super(TrickyProcessor, self).__init__(name) + super().__init__(name) self.add_during_process_target_zone = add_during_process_target_zone self.reset() @@ -640,7 +640,7 @@ class TestBaseProvider(TestCase): def __init__(self, **kwargs): self.log = MagicMock() - super(MinimalProvider, self).__init__('minimal', **kwargs) + super().__init__('minimal', **kwargs) normal = MinimalProvider(strict_supports=False) # Should log and not expect From 000541eea460bd68ce642bf3295a4f803a0b9237 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 21 Sep 2022 09:14:29 -0700 Subject: [PATCH 13/14] CHANGELOG entry for values as objects --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e835fa..84be125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ * Add TtlRestrictionFilter processor for adding ttl restriction/checking * NameAllowlistFilter & NameRejectlistFilter implementations to support filtering on record names to include/exclude records from management. +* All Record values are now first class objects. This shouldn't be an externally + visible change, but will enable future improvements. ## v0.9.19 - 2022-08-14 - Subzone handling From 9af7acfed92e0e270c2a4957d584bbfe98df07c6 Mon Sep 17 00:00:00 2001 From: Ariana Hlavaty Date: Thu, 22 Sep 2022 22:16:00 +0100 Subject: [PATCH 14/14] Update CONTRIBUTING branch name to main --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d82ff98..3f75c4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ If you have questions, or you'd like to check with us before embarking on a majo ## How to contribute -This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/). That means that the `master` branch is stable and new development is done in feature branches. Feature branches are merged into the `master` branch via a Pull Request. +This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/). That means that the `main` branch is stable and new development is done in feature branches. Feature branches are merged into the `main` branch via a Pull Request. 0. Fork and clone the repository 0. Configure and install the dependencies: `./script/bootstrap`