mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge branch 'main' into records-rfc-test
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<img src="https://raw.githubusercontent.com/octodns/octodns/master/docs/logos/octodns-logo.png?" alt="OctoDNS Logo" height=251 width=404>
|
||||
<img src="https://raw.githubusercontent.com/octodns/octodns/main/docs/logos/octodns-logo.png?" alt="OctoDNS Logo" height=251 width=404>
|
||||
|
||||
## DNS as code - Tools for managing DNS across multiple providers
|
||||
|
||||
@@ -178,7 +178,7 @@ After the reviews it's time to branch deploy the change.
|
||||
|
||||

|
||||
|
||||
If that goes smoothly, you again see the expected changes, and verify them with `dig` and/or `octodns-report` you're good to hit the merge button. If there are problems you can quickly do a `.deploy dns/master` to go back to the previous state.
|
||||
If that goes smoothly, you again see the expected changes, and verify them with `dig` and/or `octodns-report` you're good to hit the merge button. If there are problems you can quickly do a `.deploy dns/main` to go back to the previous state.
|
||||
|
||||
### Bootstrapping config files
|
||||
|
||||
@@ -378,7 +378,7 @@ If you know of any other resources, please do let us know!
|
||||
|
||||
OctoDNS is licensed under the [MIT license](LICENSE).
|
||||
|
||||
The MIT license grant is not for GitHub's trademarks, which include the logo designs. GitHub reserves all trademark and copyright rights in and to all GitHub trademarks. GitHub's logos include, for instance, the stylized designs that include "logo" in the file title in the following folder: https://github.com/octodns/octodns/tree/master/docs/logos/
|
||||
The MIT license grant is not for GitHub's trademarks, which include the logo designs. GitHub reserves all trademark and copyright rights in and to all GitHub trademarks. GitHub's logos include, for instance, the stylized designs that include "logo" in the file title in the following folder: https://github.com/octodns/octodns/tree/main/docs/logos/
|
||||
|
||||
GitHub® and its stylized versions and the Invertocat mark are GitHub's Trademarks or registered Trademarks. When using GitHub's logos, be sure to follow the GitHub logo guidelines.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -25,7 +25,7 @@ class AcmeMangingProcessor(BaseProcessor):
|
||||
- acme
|
||||
...
|
||||
'''
|
||||
super(AcmeMangingProcessor, self).__init__(name)
|
||||
super().__init__(name)
|
||||
|
||||
self._owned = set()
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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, '
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -32,7 +32,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 ''
|
||||
@@ -58,7 +58,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}'
|
||||
@@ -80,7 +80,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
|
||||
|
||||
@@ -373,7 +373,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', []))
|
||||
|
||||
@@ -390,7 +390,7 @@ class ValuesMixin(object):
|
||||
return {'ttl': rr.ttl, 'type': rr._type, 'values': values}
|
||||
|
||||
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:
|
||||
@@ -400,10 +400,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:
|
||||
@@ -441,7 +441,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():
|
||||
@@ -452,7 +452,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:
|
||||
@@ -461,7 +461,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():
|
||||
@@ -473,7 +473,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:
|
||||
@@ -482,13 +482,13 @@ 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)
|
||||
)
|
||||
@@ -505,16 +505,16 @@ class ValueMixin(object):
|
||||
}
|
||||
|
||||
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
|
||||
@@ -640,7 +640,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
|
||||
@@ -799,7 +799,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 = {}
|
||||
|
||||
@@ -829,7 +829,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
|
||||
@@ -838,7 +838,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...
|
||||
@@ -856,7 +856,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 _TargetValue(str):
|
||||
@@ -984,7 +984,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
|
||||
|
||||
|
||||
@@ -1094,7 +1094,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
|
||||
|
||||
|
||||
@@ -2136,7 +2136,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
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class AxfrBaseSource(BaseSource):
|
||||
)
|
||||
|
||||
def __init__(self, id):
|
||||
super(AxfrBaseSource, self).__init__(id)
|
||||
super().__init__(id)
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug(
|
||||
@@ -65,9 +65,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):
|
||||
@@ -83,7 +81,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):
|
||||
@@ -111,12 +109,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):
|
||||
@@ -148,7 +146,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ class Zone(object):
|
||||
self._root_ns = None
|
||||
# optional leading . to match empty hostname
|
||||
# optional trailing . b/c some sources don't have it on their fqdn
|
||||
self._name_re = re.compile(fr'\.?{name}?$')
|
||||
self._utf8_name_re = re.compile(fr'\.?{idna_decode(name)}?$')
|
||||
self._idna_name_re = re.compile(fr'\.?{self.name}?$')
|
||||
|
||||
# Copy-on-write semantics support, when `not None` this property will
|
||||
# point to a location with records for this `Zone`. Once `hydrated`
|
||||
@@ -63,7 +64,13 @@ class Zone(object):
|
||||
return self._root_ns
|
||||
|
||||
def hostname_from_fqdn(self, fqdn):
|
||||
return self._name_re.sub('', fqdn)
|
||||
try:
|
||||
fqdn.encode('ascii')
|
||||
# it's non-idna or idna encoded
|
||||
return self._idna_name_re.sub('', idna_encode(fqdn))
|
||||
except UnicodeEncodeError:
|
||||
# it has utf8 chars
|
||||
return self._utf8_name_re.sub('', fqdn)
|
||||
|
||||
def add_record(self, record, replace=False, lenient=False):
|
||||
if self._origin:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,10 +47,16 @@ class TestZone(TestCase):
|
||||
('foo.bar', 'foo.bar.unit.tests'),
|
||||
('foo.unit.tests', 'foo.unit.tests.unit.tests.'),
|
||||
('foo.unit.tests', 'foo.unit.tests.unit.tests'),
|
||||
# if we pass utf8 we get utf8
|
||||
('déjà', 'déjà.unit.tests'),
|
||||
('déjà.foo', 'déjà.foo.unit.tests'),
|
||||
('bar.déjà', 'bar.déjà.unit.tests'),
|
||||
('bar.déjà.foo', 'bar.déjà.foo.unit.tests'),
|
||||
# if we pass idna we get idna
|
||||
('xn--dj-kia8a', 'xn--dj-kia8a.unit.tests'),
|
||||
('xn--dj-kia8a.foo', 'xn--dj-kia8a.foo.unit.tests'),
|
||||
('bar.xn--dj-kia8a', 'bar.xn--dj-kia8a.unit.tests'),
|
||||
('bar.xn--dj-kia8a.foo', 'bar.xn--dj-kia8a.foo.unit.tests'),
|
||||
):
|
||||
self.assertEqual(hostname, zone.hostname_from_fqdn(fqdn))
|
||||
|
||||
@@ -68,6 +74,10 @@ class TestZone(TestCase):
|
||||
('déjà.foo', 'déjà.foo.grüßen.de'),
|
||||
('bar.déjà', 'bar.déjà.grüßen.de'),
|
||||
('bar.déjà.foo', 'bar.déjà.foo.grüßen.de'),
|
||||
('xn--dj-kia8a', 'xn--dj-kia8a.xn--gren-wna7o.de'),
|
||||
('xn--dj-kia8a.foo', 'xn--dj-kia8a.foo.xn--gren-wna7o.de'),
|
||||
('bar.xn--dj-kia8a', 'bar.xn--dj-kia8a.xn--gren-wna7o.de'),
|
||||
('bar.xn--dj-kia8a.foo', 'bar.xn--dj-kia8a.foo.xn--gren-wna7o.de'),
|
||||
):
|
||||
self.assertEqual(hostname, zone.hostname_from_fqdn(fqdn))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user