1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

Implement black formatting

This commit is contained in:
Ross McFarland
2022-07-04 12:27:39 -07:00
parent 392d8b516f
commit e116d26eec
101 changed files with 6403 additions and 5490 deletions

View File

@ -8,4 +8,5 @@ ROOT=$(dirname "$GIT")
. "$ROOT/env/bin/activate"
"$ROOT/script/lint"
"$ROOT/script/format" --check --quiet || (echo "Formatting check failed, run ./script/format" && exit 1)
"$ROOT/script/coverage"

View File

@ -1,6 +1,10 @@
'OctoDNS: DNS as code - Tools for managing DNS across multiple providers'
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
__VERSION__ = '0.9.17'

View File

@ -2,5 +2,9 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)

View File

@ -2,12 +2,15 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from argparse import ArgumentParser as _Base
from logging import DEBUG, INFO, WARN, Formatter, StreamHandler, \
getLogger
from logging import DEBUG, INFO, WARN, Formatter, StreamHandler, getLogger
from logging.handlers import SysLogHandler
from sys import stderr, stdout
@ -26,22 +29,33 @@ class ArgumentParser(_Base):
def parse_args(self, default_log_level=INFO):
version = f'octoDNS {__VERSION__}'
self.add_argument('--version', action='version', version=version,
help='Print octoDNS version and exit')
self.add_argument('--log-stream-stdout', action='store_true',
default=False,
help='Log to stdout instead of stderr')
self.add_argument(
'--version',
action='version',
version=version,
help='Print octoDNS version and exit',
)
self.add_argument(
'--log-stream-stdout',
action='store_true',
default=False,
help='Log to stdout instead of stderr',
)
_help = 'Send logging data to syslog in addition to stderr'
self.add_argument('--log-syslog', action='store_true', default=False,
help=_help)
self.add_argument('--syslog-device', default='/dev/log',
help='Syslog device')
self.add_argument('--syslog-facility', default='local0',
help='Syslog facility')
self.add_argument(
'--log-syslog', action='store_true', default=False, help=_help
)
self.add_argument(
'--syslog-device', default='/dev/log', help='Syslog device'
)
self.add_argument(
'--syslog-facility', default='local0', help='Syslog facility'
)
_help = 'Increase verbosity to get details and help track down issues'
self.add_argument('--debug', action='store_true', default=False,
help=_help)
self.add_argument(
'--debug', action='store_true', default=False, help=_help
)
args = super(ArgumentParser, self).parse_args()
self._setup_logging(args, default_log_level)
@ -57,10 +71,13 @@ class ArgumentParser(_Base):
logger.addHandler(handler)
if args.log_syslog:
fmt = 'octodns[%(process)-5s:%(thread)d]: %(name)s ' \
fmt = (
'octodns[%(process)-5s:%(thread)d]: %(name)s '
'%(levelname)-5s %(message)s'
handler = SysLogHandler(address=args.syslog_device,
facility=args.syslog_facility)
)
handler = SysLogHandler(
address=args.syslog_device, facility=args.syslog_facility
)
handler.setFormatter(Formatter(fmt=fmt))
logger.addHandler(handler)

View File

@ -3,8 +3,12 @@
Octo-DNS Comparator
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from pprint import pprint
import sys
@ -16,16 +20,32 @@ from octodns.manager import Manager
def main():
parser = ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('--config-file', required=True,
help='The Manager configuration file to use')
parser.add_argument('--a', nargs='+', required=True,
help='First source(s) to pull data from')
parser.add_argument('--b', nargs='+', required=True,
help='Second source(s) to pull data from')
parser.add_argument('--zone', default=None, required=True,
help='Zone to compare')
parser.add_argument('--ignore-prefix', default=None, required=False,
help='Record prefix to ignore from list of changes')
parser.add_argument(
'--config-file',
required=True,
help='The Manager configuration file to use',
)
parser.add_argument(
'--a',
nargs='+',
required=True,
help='First source(s) to pull data from',
)
parser.add_argument(
'--b',
nargs='+',
required=True,
help='Second source(s) to pull data from',
)
parser.add_argument(
'--zone', default=None, required=True, help='Zone to compare'
)
parser.add_argument(
'--ignore-prefix',
default=None,
required=False,
help='Record prefix to ignore from list of changes',
)
args = parser.parse_args()
manager = Manager(args.config_file)
@ -34,8 +54,7 @@ def main():
# Filter changes list based on ignore-prefix argument if present
if args.ignore_prefix:
pattern = args.ignore_prefix
changes = [c for c in changes
if not c.record.fqdn.startswith(pattern)]
changes = [c for c in changes if not c.record.fqdn.startswith(pattern)]
pprint(changes)

View File

@ -3,8 +3,12 @@
Octo-DNS Dumper
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from octodns.cmds.args import ArgumentParser
from octodns.manager import Manager
@ -13,26 +17,38 @@ from octodns.manager import Manager
def main():
parser = ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('--config-file', required=True,
help='The Manager configuration file to use')
parser.add_argument('--output-dir', required=True,
help='The directory into which the results will be '
'written (Note: will overwrite existing files)')
parser.add_argument('--lenient', action='store_true', default=False,
help='Ignore record validations and do a best effort '
'dump')
parser.add_argument('--split', action='store_true', default=False,
help='Split the dumped zone into a YAML file per '
'record')
parser.add_argument(
'--config-file',
required=True,
help='The Manager configuration file to use',
)
parser.add_argument(
'--output-dir',
required=True,
help='The directory into which the results will be '
'written (Note: will overwrite existing files)',
)
parser.add_argument(
'--lenient',
action='store_true',
default=False,
help='Ignore record validations and do a best effort ' 'dump',
)
parser.add_argument(
'--split',
action='store_true',
default=False,
help='Split the dumped zone into a YAML file per ' 'record',
)
parser.add_argument('zone', help='Zone to dump')
parser.add_argument('source', nargs='+',
help='Source(s) to pull data from')
parser.add_argument('source', nargs='+', help='Source(s) to pull data from')
args = parser.parse_args()
manager = Manager(args.config_file)
manager.dump(args.zone, args.output_dir, args.lenient, args.split,
*args.source)
manager.dump(
args.zone, args.output_dir, args.lenient, args.split, *args.source
)
if __name__ == '__main__':

View File

@ -3,8 +3,12 @@
Octo-DNS Reporter
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from concurrent.futures import ThreadPoolExecutor
from dns.exception import Timeout
@ -18,28 +22,38 @@ from octodns.manager import Manager
class AsyncResolver(Resolver):
def __init__(self, num_workers, *args, **kwargs):
super(AsyncResolver, self).__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(AsyncResolver, self).query, *args, **kwargs
)
def main():
parser = ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('--config-file', required=True,
help='The Manager configuration file to use')
parser.add_argument(
'--config-file',
required=True,
help='The Manager configuration file to use',
)
parser.add_argument('--zone', required=True, help='Zone to dump')
parser.add_argument('--source', required=True, default=[], action='append',
help='Source(s) to pull data from')
parser.add_argument('--num-workers', default=4,
help='Number of background workers')
parser.add_argument('--timeout', default=1,
help='Number seconds to wait for an answer')
parser.add_argument(
'--source',
required=True,
default=[],
action='append',
help='Source(s) to pull data from',
)
parser.add_argument(
'--num-workers', default=4, help='Number of background workers'
)
parser.add_argument(
'--timeout', default=1, help='Number seconds to wait for an answer'
)
parser.add_argument('server', nargs='+', help='Servers to query')
args = parser.parse_args()
@ -62,8 +76,9 @@ def main():
resolvers = []
ip_addr_re = re.compile(r'^[\d\.]+$')
for server in args.server:
resolver = AsyncResolver(configure=False,
num_workers=int(args.num_workers))
resolver = AsyncResolver(
configure=False, num_workers=int(args.num_workers)
)
if not ip_addr_re.match(server):
server = str(query(server, 'A')[0])
log.info('server=%s', server)
@ -73,8 +88,9 @@ def main():
queries = {}
for record in sorted(zone.records):
queries[record] = [r.query(record.fqdn, record._type)
for r in resolvers]
queries[record] = [
r.query(record.fqdn, record._type) for r in resolvers
]
for record, futures in sorted(queries.items(), key=lambda d: d[0]):
stdout.write(record.fqdn)

View File

@ -3,8 +3,12 @@
Octo-DNS Multiplexer
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from octodns.cmds.args import ArgumentParser
from octodns.manager import Manager
@ -13,31 +17,57 @@ from octodns.manager import Manager
def main():
parser = ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('--config-file', required=True,
help='The Manager configuration file to use')
parser.add_argument('--doit', action='store_true', default=False,
help='Whether to take action or just show what would '
'change')
parser.add_argument('--force', action='store_true', default=False,
help='Acknowledge that significant changes are being '
'made and do them')
parser.add_argument(
'--config-file',
required=True,
help='The Manager configuration file to use',
)
parser.add_argument(
'--doit',
action='store_true',
default=False,
help='Whether to take action or just show what would ' 'change',
)
parser.add_argument(
'--force',
action='store_true',
default=False,
help='Acknowledge that significant changes are being '
'made and do them',
)
parser.add_argument('zone', nargs='*', default=[],
help='Limit sync to the specified zone(s)')
parser.add_argument(
'zone',
nargs='*',
default=[],
help='Limit sync to the specified zone(s)',
)
parser.add_argument('--source', default=[], action='append',
help='Limit sync to zones with the specified '
'source(s) (all sources will be synchronized for the '
'selected zones)')
parser.add_argument('--target', default=[], action='append',
help='Limit sync to the specified target(s)')
parser.add_argument(
'--source',
default=[],
action='append',
help='Limit sync to zones with the specified '
'source(s) (all sources will be synchronized for the '
'selected zones)',
)
parser.add_argument(
'--target',
default=[],
action='append',
help='Limit sync to the specified target(s)',
)
args = parser.parse_args()
manager = Manager(args.config_file)
manager.sync(eligible_zones=args.zone, eligible_sources=args.source,
eligible_targets=args.target, dry_run=not args.doit,
force=args.force)
manager.sync(
eligible_zones=args.zone,
eligible_sources=args.source,
eligible_targets=args.target,
dry_run=not args.doit,
force=args.force,
)
if __name__ == '__main__':

View File

@ -3,8 +3,12 @@
Octo-DNS Validator
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import WARN
@ -15,8 +19,11 @@ from octodns.manager import Manager
def main():
parser = ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('--config-file', required=True,
help='The Manager configuration file to use')
parser.add_argument(
'--config-file',
required=True,
help='The Manager configuration file to use',
)
args = parser.parse_args(WARN)

View File

@ -3,8 +3,12 @@
octoDNS Versions
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from octodns.cmds.args import ArgumentParser
from octodns.manager import Manager
@ -13,8 +17,11 @@ from octodns.manager import Manager
def main():
parser = ArgumentParser(description=__doc__.split('\n')[1])
parser.add_argument('--config-file', required=True,
help='The Manager configuration file to use')
parser.add_argument(
'--config-file',
required=True,
help='The Manager configuration file to use',
)
args = parser.parse_args()

View File

@ -2,12 +2,15 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
class EqualityTupleMixin(object):
def _equality_tuple(self):
raise NotImplementedError('_equality_tuple method not implemented')

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from concurrent.futures import ThreadPoolExecutor
from importlib import import_module
@ -22,9 +26,12 @@ from .zone import Zone
# TODO: this can go away once we no longer support python 3.7
try:
from importlib.metadata import PackageNotFoundError, \
version as module_version
from importlib.metadata import (
PackageNotFoundError,
version as module_version,
)
except ModuleNotFoundError: # pragma: no cover
class PackageNotFoundError(Exception):
pass
@ -61,7 +68,6 @@ class _AggregateTarget(object):
class MakeThreadFuture(object):
def __init__(self, func, args, kwargs):
self.func = func
self.args = args
@ -98,24 +104,29 @@ class Manager(object):
def __init__(self, config_file, max_workers=None, include_meta=False):
version = self._try_version('octodns', version=__VERSION__)
self.log.info('__init__: config_file=%s (octoDNS %s)', config_file,
version)
self.log.info(
'__init__: config_file=%s (octoDNS %s)', config_file, version
)
# Read our config file
with open(config_file, 'r') as fh:
self.config = safe_load(fh, enforce_order=False)
manager_config = self.config.get('manager', {})
max_workers = manager_config.get('max_workers', 1) \
if max_workers is None else max_workers
max_workers = (
manager_config.get('max_workers', 1)
if max_workers is None
else max_workers
)
self.log.info('__init__: max_workers=%d', max_workers)
if max_workers > 1:
self._executor = ThreadPoolExecutor(max_workers=max_workers)
else:
self._executor = MainThreadExecutor()
self.include_meta = include_meta or manager_config.get('include_meta',
False)
self.include_meta = include_meta or manager_config.get(
'include_meta', False
)
self.log.info('__init__: include_meta=%s', self.include_meta)
self.log.debug('__init__: configuring providers')
@ -126,87 +137,112 @@ class Manager(object):
_class = provider_config.pop('class')
except KeyError:
self.log.exception('Invalid provider class')
raise ManagerException(f'Provider {provider_name} is missing '
'class')
raise ManagerException(
f'Provider {provider_name} is missing ' 'class'
)
_class, module, version = self._get_named_class('provider', _class)
kwargs = self._build_kwargs(provider_config)
try:
self.providers[provider_name] = _class(provider_name, **kwargs)
self.log.info('__init__: provider=%s (%s %s)', provider_name,
module, version)
self.log.info(
'__init__: provider=%s (%s %s)',
provider_name,
module,
version,
)
except TypeError:
self.log.exception('Invalid provider config')
raise ManagerException('Incorrect provider config for ' +
provider_name)
raise ManagerException(
'Incorrect provider config for ' + provider_name
)
self.processors = {}
for processor_name, processor_config in \
self.config.get('processors', {}).items():
for processor_name, processor_config in self.config.get(
'processors', {}
).items():
try:
_class = processor_config.pop('class')
except KeyError:
self.log.exception('Invalid processor class')
raise ManagerException(f'Processor {processor_name} is '
'missing class')
_class, module, version = self._get_named_class('processor',
_class)
raise ManagerException(
f'Processor {processor_name} is ' 'missing class'
)
_class, module, version = self._get_named_class('processor', _class)
kwargs = self._build_kwargs(processor_config)
try:
self.processors[processor_name] = _class(processor_name,
**kwargs)
self.log.info('__init__: processor=%s (%s %s)', processor_name,
module, version)
self.processors[processor_name] = _class(
processor_name, **kwargs
)
self.log.info(
'__init__: processor=%s (%s %s)',
processor_name,
module,
version,
)
except TypeError:
self.log.exception('Invalid processor config')
raise ManagerException('Incorrect processor config for ' +
processor_name)
raise ManagerException(
'Incorrect processor config for ' + processor_name
)
zone_tree = {}
# Sort so we iterate on the deepest nodes first, ensuring if a parent
# zone exists it will be seen after the subzone, thus we can easily
# reparent children to their parent zone from the tree root.
for name in sorted(self.config['zones'].keys(),
key=lambda s: 0 - s.count('.')):
for name in sorted(
self.config['zones'].keys(), key=lambda s: 0 - s.count('.')
):
# Trim the trailing dot from FQDN
name = name[:-1]
this = {}
for sz in [k for k in zone_tree.keys() if k.endswith(name)]:
# Found a zone in tree root that is our child, slice the
# name and move its tree under ours.
this[sz[:-(len(name) + 1)]] = zone_tree.pop(sz)
this[sz[: -(len(name) + 1)]] = zone_tree.pop(sz)
# Add to tree root where it will be reparented as we iterate up
# the tree.
zone_tree[name] = this
self.zone_tree = zone_tree
self.plan_outputs = {}
plan_outputs = manager_config.get('plan_outputs', {
'_logger': {
'class': 'octodns.provider.plan.PlanLogger',
'level': 'info'
}
})
plan_outputs = manager_config.get(
'plan_outputs',
{
'_logger': {
'class': 'octodns.provider.plan.PlanLogger',
'level': 'info',
}
},
)
for plan_output_name, plan_output_config in plan_outputs.items():
try:
_class = plan_output_config.pop('class')
except KeyError:
self.log.exception('Invalid plan_output class')
raise ManagerException(f'plan_output {plan_output_name} is '
'missing class')
_class, module, version = self._get_named_class('plan_output',
_class)
raise ManagerException(
f'plan_output {plan_output_name} is ' 'missing class'
)
_class, module, version = self._get_named_class(
'plan_output', _class
)
kwargs = self._build_kwargs(plan_output_config)
try:
self.plan_outputs[plan_output_name] = \
_class(plan_output_name, **kwargs)
self.plan_outputs[plan_output_name] = _class(
plan_output_name, **kwargs
)
# Don't print out version info for the default output
if plan_output_name != '_logger':
self.log.info('__init__: plan_output=%s (%s %s)',
plan_output_name, module, version)
self.log.info(
'__init__: plan_output=%s (%s %s)',
plan_output_name,
module,
version,
)
except TypeError:
self.log.exception('Invalid plan_output config')
raise ManagerException('Incorrect plan_output config for ' +
plan_output_name)
raise ManagerException(
'Incorrect plan_output config for ' + plan_output_name
)
def _try_version(self, module_name, module=None, version=None):
try:
@ -241,15 +277,19 @@ class Manager(object):
module_name, class_name = _class.rsplit('.', 1)
module, version = self._import_module(module_name)
except (ImportError, ValueError):
self.log.exception('_get_{}_class: Unable to import '
'module %s', _class)
self.log.exception(
'_get_{}_class: Unable to import ' 'module %s', _class
)
raise ManagerException(f'Unknown {_type} class: {_class}')
try:
return getattr(module, class_name), module_name, version
except AttributeError:
self.log.exception('_get_{}_class: Unable to get class %s '
'from module %s', class_name, module)
self.log.exception(
'_get_{}_class: Unable to get class %s ' 'from module %s',
class_name,
module,
)
raise ManagerException(f'Unknown {_type} class: {_class}')
def _build_kwargs(self, source):
@ -263,8 +303,10 @@ class Manager(object):
v = environ[env_var]
except KeyError:
self.log.exception('Invalid provider config')
raise ManagerException('Incorrect provider config, '
'missing env var ' + env_var)
raise ManagerException(
'Incorrect provider config, '
'missing env var ' + env_var
)
except AttributeError:
pass
kwargs[k] = v
@ -283,20 +325,27 @@ class Manager(object):
# Move down the tree and slice name to get the remainder for the
# next round of the search.
where = where[parent]
name = name[:-(len(parent) + 1)]
name = name[: -(len(parent) + 1)]
# `where` is now pointing at the dictionary of children for zone_name
# in the tree
sub_zone_names = where.keys()
self.log.debug('configured_sub_zones: subs=%s', sub_zone_names)
return set(sub_zone_names)
def _populate_and_plan(self, zone_name, processors, sources, targets,
desired=None, lenient=False):
def _populate_and_plan(
self,
zone_name,
processors,
sources,
targets,
desired=None,
lenient=False,
):
self.log.debug('sync: populating, zone=%s, lenient=%s',
zone_name, lenient)
zone = Zone(zone_name,
sub_zones=self.configured_sub_zones(zone_name))
self.log.debug(
'sync: populating, zone=%s, lenient=%s', zone_name, lenient
)
zone = Zone(zone_name, sub_zones=self.configured_sub_zones(zone_name))
if desired:
# This is an alias zone, rather than populate it we'll copy the
@ -309,11 +358,12 @@ class Manager(object):
try:
source.populate(zone, lenient=lenient)
except TypeError as e:
if ("unexpected keyword argument 'lenient'"
not in str(e)):
if "unexpected keyword argument 'lenient'" not in str(e):
raise
self.log.warning('provider %s does not accept lenient '
'param', source.__class__.__name__)
self.log.warning(
'provider %s does not accept lenient ' 'param',
source.__class__.__name__,
)
source.populate(zone)
for processor in processors:
@ -324,38 +374,56 @@ class Manager(object):
for target in targets:
if self.include_meta:
meta = Record.new(zone, 'octodns-meta', {
'type': 'TXT',
'ttl': 60,
'value': f'provider={target.id}',
})
meta = Record.new(
zone,
'octodns-meta',
{
'type': 'TXT',
'ttl': 60,
'value': f'provider={target.id}',
},
)
zone.add_record(meta, replace=True)
try:
plan = target.plan(zone, processors=processors)
except TypeError as e:
if "keyword argument 'processors'" not in str(e):
raise
self.log.warning('provider.plan %s does not accept processors '
'param', target.__class__.__name__)
self.log.warning(
'provider.plan %s does not accept processors ' 'param',
target.__class__.__name__,
)
plan = target.plan(zone)
for processor in processors:
plan = processor.process_plan(plan, sources=sources,
target=target)
plan = processor.process_plan(
plan, sources=sources, target=target
)
if plan:
plans.append((target, plan))
# Return the zone as it's the desired state
return plans, zone
def sync(self, eligible_zones=[], eligible_sources=[], eligible_targets=[],
dry_run=True, force=False, plan_output_fh=stdout):
def sync(
self,
eligible_zones=[],
eligible_sources=[],
eligible_targets=[],
dry_run=True,
force=False,
plan_output_fh=stdout,
):
self.log.info(
'sync: eligible_zones=%s, eligible_targets=%s, dry_run=%s, '
'force=%s, plan_output_fh=%s',
eligible_zones, eligible_targets, dry_run, force,
getattr(plan_output_fh, 'name', plan_output_fh.__class__.__name__))
eligible_zones,
eligible_targets,
dry_run,
force,
getattr(plan_output_fh, 'name', plan_output_fh.__class__.__name__),
)
zones = self.config['zones'].items()
if eligible_zones:
@ -370,19 +438,27 @@ class Manager(object):
# Check that the source zone is defined.
if source_zone not in self.config['zones']:
self.log.error(f'Invalid alias zone {zone_name}, '
f'target {source_zone} does not exist')
raise ManagerException(f'Invalid alias zone {zone_name}: '
f'source zone {source_zone} does '
'not exist')
self.log.error(
f'Invalid alias zone {zone_name}, '
f'target {source_zone} does not exist'
)
raise ManagerException(
f'Invalid alias zone {zone_name}: '
f'source zone {source_zone} does '
'not exist'
)
# Check that the source zone is not an alias zone itself.
if 'alias' in self.config['zones'][source_zone]:
self.log.error(f'Invalid alias zone {zone_name}, '
f'target {source_zone} is an alias zone')
raise ManagerException(f'Invalid alias zone {zone_name}: '
f'source zone {source_zone} is an '
'alias zone')
self.log.error(
f'Invalid alias zone {zone_name}, '
f'target {source_zone} is an alias zone'
)
raise ManagerException(
f'Invalid alias zone {zone_name}: '
f'source zone {source_zone} is an '
'alias zone'
)
aliased_zones[zone_name] = source_zone
continue
@ -400,8 +476,9 @@ class Manager(object):
processors = config.get('processors', [])
if (eligible_sources and not
[s for s in sources if s in eligible_sources]):
if eligible_sources and not [
s for s in sources if s in eligible_sources
]:
self.log.info('sync: no eligible sources, skipping')
continue
@ -423,8 +500,9 @@ class Manager(object):
collected.append(self.processors[processor])
processors = collected
except KeyError:
raise ManagerException(f'Zone {zone_name}, unknown '
f'processor: {processor}')
raise ManagerException(
f'Zone {zone_name}, unknown ' f'processor: {processor}'
)
try:
# rather than using a list comprehension, we break this loop
@ -435,26 +513,35 @@ class Manager(object):
collected.append(self.providers[source])
sources = collected
except KeyError:
raise ManagerException(f'Zone {zone_name}, unknown '
f'source: {source}')
raise ManagerException(
f'Zone {zone_name}, unknown ' f'source: {source}'
)
try:
trgs = []
for target in targets:
trg = self.providers[target]
if not isinstance(trg, BaseProvider):
raise ManagerException(f'{trg} - "{target}" does not '
'support targeting')
raise ManagerException(
f'{trg} - "{target}" does not ' 'support targeting'
)
trgs.append(trg)
targets = trgs
except KeyError:
raise ManagerException(f'Zone {zone_name}, unknown '
f'target: {target}')
raise ManagerException(
f'Zone {zone_name}, unknown ' f'target: {target}'
)
futures.append(self._executor.submit(self._populate_and_plan,
zone_name, processors,
sources, targets,
lenient=lenient))
futures.append(
self._executor.submit(
self._populate_and_plan,
zone_name,
processors,
sources,
targets,
lenient=lenient,
)
)
# Wait on all results and unpack/flatten the plans and store the
# desired states in case we need them below
@ -473,18 +560,22 @@ class Manager(object):
try:
desired_config = desired[zone_source]
except KeyError:
raise ManagerException(f'Zone {zone_name} cannot be sync '
f'without zone {zone_source} sinced '
'it is aliased')
futures.append(self._executor.submit(
self._populate_and_plan,
zone_name,
processors,
[],
[self.providers[t] for t in source_config['targets']],
desired=desired_config,
lenient=lenient
))
raise ManagerException(
f'Zone {zone_name} cannot be sync '
f'without zone {zone_source} sinced '
'it is aliased'
)
futures.append(
self._executor.submit(
self._populate_and_plan,
zone_name,
processors,
[],
[self.providers[t] for t in source_config['targets']],
desired=desired_config,
lenient=lenient,
)
)
# Wait on results and unpack/flatten the plans, ignore the desired here
# as these are aliased zones
@ -514,8 +605,9 @@ class Manager(object):
for target, plan in plans:
zone_name = plan.existing.name
if zones[zone_name].get('always-dry-run', False):
self.log.info('sync: zone=%s skipping always-dry-run',
zone_name)
self.log.info(
'sync: zone=%s skipping always-dry-run', zone_name
)
continue
total_changes += target.apply(plan)
@ -583,15 +675,19 @@ class Manager(object):
if source_zone:
if source_zone not in self.config['zones']:
self.log.exception('Invalid alias zone')
raise ManagerException(f'Invalid alias zone {zone_name}: '
f'source zone {source_zone} does '
'not exist')
raise ManagerException(
f'Invalid alias zone {zone_name}: '
f'source zone {source_zone} does '
'not exist'
)
if 'alias' in self.config['zones'][source_zone]:
self.log.exception('Invalid alias zone')
raise ManagerException(f'Invalid alias zone {zone_name}: '
'source zone {source_zone} is an '
'alias zone')
raise ManagerException(
f'Invalid alias zone {zone_name}: '
'source zone {source_zone} is an '
'alias zone'
)
# this is just here to satisfy coverage, see
# https://github.com/nedbat/coveragepy/issues/198
@ -613,8 +709,9 @@ class Manager(object):
collected.append(self.providers[source])
sources = collected
except KeyError:
raise ManagerException(f'Zone {zone_name}, unknown source: ' +
source)
raise ManagerException(
f'Zone {zone_name}, unknown source: ' + source
)
for source in sources:
if isinstance(source, YamlProvider):
@ -627,13 +724,15 @@ class Manager(object):
for processor in processors:
collected.append(self.processors[processor])
except KeyError:
raise ManagerException(f'Zone {zone_name}, unknown '
f'processor: {processor}')
raise ManagerException(
f'Zone {zone_name}, unknown ' f'processor: {processor}'
)
def get_zone(self, zone_name):
if not zone_name[-1] == '.':
raise ManagerException(f'Invalid zone name {zone_name}, missing '
'ending dot')
raise ManagerException(
f'Invalid zone name {zone_name}, missing ' 'ending dot'
)
for name, config in self.config['zones'].items():
if name == zone_name:

View File

@ -2,5 +2,9 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
@ -34,8 +38,9 @@ class AcmeMangingProcessor(BaseProcessor):
def process_source_zone(self, desired, *args, **kwargs):
for record in desired.records:
if record._type == 'TXT' and \
record.name.startswith('_acme-challenge'):
if record._type == 'TXT' and record.name.startswith(
'_acme-challenge'
):
# We have a managed acme challenge record (owned by octoDNS) so
# we should mark it as such
record = record.copy()
@ -51,10 +56,12 @@ class AcmeMangingProcessor(BaseProcessor):
for record in existing.records:
# Uses a startswith rather than == to ignore subdomain challenges,
# e.g. _acme-challenge.foo.domain.com when managing domain.com
if record._type == 'TXT' and \
record.name.startswith('_acme-challenge') and \
'*octoDNS*' not in record.values and \
record not in self._owned:
if (
record._type == 'TXT'
and record.name.startswith('_acme-challenge')
and '*octoDNS*' not in record.values
and record not in self._owned
):
self.log.info('_process: ignoring %s', record.fqdn)
existing.remove_record(record)

View File

@ -2,21 +2,30 @@
# Ignores AWS ACM validation CNAME records.
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Route53')
try:
logger.warning('octodns_route53 shimmed. Update your processor class to '
'octodns_route53.processor.AwsAcmMangingProcessor. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_route53 shimmed. Update your processor class to '
'octodns_route53.processor.AwsAcmMangingProcessor. '
'Shim will be removed in 1.0'
)
from octodns_route53.processor import AwsAcmMangingProcessor
AwsAcmMangingProcessor # pragma: no cover
except ModuleNotFoundError:
logger.exception('AwsAcmMangingProcessor has been moved into a separate '
'module, octodns_route53 is now required. Processor '
'class should be updated to '
'octodns_route53.processor.AwsAcmMangingProcessor')
logger.exception(
'AwsAcmMangingProcessor has been moved into a separate '
'module, octodns_route53 is now required. Processor '
'class should be updated to '
'octodns_route53.processor.AwsAcmMangingProcessor'
)
raise

View File

@ -2,12 +2,15 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
class BaseProcessor(object):
def __init__(self, name):
self.name = name

View File

@ -2,14 +2,17 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from .base import BaseProcessor
class TypeAllowlistFilter(BaseProcessor):
def __init__(self, name, allowlist):
super(TypeAllowlistFilter, self).__init__(name)
self.allowlist = set(allowlist)
@ -26,7 +29,6 @@ class TypeAllowlistFilter(BaseProcessor):
class TypeRejectlistFilter(BaseProcessor):
def __init__(self, name, rejectlist):
super(TypeRejectlistFilter, self).__init__(name)
self.rejectlist = set(rejectlist)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from collections import defaultdict
@ -17,7 +21,6 @@ from .base import BaseProcessor
# delete. We'll take ownership of existing records that we're told to manage
# and thus "own" them going forward.
class OwnershipProcessor(BaseProcessor):
def __init__(self, name, txt_name='_owner', txt_value='*octodns*'):
super(OwnershipProcessor, self).__init__(name)
self.txt_name = txt_name
@ -32,19 +35,21 @@ class OwnershipProcessor(BaseProcessor):
name = f'{self.txt_name}.{record._type}.{record_name}'
else:
name = f'{self.txt_name}.{record._type}'
txt = Record.new(desired, name, {
'type': 'TXT',
'ttl': 60,
'value': self.txt_value,
})
txt = Record.new(
desired,
name,
{'type': 'TXT', 'ttl': 60, 'value': self.txt_value},
)
desired.add_record(txt)
return desired
def _is_ownership(self, record):
return record._type == 'TXT' and \
record.name.startswith(self.txt_name) \
return (
record._type == 'TXT'
and record.name.startswith(self.txt_name)
and record.values == self._txt_values
)
def process_plan(self, plan, *args, **kwargs):
if not plan:
@ -80,9 +85,11 @@ class OwnershipProcessor(BaseProcessor):
for change in plan.changes:
record = change.record
if not self._is_ownership(record) and \
record._type not in owned[record.name] and \
record.name != 'octodns-meta':
if (
not self._is_ownership(record)
and record._type not in owned[record.name]
and record.name != 'octodns-meta'
):
# It's not an ownership TXT, it's not owned, and it's not
# special we're going to ignore it
continue
@ -92,8 +99,13 @@ class OwnershipProcessor(BaseProcessor):
filtered_changes.append(change)
if plan.changes != filtered_changes:
return Plan(plan.existing, plan.desired, filtered_changes,
plan.exists, plan.update_pcent_threshold,
plan.delete_pcent_threshold)
return Plan(
plan.existing,
plan.desired,
filtered_changes,
plan.exists,
plan.update_pcent_threshold,
plan.delete_pcent_threshold,
)
return plan

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
class ProviderException(Exception):

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Azure')
try:
logger.warning('octodns_azure shimmed. Update your provider class to '
'octodns_azure.AzureProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_azure shimmed. Update your provider class to '
'octodns_azure.AzureProvider. '
'Shim will be removed in 1.0'
)
from octodns_azure import AzureProvider
AzureProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('AzureProvider has been moved into a separate module, '
'octodns_azure is now required. Provider class should '
'be updated to octodns_azure.AzureProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'AzureProvider has been moved into a separate module, '
'octodns_azure is now required. Provider class should '
'be updated to octodns_azure.AzureProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from ..source.base import BaseSource
from ..zone import Zone
@ -12,19 +16,24 @@ from . import SupportsException
class BaseProvider(BaseSource):
def __init__(self, id, apply_disabled=False,
update_pcent_threshold=Plan.MAX_SAFE_UPDATE_PCENT,
delete_pcent_threshold=Plan.MAX_SAFE_DELETE_PCENT,
strict_supports=False):
def __init__(
self,
id,
apply_disabled=False,
update_pcent_threshold=Plan.MAX_SAFE_UPDATE_PCENT,
delete_pcent_threshold=Plan.MAX_SAFE_DELETE_PCENT,
strict_supports=False,
):
super(BaseProvider, self).__init__(id)
self.log.debug('__init__: id=%s, apply_disabled=%s, '
'update_pcent_threshold=%.2f, '
'delete_pcent_threshold=%.2f',
id,
apply_disabled,
update_pcent_threshold,
delete_pcent_threshold)
self.log.debug(
'__init__: id=%s, apply_disabled=%s, '
'update_pcent_threshold=%.2f, '
'delete_pcent_threshold=%.2f',
id,
apply_disabled,
update_pcent_threshold,
delete_pcent_threshold,
)
self.apply_disabled = apply_disabled
self.update_pcent_threshold = update_pcent_threshold
self.delete_pcent_threshold = delete_pcent_threshold
@ -68,8 +77,10 @@ class BaseProvider(BaseSource):
if not unsupported_pools:
continue
unsupported_pools = ','.join(unsupported_pools)
msg = f'"status" flag used in pools {unsupported_pools}' \
msg = (
f'"status" flag used in pools {unsupported_pools}'
f' in {record.fqdn} is not supported'
)
fallback = 'will ignore it and respect the healthcheck'
self.supports_warn_or_except(msg, fallback)
record = record.copy()
@ -84,11 +95,13 @@ class BaseProvider(BaseSource):
record = record.copy()
record.dynamic = None
desired.add_record(record, replace=True)
elif record._type == 'PTR' and len(record.values) > 1 and \
not self.SUPPORTS_MULTIVALUE_PTR:
elif (
record._type == 'PTR'
and len(record.values) > 1
and not self.SUPPORTS_MULTIVALUE_PTR
):
# replace with a single-value copy
msg = \
f'multi-value PTR records not supported for {record.fqdn}'
msg = f'multi-value PTR records not supported for {record.fqdn}'
fallback = f'falling back to single value, {record.value}'
self.supports_warn_or_except(msg, fallback)
record = record.copy()
@ -98,13 +111,15 @@ class BaseProvider(BaseSource):
record = desired.root_ns
if self.SUPPORTS_ROOT_NS:
if not record:
self.log.warning('root NS record supported, but no record '
'is configured for %s', desired.name)
self.log.warning(
'root NS record supported, but no record '
'is configured for %s',
desired.name,
)
else:
if record:
# we can't manage root NS records, get rid of it
msg = \
f'root NS record not supported for {record.fqdn}'
msg = f'root NS record not supported for {record.fqdn}'
fallback = 'ignoring it'
self.supports_warn_or_except(msg, fallback)
desired.remove_record(record)
@ -132,10 +147,13 @@ class BaseProvider(BaseSource):
'''
existing_root_ns = existing.root_ns
if existing_root_ns and (not self.SUPPORTS_ROOT_NS or
not desired.root_ns):
self.log.info('root NS record in existing, but not supported or '
'not configured; ignoring it')
if existing_root_ns and (
not self.SUPPORTS_ROOT_NS or not desired.root_ns
):
self.log.info(
'root NS record in existing, but not supported or '
'not configured; ignoring it'
)
existing.remove_record(existing_root_ns)
return existing
@ -168,8 +186,10 @@ class BaseProvider(BaseSource):
if exists is None:
# If your code gets this warning see Source.populate for more
# information
self.log.warning('Provider %s used in target mode did not return '
'exists', self.id)
self.log.warning(
'Provider %s used in target mode did not return ' 'exists',
self.id,
)
# Make a (shallow) copy of the desired state so that everything from
# now on (in this target) can modify it as they see fit without
@ -194,17 +214,25 @@ class BaseProvider(BaseSource):
self.log.info('plan: filtered out %s changes', before - after)
# allow the provider to add extra changes it needs
extra = self._extra_changes(existing=existing, desired=desired,
changes=changes)
extra = self._extra_changes(
existing=existing, desired=desired, changes=changes
)
if extra:
self.log.info('plan: extra changes\n %s', '\n '
.join([str(c) for c in extra]))
self.log.info(
'plan: extra changes\n %s',
'\n '.join([str(c) for c in extra]),
)
changes += extra
if changes:
plan = Plan(existing, desired, changes, exists,
self.update_pcent_threshold,
self.delete_pcent_threshold)
plan = Plan(
existing,
desired,
changes,
exists,
self.update_pcent_threshold,
self.delete_pcent_threshold,
)
self.log.info('plan: %s', plan)
return plan
self.log.info('plan: No changes')
@ -221,11 +249,11 @@ class BaseProvider(BaseSource):
zone_name = plan.desired.name
num_changes = len(plan.changes)
self.log.info('apply: making %d changes to %s', num_changes,
zone_name)
self.log.info('apply: making %d changes to %s', num_changes, zone_name)
self._apply(plan)
return len(plan.changes)
def _apply(self, plan):
raise NotImplementedError('Abstract base class, _apply method '
'missing')
raise NotImplementedError(
'Abstract base class, _apply method ' 'missing'
)

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Cloudflare')
try:
logger.warning('octodns_cloudflare shimmed. Update your provider class to '
'octodns_cloudflare.CloudflareProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_cloudflare shimmed. Update your provider class to '
'octodns_cloudflare.CloudflareProvider. '
'Shim will be removed in 1.0'
)
from octodns_cloudflare import CloudflareProvider
CloudflareProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('CloudflareProvider has been moved into a separate '
'module, octodns_cloudflare is now required. Provider '
'class should be updated to '
'octodns_cloudflare.CloudflareProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'CloudflareProvider has been moved into a separate '
'module, octodns_cloudflare is now required. Provider '
'class should be updated to '
'octodns_cloudflare.CloudflareProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Constellix')
try:
logger.warning('octodns_constellix shimmed. Update your provider class to '
'octodns_constellix.ConstellixProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_constellix shimmed. Update your provider class to '
'octodns_constellix.ConstellixProvider. '
'Shim will be removed in 1.0'
)
from octodns_constellix import ConstellixProvider
ConstellixProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('ConstellixProvider has been moved into a separate '
'module, octodns_constellix is now required. Provider '
'class should be updated to '
'octodns_constellix.ConstellixProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'ConstellixProvider has been moved into a separate '
'module, octodns_constellix is now required. Provider '
'class should be updated to '
'octodns_constellix.ConstellixProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('DigitalOcean')
try:
logger.warning('octodns_digitalocean shimmed. Update your provider class '
'to octodns_digitalocean.DigitalOceanProvider. Shim will '
'be removed in 1.0')
logger.warning(
'octodns_digitalocean shimmed. Update your provider class '
'to octodns_digitalocean.DigitalOceanProvider. Shim will '
'be removed in 1.0'
)
from octodns_digitalocean import DigitalOceanProvider
DigitalOceanProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('DigitalOceanProvider has been moved into a separate '
'module, octodns_digitalocean is now required. Provider '
'class should be updated to '
'octodns_digitalocean.DigitalOceanProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'DigitalOceanProvider has been moved into a separate '
'module, octodns_digitalocean is now required. Provider '
'class should be updated to '
'octodns_digitalocean.DigitalOceanProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Dnsimple')
try:
logger.warning('octodns_dnsimple shimmed. Update your provider class to '
'octodns_dnsimple.DnsimpleProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_dnsimple shimmed. Update your provider class to '
'octodns_dnsimple.DnsimpleProvider. '
'Shim will be removed in 1.0'
)
from octodns_dnsimple import DnsimpleProvider
DnsimpleProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('DnsimpleProvider has been moved into a separate module, '
'octodns_dnsimple is now required. Provider class should '
'be updated to octodns_dnsimple.DnsimpleProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'DnsimpleProvider has been moved into a separate module, '
'octodns_dnsimple is now required. Provider class should '
'be updated to octodns_dnsimple.DnsimpleProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('DnsMadeEasy')
try:
logger.warning('octodns_dnsmadeeasy shimmed. Update your provider class '
'to octodns_dnsmadeeasy.DnsMadeEasyProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_dnsmadeeasy shimmed. Update your provider class '
'to octodns_dnsmadeeasy.DnsMadeEasyProvider. '
'Shim will be removed in 1.0'
)
from octodns_dnsmadeeasy import DnsMadeEasyProvider
DnsMadeEasyProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('DnsMadeEasyProvider has been moved into a separate '
'module, octodns_dnsmadeeasy is now required. Provider '
'class should be updated to '
'octodns_dnsmadeeasy.DnsMadeEasyProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'DnsMadeEasyProvider has been moved into a separate '
'module, octodns_dnsmadeeasy is now required. Provider '
'class should be updated to '
'octodns_dnsmadeeasy.DnsMadeEasyProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Dyn')
try:
logger.warning('octodns_dyn shimmed. Update your provider class to '
'octodns_dyn.DynProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_dyn shimmed. Update your provider class to '
'octodns_dyn.DynProvider. '
'Shim will be removed in 1.0'
)
from octodns_dyn import DynProvider
DynProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('DynProvider has been moved into a separate module, '
'octodns_dyn is now required. Provider class should '
'be updated to octodns_dyn.DynProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'DynProvider has been moved into a separate module, '
'octodns_dyn is now required. Provider class should '
'be updated to octodns_dyn.DynProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('EasyDns')
try:
logger.warning('octodns_easydns shimmed. Update your provider class to '
'octodns_easydns.EasyDnsProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_easydns shimmed. Update your provider class to '
'octodns_easydns.EasyDnsProvider. '
'Shim will be removed in 1.0'
)
from octodns_easydns import EasyDnsProvider, EasyDNSProvider
EasyDnsProvider # pragma: no cover
EasyDNSProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('EasyDNSProvider has been moved into a separate module, '
'octodns_easydns is now required. Provider class should '
'be updated to octodns_easydns.EasyDnsProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'EasyDNSProvider has been moved into a separate module, '
'octodns_easydns is now required. Provider class should '
'be updated to octodns_easydns.EasyDnsProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Akamai')
try:
logger.warning('octodns_edgedns shimmed. Update your provider class to '
'octodns_edgedns.AkamaiProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_edgedns shimmed. Update your provider class to '
'octodns_edgedns.AkamaiProvider. '
'Shim will be removed in 1.0'
)
from octodns_edgedns import AkamaiProvider
AkamaiProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('AkamaiProvider has been moved into a separate module, '
'octodns_edgedns is now required. Provider class should '
'be updated to octodns_edgedns.AkamaiProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'AkamaiProvider has been moved into a separate module, '
'octodns_edgedns is now required. Provider class should '
'be updated to octodns_edgedns.AkamaiProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('EtcHosts')
try:
logger.warning('octodns_etchosts shimmed. Update your provider class to '
'octodns_etchosts.EtcHostsProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_etchosts shimmed. Update your provider class to '
'octodns_etchosts.EtcHostsProvider. '
'Shim will be removed in 1.0'
)
from octodns_etchosts import EtcHostsProvider
EtcHostsProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('EtcHostsProvider has been moved into a separate module, '
'octodns_etchosts is now required. Provider class should '
'be updated to octodns_etchosts.EtcHostsProvider. See '
'See https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'EtcHostsProvider has been moved into a separate module, '
'octodns_etchosts is now required. Provider class should '
'be updated to octodns_etchosts.EtcHostsProvider. See '
'See https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,14 +2,20 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Akamai')
logger.warning('AkamaiProvider has been moved into a separate module, '
'octodns_edgedns is now required. Provider class should '
'be updated to octodns_edgedns.AkamaiProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.warning(
'AkamaiProvider has been moved into a separate module, '
'octodns_edgedns is now required. Provider class should '
'be updated to octodns_edgedns.AkamaiProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Gandi')
try:
logger.warning('octodns_gandi shimmed. Update your provider class to '
'octodns_gandi.GandiProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_gandi shimmed. Update your provider class to '
'octodns_gandi.GandiProvider. '
'Shim will be removed in 1.0'
)
from octodns_gandi import GandiProvider
GandiProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('GandiProvider has been moved into a separate module, '
'octodns_gandi is now required. Provider class should '
'be updated to octodns_gandi.GandiProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'GandiProvider has been moved into a separate module, '
'octodns_gandi is now required. Provider class should '
'be updated to octodns_gandi.GandiProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('GCore')
try:
logger.warning('octodns_gcore shimmed. Update your provider class to '
'octodns_gcore.GCoreProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_gcore shimmed. Update your provider class to '
'octodns_gcore.GCoreProvider. '
'Shim will be removed in 1.0'
)
from octodns_gcore import GCoreProvider
GCoreProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('GCoreProvider has been moved into a separate module, '
'octodns_gcore is now required. Provider class should '
'be updated to octodns_gcore.GCoreProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'GCoreProvider has been moved into a separate module, '
'octodns_gcore is now required. Provider class should '
'be updated to octodns_gcore.GCoreProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('GoogleCloud')
try:
logger.warning('octodns_googlecloud shimmed. Update your provider class '
'to octodns_googlecloud.GoogleCloudProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_googlecloud shimmed. Update your provider class '
'to octodns_googlecloud.GoogleCloudProvider. '
'Shim will be removed in 1.0'
)
from octodns_googlecloud import GoogleCloudProvider
GoogleCloudProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('GoogleCloudProvider has been moved into a separate '
'module, octodns_googlecloud is now required. Provider '
'class should be updated to '
'octodns_googlecloud.GoogleCloudProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'GoogleCloudProvider has been moved into a separate '
'module, octodns_googlecloud is now required. Provider '
'class should be updated to '
'octodns_googlecloud.GoogleCloudProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Hetzner')
try:
logger.warning('octodns_hetzner shimmed. Update your provider class to '
'octodns_hetzner.HetznerProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_hetzner shimmed. Update your provider class to '
'octodns_hetzner.HetznerProvider. '
'Shim will be removed in 1.0'
)
from octodns_hetzner import HetznerProvider
HetznerProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('HetznerProvider has been moved into a separate module, '
'octodns_hetzner is now required. Provider class should '
'be updated to octodns_hetzner.HetznerProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'HetznerProvider has been moved into a separate module, '
'octodns_hetzner is now required. Provider class should '
'be updated to octodns_hetzner.HetznerProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('MythicBeasts')
try:
logger.warning('octodns_mythicbeasts shimmed. Update your provider class '
'to octodns_mythicbeasts.MythicBeastsProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_mythicbeasts shimmed. Update your provider class '
'to octodns_mythicbeasts.MythicBeastsProvider. '
'Shim will be removed in 1.0'
)
from octodns_mythicbeasts import MythicBeastsProvider
MythicBeastsProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('MythicBeastsProvider has been moved into a separate '
'module, octodns_mythicbeasts is now required. Provider '
'class should be updated to '
'octodns_mythicbeasts.MythicBeastsProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'MythicBeastsProvider has been moved into a separate '
'module, octodns_mythicbeasts is now required. Provider '
'class should be updated to '
'octodns_mythicbeasts.MythicBeastsProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Ns1')
try:
logger.warning('octodns_ns1 shimmed. Update your provider class to '
'octodns_ns1.Ns1Provider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_ns1 shimmed. Update your provider class to '
'octodns_ns1.Ns1Provider. '
'Shim will be removed in 1.0'
)
from octodns_ns1 import Ns1Provider
Ns1Provider # pragma: no cover
except ModuleNotFoundError:
logger.exception('Ns1Provider has been moved into a separate module, '
'octodns_ns1 is now required. Provider class should '
'be updated to octodns_ns1.Ns1Provider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'Ns1Provider has been moved into a separate module, '
'octodns_ns1 is now required. Provider class should '
'be updated to octodns_ns1.Ns1Provider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Ovh')
try:
logger.warning('octodns_ovh shimmed. Update your provider class to '
'octodns_ovh.OvhProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_ovh shimmed. Update your provider class to '
'octodns_ovh.OvhProvider. '
'Shim will be removed in 1.0'
)
from octodns_ovh import OvhProvider
OvhProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('OvhProvider has been moved into a separate module, '
'octodns_ovh is now required. Provider class should '
'be updated to octodns_ovh.OvhProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'OvhProvider has been moved into a separate module, '
'octodns_ovh is now required. Provider class should '
'be updated to octodns_ovh.OvhProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import DEBUG, ERROR, INFO, WARN, getLogger
from sys import stdout
@ -16,30 +20,37 @@ class UnsafePlan(Exception):
class RootNsChange(UnsafePlan):
def __init__(self):
super().__init__('Root NS record change, force required')
class TooMuchChange(UnsafePlan):
def __init__(self, why, update_pcent, update_threshold, change_count,
existing_count):
msg = f'{why}, {update_pcent:.2f}% is over {update_threshold:.2f}% ' \
def __init__(
self, why, update_pcent, update_threshold, change_count, existing_count
):
msg = (
f'{why}, {update_pcent:.2f}% is over {update_threshold:.2f}% '
f'({change_count}/{existing_count}), force required'
)
super().__init__(msg)
class Plan(object):
log = getLogger('Plan')
MAX_SAFE_UPDATE_PCENT = .3
MAX_SAFE_DELETE_PCENT = .3
MAX_SAFE_UPDATE_PCENT = 0.3
MAX_SAFE_DELETE_PCENT = 0.3
MIN_EXISTING_RECORDS = 10
def __init__(self, existing, desired, changes, exists,
update_pcent_threshold=MAX_SAFE_UPDATE_PCENT,
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT):
def __init__(
self,
existing,
desired,
changes,
exists,
update_pcent_threshold=MAX_SAFE_UPDATE_PCENT,
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT,
):
self.existing = existing
self.desired = desired
# Sort changes to ensure we always have a consistent ordering for
@ -51,11 +62,7 @@ class Plan(object):
self.update_pcent_threshold = update_pcent_threshold
self.delete_pcent_threshold = delete_pcent_threshold
change_counts = {
'Create': 0,
'Delete': 0,
'Update': 0
}
change_counts = {'Create': 0, 'Delete': 0, 'Update': 0}
for change in changes:
change_counts[change.__class__.__name__] += 1
self.change_counts = change_counts
@ -65,43 +72,57 @@ class Plan(object):
except AttributeError:
existing_n = 0
self.log.debug('__init__: Creates=%d, Updates=%d, Deletes=%d '
'Existing=%d',
self.change_counts['Create'],
self.change_counts['Update'],
self.change_counts['Delete'], existing_n)
self.log.debug(
'__init__: Creates=%d, Updates=%d, Deletes=%d ' 'Existing=%d',
self.change_counts['Create'],
self.change_counts['Update'],
self.change_counts['Delete'],
existing_n,
)
def raise_if_unsafe(self):
# TODO: what is safe really?
if self.existing and \
len(self.existing.records) >= self.MIN_EXISTING_RECORDS:
if (
self.existing
and len(self.existing.records) >= self.MIN_EXISTING_RECORDS
):
existing_record_count = len(self.existing.records)
if existing_record_count > 0:
update_pcent = (self.change_counts['Update'] /
existing_record_count)
delete_pcent = (self.change_counts['Delete'] /
existing_record_count)
update_pcent = (
self.change_counts['Update'] / existing_record_count
)
delete_pcent = (
self.change_counts['Delete'] / existing_record_count
)
else:
update_pcent = 0
delete_pcent = 0
if update_pcent > self.update_pcent_threshold:
raise TooMuchChange('Too many updates', update_pcent * 100,
self.update_pcent_threshold * 100,
self.change_counts['Update'],
existing_record_count)
raise TooMuchChange(
'Too many updates',
update_pcent * 100,
self.update_pcent_threshold * 100,
self.change_counts['Update'],
existing_record_count,
)
if delete_pcent > self.delete_pcent_threshold:
raise TooMuchChange('Too many deletes', delete_pcent * 100,
self.delete_pcent_threshold * 100,
self.change_counts['Delete'],
existing_record_count)
raise TooMuchChange(
'Too many deletes',
delete_pcent * 100,
self.delete_pcent_threshold * 100,
self.change_counts['Delete'],
existing_record_count,
)
# If we have any changes of the root NS record for the zone it's a huge
# deal and force should always be required for extra care
if self.exists and any(c for c in self.changes
if c.record and c.record._type == 'NS' and
c.record.name == ''):
if self.exists and any(
c
for c in self.changes
if c.record and c.record._type == 'NS' and c.record.name == ''
):
raise RootNsChange()
def __repr__(self):
@ -109,18 +130,18 @@ class Plan(object):
updates = self.change_counts['Update']
deletes = self.change_counts['Delete']
existing = len(self.existing.records)
return f'Creates={creates}, Updates={updates}, Deletes={deletes}, ' \
return (
f'Creates={creates}, Updates={updates}, Deletes={deletes}, '
f'Existing Records={existing}'
)
class _PlanOutput(object):
def __init__(self, name):
self.name = name
class PlanLogger(_PlanOutput):
def __init__(self, name, level='info'):
super(PlanLogger, self).__init__(name)
try:
@ -129,14 +150,16 @@ class PlanLogger(_PlanOutput):
'info': INFO,
'warn': WARN,
'warning': WARN,
'error': ERROR
'error': ERROR,
}[level.lower()]
except (AttributeError, KeyError):
raise Exception(f'Unsupported level: {level}')
def run(self, log, plans, *args, **kwargs):
hr = '*************************************************************' \
hr = (
'*************************************************************'
'*******************\n'
)
buf = StringIO()
buf.write('\n')
if plans:
@ -188,7 +211,6 @@ def _value_stringifier(record, sep):
class PlanMarkdown(_PlanOutput):
def run(self, plans, fh=stdout, *args, **kwargs):
if plans:
current_zone = None
@ -203,8 +225,10 @@ class PlanMarkdown(_PlanOutput):
fh.write(target.id)
fh.write('\n\n')
fh.write('| Operation | Name | Type | TTL | Value | Source |\n'
'|--|--|--|--|--|--|\n')
fh.write(
'| Operation | Name | Type | TTL | Value | Source |\n'
'|--|--|--|--|--|--|\n'
)
if plan.exists is False:
fh.write('| Create | ')
@ -248,7 +272,6 @@ class PlanMarkdown(_PlanOutput):
class PlanHtml(_PlanOutput):
def run(self, plans, fh=stdout, *args, **kwargs):
if plans:
current_zone = None
@ -261,7 +284,8 @@ class PlanHtml(_PlanOutput):
fh.write('<h3>')
fh.write(target.id)
fh.write('''</h3>
fh.write(
'''</h3>
<table>
<tr>
<th>Operation</th>
@ -271,7 +295,8 @@ class PlanHtml(_PlanOutput):
<th>Value</th>
<th>Source</th>
</tr>
''')
'''
)
if plan.exists is False:
fh.write(' <tr>\n <td>Create</td>\n <td colspan=5>')

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('PowerDns')
try:
logger.warning('octodns_powerdns shimmed. Update your provider class to '
'octodns_powerdns.PowerDnsProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_powerdns shimmed. Update your provider class to '
'octodns_powerdns.PowerDnsProvider. '
'Shim will be removed in 1.0'
)
from octodns_powerdns import PowerDnsProvider, PowerDnsBaseProvider
PowerDnsProvider # pragma: no cover
PowerDnsBaseProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('PowerDnsProvider has been moved into a separate module, '
'octodns_powerdns is now required. Provider class should '
'be updated to octodns_powerdns.PowerDnsProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'PowerDnsProvider has been moved into a separate module, '
'octodns_powerdns is now required. Provider class should '
'be updated to octodns_powerdns.PowerDnsProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,23 +2,32 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Rackspace')
try:
logger.warning('octodns_rackspace shimmed. Update your provider class to '
'octodns_rackspace.RackspaceProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_rackspace shimmed. Update your provider class to '
'octodns_rackspace.RackspaceProvider. '
'Shim will be removed in 1.0'
)
from octodns_rackspace import RackspaceProvider
RackspaceProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('RackspaceProvider has been moved into a separate '
'module, octodns_rackspace is now required. Provider '
'class should be updated to '
'octodns_rackspace.RackspaceProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'RackspaceProvider has been moved into a separate '
'module, octodns_rackspace is now required. Provider '
'class should be updated to '
'octodns_rackspace.RackspaceProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Route53')
try:
logger.warning('octodns_route53 shimmed. Update your provider class to '
'octodns_route53.Route53Provider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_route53 shimmed. Update your provider class to '
'octodns_route53.Route53Provider. '
'Shim will be removed in 1.0'
)
from octodns_route53 import Route53Provider
Route53Provider # pragma: no cover
except ModuleNotFoundError:
logger.exception('Route53Provider has been moved into a separate module, '
'octodns_route53 is now required. Provider class should '
'be updated to octodns_route53.Route53Provider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'Route53Provider has been moved into a separate module, '
'octodns_route53 is now required. Provider class should '
'be updated to octodns_route53.Route53Provider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Selectel')
try:
logger.warning('octodns_selectel shimmed. Update your provider class to '
'octodns_selectel.SelectelProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_selectel shimmed. Update your provider class to '
'octodns_selectel.SelectelProvider. '
'Shim will be removed in 1.0'
)
from octodns_selectel import SelectelProvider
SelectelProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('SelectelProvider has been moved into a separate module, '
'octodns_selectel is now required. Provider class should '
'be updated to octodns_selectel.SelectelProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'SelectelProvider has been moved into a separate module, '
'octodns_selectel is now required. Provider class should '
'be updated to octodns_selectel.SelectelProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Transip')
try:
logger.warning('octodns_transip shimmed. Update your provider class to '
'octodns_transip.TransipProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_transip shimmed. Update your provider class to '
'octodns_transip.TransipProvider. '
'Shim will be removed in 1.0'
)
from octodns_transip import TransipProvider
TransipProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('TransipProvider has been moved into a separate module, '
'octodns_transip is now required. Provider class should '
'be updated to octodns_transip.TransipProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'TransipProvider has been moved into a separate module, '
'octodns_transip is now required. Provider class should '
'be updated to octodns_transip.TransipProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,22 +2,31 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
logger = getLogger('Ultra')
try:
logger.warning('octodns_ultra shimmed. Update your provider class to '
'octodns_ultra.UltraProvider. '
'Shim will be removed in 1.0')
logger.warning(
'octodns_ultra shimmed. Update your provider class to '
'octodns_ultra.UltraProvider. '
'Shim will be removed in 1.0'
)
from octodns_ultra import UltraProvider
UltraProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('UltraProvider has been moved into a separate module, '
'octodns_ultra is now required. Provider class should '
'be updated to octodns_ultra.UltraProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.')
logger.exception(
'UltraProvider has been moved into a separate module, '
'octodns_ultra is now required. Provider class should '
'be updated to octodns_ultra.UltraProvider. See '
'https://github.com/octodns/octodns#updating-'
'to-use-extracted-providers for more information.'
)
raise

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from collections import defaultdict
from os import listdir, makedirs
@ -102,23 +106,54 @@ class YamlProvider(BaseProvider):
`--config-file=internal.yaml`
'''
SUPPORTS_GEO = True
SUPPORTS_DYNAMIC = True
SUPPORTS_POOL_VALUE_STATUS = True
SUPPORTS_MULTIVALUE_PTR = True
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'DNAME', 'LOC', 'MX',
'NAPTR', 'NS', 'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT',
'URLFWD'))
SUPPORTS = set(
(
'A',
'AAAA',
'ALIAS',
'CAA',
'CNAME',
'DNAME',
'LOC',
'MX',
'NAPTR',
'NS',
'PTR',
'SSHFP',
'SPF',
'SRV',
'TXT',
'URLFWD',
)
)
def __init__(self, id, directory, default_ttl=3600, enforce_order=True,
populate_should_replace=False, supports_root_ns=True,
*args, **kwargs):
def __init__(
self,
id,
directory,
default_ttl=3600,
enforce_order=True,
populate_should_replace=False,
supports_root_ns=True,
*args,
**kwargs,
):
klass = self.__class__.__name__
self.log = logging.getLogger(f'{klass}[{id}]')
self.log.debug('__init__: id=%s, directory=%s, default_ttl=%d, '
'enforce_order=%d, populate_should_replace=%d',
id, directory, default_ttl, enforce_order,
populate_should_replace)
self.log.debug(
'__init__: id=%s, directory=%s, default_ttl=%d, '
'enforce_order=%d, populate_should_replace=%d',
id,
directory,
default_ttl,
enforce_order,
populate_should_replace,
)
super(YamlProvider, self).__init__(id, *args, **kwargs)
self.directory = directory
self.default_ttl = default_ttl
@ -140,16 +175,25 @@ class YamlProvider(BaseProvider):
for d in data:
if 'ttl' not in d:
d['ttl'] = self.default_ttl
record = Record.new(zone, name, d, source=self,
lenient=lenient)
zone.add_record(record, lenient=lenient,
replace=self.populate_should_replace)
self.log.debug('_populate_from_file: successfully loaded "%s"',
filename)
record = Record.new(
zone, name, d, source=self, lenient=lenient
)
zone.add_record(
record,
lenient=lenient,
replace=self.populate_should_replace,
)
self.log.debug(
'_populate_from_file: successfully loaded "%s"', filename
)
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug(
'populate: name=%s, target=%s, lenient=%s',
zone.name,
target,
lenient,
)
if target:
# When acting as a target we ignore any existing records so that we
@ -160,15 +204,18 @@ class YamlProvider(BaseProvider):
filename = join(self.directory, f'{zone.name}yaml')
self._populate_from_file(filename, zone, lenient)
self.log.info('populate: found %s records, exists=False',
len(zone.records) - before)
self.log.info(
'populate: found %s records, exists=False',
len(zone.records) - before,
)
return False
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes))
self.log.debug(
'_apply: zone=%s, len(changes)=%d', desired.name, len(changes)
)
# Since we don't have existing we'll only see creates
records = [c.new for c in changes]
# Order things alphabetically (records sort that way
@ -257,8 +304,12 @@ class SplitYamlProvider(YamlProvider):
return join(self.directory, filename)
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug(
'populate: name=%s, target=%s, lenient=%s',
zone.name,
target,
lenient,
)
if target:
# When acting as a target we ignore any existing records so that we
@ -271,8 +322,10 @@ class SplitYamlProvider(YamlProvider):
for yaml_filename in yaml_filenames:
self._populate_from_file(yaml_filename, zone, lenient)
self.log.info('populate: found %s records, exists=False',
len(zone.records) - before)
self.log.info(
'populate: found %s records, exists=False',
len(zone.records) - before,
)
return False
def _do_apply(self, desired, data):

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from ipaddress import IPv4Address, IPv6Address
from logging import getLogger
@ -16,7 +20,6 @@ from .geo import GeoCodes
class Change(EqualityTupleMixin):
def __init__(self, existing, new):
self.existing = existing
self.new = new
@ -50,8 +53,10 @@ class Update(Change):
# do nothing
def __repr__(self, leader=''):
source = self.new.source.id if self.new.source else ''
return f'Update\n{leader} {self.existing} ->\n' \
return (
f'Update\n{leader} {self.existing} ->\n'
f'{leader} {self.new} ({source})'
)
class Delete(Change):
@ -69,7 +74,6 @@ class RecordException(Exception):
class ValidationError(RecordException):
@classmethod
def build_message(cls, fqdn, reasons):
reasons = '\n - '.join(reasons)
@ -129,13 +133,17 @@ class Record(EqualityTupleMixin):
reasons.append('invalid name "@", use "" instead')
n = len(fqdn)
if n > 253:
reasons.append(f'invalid fqdn, "{fqdn}" is too long at {n} '
'chars, max is 253')
reasons.append(
f'invalid fqdn, "{fqdn}" is too long at {n} '
'chars, max is 253'
)
for label in name.split('.'):
n = len(label)
if n > 63:
reasons.append(f'invalid label, "{label}" is too long at {n}'
' chars, max is 63')
reasons.append(
f'invalid label, "{label}" is too long at {n}'
' chars, max is 63'
)
try:
ttl = int(data['ttl'])
if ttl < 0:
@ -143,16 +151,23 @@ class Record(EqualityTupleMixin):
except KeyError:
reasons.append('missing ttl')
try:
if data['octodns']['healthcheck']['protocol'] \
not in ('HTTP', 'HTTPS', 'TCP'):
if data['octodns']['healthcheck']['protocol'] not in (
'HTTP',
'HTTPS',
'TCP',
):
reasons.append('invalid healthcheck protocol')
except KeyError:
pass
return reasons
def __init__(self, zone, name, data, source=None):
self.log.debug('__init__: zone.name=%s, type=%11s, name=%s', zone.name,
self.__class__.__name__, name)
self.log.debug(
'__init__: zone.name=%s, type=%11s, name=%s',
zone.name,
self.__class__.__name__,
name,
)
self.zone = zone
# force everything lower-case just to be safe
self.name = str(name).lower() if name else name
@ -231,7 +246,7 @@ class Record(EqualityTupleMixin):
self.name,
data,
self.source,
lenient=True
lenient=True,
)
# NOTE: we're using __hash__ and ordering methods that consider Records
@ -250,8 +265,10 @@ class Record(EqualityTupleMixin):
class GeoValue(EqualityTupleMixin):
geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
r'(-(?P<subdivision_code>\w\w))?)?$')
geo_re = re.compile(
r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
r'(-(?P<subdivision_code>\w\w))?)?$'
)
@classmethod
def _validate_geo(cls, code):
@ -277,16 +294,21 @@ class GeoValue(EqualityTupleMixin):
bits.pop()
def _equality_tuple(self):
return (self.continent_code, self.country_code, self.subdivision_code,
self.values)
return (
self.continent_code,
self.country_code,
self.subdivision_code,
self.values,
)
def __repr__(self):
return f"'Geo {self.continent_code} {self.country_code} " \
return (
f"'Geo {self.continent_code} {self.country_code} "
"{self.subdivision_code} {self.values}'"
)
class ValuesMixin(object):
@classmethod
def validate(cls, name, fqdn, data):
reasons = super(ValuesMixin, cls).validate(name, fqdn, data)
@ -377,18 +399,20 @@ class _GeoMixin(ValuesMixin):
def __repr__(self):
if self.geo:
klass = self.__class__.__name__
return f'<{klass} {self._type} {self.ttl}, {self.fqdn}, ' \
return (
f'<{klass} {self._type} {self.ttl}, {self.fqdn}, '
f'{self.values}, {self.geo}>'
)
return super(_GeoMixin, self).__repr__()
class ValueMixin(object):
@classmethod
def validate(cls, name, fqdn, data):
reasons = super(ValueMixin, cls).validate(name, fqdn, data)
reasons.extend(cls._value_type.validate(data.get('value', None),
cls._type))
reasons.extend(
cls._value_type.validate(data.get('value', None), cls._type)
)
return reasons
def __init__(self, zone, name, data, source=None):
@ -422,7 +446,8 @@ class _DynamicPool(object):
'value': d['value'],
'weight': d.get('weight', 1),
'status': d.get('status', 'obey'),
} for d in data['values']
}
for d in data['values']
]
values.sort(key=lambda d: d['value'])
@ -432,7 +457,9 @@ class _DynamicPool(object):
if weight != 1:
self.log.warning(
'Using weight=1 instead of %s for single-value pool %s',
weight, _id)
weight,
_id,
)
values[0]['weight'] = 1
fallback = data.get('fallback', None)
@ -457,7 +484,6 @@ class _DynamicPool(object):
class _DynamicRule(object):
def __init__(self, i, data):
self.i = i
@ -487,7 +513,6 @@ class _DynamicRule(object):
class _Dynamic(object):
def __init__(self, pools, rules):
self.pools = pools
self.rules = rules
@ -499,10 +524,7 @@ class _Dynamic(object):
rules = []
for rule in self.rules:
rules.append(rule._data())
return {
'pools': pools,
'rules': rules,
}
return {'pools': pools, 'rules': rules}
def __eq__(self, other):
if not isinstance(other, _Dynamic):
@ -518,8 +540,10 @@ class _Dynamic(object):
class _DynamicMixin(object):
geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
r'(-(?P<subdivision_code>\w\w))?)?$')
geo_re = re.compile(
r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
r'(-(?P<subdivision_code>\w\w))?)?$'
)
@classmethod
def validate(cls, name, fqdn, data):
@ -561,41 +585,53 @@ class _DynamicMixin(object):
weight = value['weight']
weight = int(weight)
if weight < 1 or weight > 100:
reasons.append(f'invalid weight "{weight}" in '
f'pool "{_id}" value {value_num}')
reasons.append(
f'invalid weight "{weight}" in '
f'pool "{_id}" value {value_num}'
)
except KeyError:
pass
except ValueError:
reasons.append(f'invalid weight "{weight}" in '
f'pool "{_id}" value {value_num}')
reasons.append(
f'invalid weight "{weight}" in '
f'pool "{_id}" value {value_num}'
)
try:
status = value['status']
if status not in ['up', 'down', 'obey']:
reasons.append(f'invalid status "{status}" in '
f'pool "{_id}" value {value_num}')
reasons.append(
f'invalid status "{status}" in '
f'pool "{_id}" value {value_num}'
)
except KeyError:
pass
try:
value = value['value']
reasons.extend(cls._value_type.validate(value,
cls._type))
reasons.extend(
cls._value_type.validate(value, cls._type)
)
except KeyError:
reasons.append(f'missing value in pool "{_id}" '
f'value {value_num}')
reasons.append(
f'missing value in pool "{_id}" '
f'value {value_num}'
)
if len(values) == 1 and values[0].get('weight', 1) != 1:
reasons.append(f'pool "{_id}" has single value with '
'weight!=1')
reasons.append(
f'pool "{_id}" has single value with ' 'weight!=1'
)
fallback = pool.get('fallback', None)
if fallback is not None:
if fallback in pools:
pools_seen_as_fallback.add(fallback)
else:
reasons.append(f'undefined fallback "{fallback}" '
f'for pool "{_id}"')
reasons.append(
f'undefined fallback "{fallback}" '
f'for pool "{_id}"'
)
# Check for loops
fallback = pools[_id].get('fallback', None)
@ -639,11 +675,14 @@ class _DynamicMixin(object):
reasons.append(f'rule {rule_num} invalid pool "{pool}"')
else:
if pool not in pools:
reasons.append(f'rule {rule_num} undefined pool '
f'"{pool}"')
reasons.append(
f'rule {rule_num} undefined pool ' f'"{pool}"'
)
elif pool in pools_seen and geos:
reasons.append(f'rule {rule_num} invalid, target '
f'pool "{pool}" reused')
reasons.append(
f'rule {rule_num} invalid, target '
f'pool "{pool}" reused'
)
pools_seen.add(pool)
if not geos:
@ -655,8 +694,9 @@ class _DynamicMixin(object):
reasons.append(f'rule {rule_num} geos must be a list')
else:
for geo in geos:
reasons.extend(GeoCodes.validate(geo,
f'rule {rule_num} '))
reasons.extend(
GeoCodes.validate(geo, f'rule {rule_num} ')
)
unused = pools_exist - pools_seen - pools_seen_as_fallback
if unused:
@ -666,8 +706,7 @@ class _DynamicMixin(object):
return reasons
def __init__(self, zone, name, data, *args, **kwargs):
super(_DynamicMixin, self).__init__(zone, name, data, *args,
**kwargs)
super(_DynamicMixin, self).__init__(zone, name, data, *args, **kwargs)
self.dynamic = {}
@ -720,13 +759,14 @@ class _DynamicMixin(object):
values = self.value
klass = self.__class__.__name__
return f'<{klass} {self._type} {self.ttl}, {self.fqdn}, ' \
return (
f'<{klass} {self._type} {self.ttl}, {self.fqdn}, '
f'{values}, {self.dynamic}>'
)
return super(_DynamicMixin, self).__repr__()
class _IpList(object):
@classmethod
def validate(cls, data, _type):
if not isinstance(data, (list, tuple)):
@ -754,8 +794,7 @@ class _IpList(object):
values = [str(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]
return [str(cls._address_type(v)) if v != '' else '' for v in values]
class Ipv4List(_IpList):
@ -769,7 +808,6 @@ class Ipv6List(_IpList):
class _TargetValue(object):
@classmethod
def validate(cls, data, _type):
reasons = []
@ -869,11 +907,7 @@ class CaaValue(EqualityTupleMixin):
@property
def data(self):
return {
'flags': self.flags,
'tag': self.tag,
'value': self.value,
}
return {'flags': self.flags, 'tag': self.tag, 'value': self.value}
def _equality_tuple(self):
return (self.flags, self.tag, self.value)
@ -935,10 +969,7 @@ class LocValue(EqualityTupleMixin):
'precision_vert',
]
direction_keys = [
'lat_direction',
'long_direction',
]
direction_keys = ['lat_direction', 'long_direction']
if not isinstance(data, (list, tuple)):
data = (data,)
@ -949,18 +980,21 @@ class LocValue(EqualityTupleMixin):
int(value[key])
if (
(
key == 'lat_degrees' and
not 0 <= int(value[key]) <= 90
) or (
key == 'long_degrees' and
not 0 <= int(value[key]) <= 180
) or (
key in ['lat_minutes', 'long_minutes'] and
not 0 <= int(value[key]) <= 59
key == 'lat_degrees'
and not 0 <= int(value[key]) <= 90
)
or (
key == 'long_degrees'
and not 0 <= int(value[key]) <= 180
)
or (
key in ['lat_minutes', 'long_minutes']
and not 0 <= int(value[key]) <= 59
)
):
reasons.append(f'invalid value for {key} '
f'"{value[key]}"')
reasons.append(
f'invalid value for {key} ' f'"{value[key]}"'
)
except KeyError:
reasons.append(f'missing {key}')
except ValueError:
@ -971,20 +1005,23 @@ class LocValue(EqualityTupleMixin):
float(value[key])
if (
(
key in ['lat_seconds', 'long_seconds'] and
not 0 <= float(value[key]) <= 59.999
) or (
key == 'altitude' and
not -100000.00 <= float(value[key]) <= 42849672.95
) or (
key in ['size',
'precision_horz',
'precision_vert'] and
not 0 <= float(value[key]) <= 90000000.00
key in ['lat_seconds', 'long_seconds']
and not 0 <= float(value[key]) <= 59.999
)
or (
key == 'altitude'
and not -100000.00
<= float(value[key])
<= 42849672.95
)
or (
key in ['size', 'precision_horz', 'precision_vert']
and not 0 <= float(value[key]) <= 90000000.00
)
):
reasons.append(f'invalid value for {key} '
f'"{value[key]}"')
reasons.append(
f'invalid value for {key} ' f'"{value[key]}"'
)
except KeyError:
reasons.append(f'missing {key}')
except ValueError:
@ -993,18 +1030,14 @@ class LocValue(EqualityTupleMixin):
for key in direction_keys:
try:
str(value[key])
if (
key == 'lat_direction' and
value[key] not in ['N', 'S']
):
reasons.append(f'invalid direction for {key} '
f'"{value[key]}"')
if (
key == 'long_direction' and
value[key] not in ['E', 'W']
):
reasons.append(f'invalid direction for {key} '
f'"{value[key]}"')
if key == 'lat_direction' and value[key] not in ['N', 'S']:
reasons.append(
f'invalid direction for {key} ' f'"{value[key]}"'
)
if key == 'long_direction' and value[key] not in ['E', 'W']:
reasons.append(
f'invalid direction for {key} ' f'"{value[key]}"'
)
except KeyError:
reasons.append(f'missing {key}')
return reasons
@ -1045,20 +1078,22 @@ class LocValue(EqualityTupleMixin):
}
def __hash__(self):
return hash((
self.lat_degrees,
self.lat_minutes,
self.lat_seconds,
self.lat_direction,
self.long_degrees,
self.long_minutes,
self.long_seconds,
self.long_direction,
self.altitude,
self.size,
self.precision_horz,
self.precision_vert,
))
return hash(
(
self.lat_degrees,
self.lat_minutes,
self.lat_seconds,
self.lat_direction,
self.long_degrees,
self.long_minutes,
self.long_seconds,
self.long_direction,
self.altitude,
self.size,
self.precision_horz,
self.precision_vert,
)
)
def _equality_tuple(self):
return (
@ -1077,12 +1112,14 @@ class LocValue(EqualityTupleMixin):
)
def __repr__(self):
return f"'{self.lat_degrees} {self.lat_minutes} " \
f"{self.lat_seconds:.3f} {self.lat_direction} " \
f"{self.long_degrees} {self.long_minutes} " \
f"{self.long_seconds:.3f} {self.long_direction} " \
f"{self.altitude:.2f}m {self.size:.2f}m " \
return (
f"'{self.lat_degrees} {self.lat_minutes} "
f"{self.lat_seconds:.3f} {self.lat_direction} "
f"{self.long_degrees} {self.long_minutes} "
f"{self.long_seconds:.3f} {self.long_direction} "
f"{self.altitude:.2f}m {self.size:.2f}m "
f"{self.precision_horz:.2f}m {self.precision_vert:.2f}m'"
)
class LocRecord(ValuesMixin, Record):
@ -1094,7 +1131,6 @@ Record.register_type(LocRecord)
class MxValue(EqualityTupleMixin):
@classmethod
def validate(cls, data, _type):
if not isinstance(data, (list, tuple)):
@ -1113,10 +1149,14 @@ class MxValue(EqualityTupleMixin):
exchange = None
try:
exchange = str(value.get('exchange', None) or value['value'])
if exchange != '.' and \
not FQDN(exchange, allow_underscores=True).is_valid:
reasons.append(f'Invalid MX exchange "{exchange}" is not '
'a valid FQDN.')
if (
exchange != '.'
and not FQDN(exchange, allow_underscores=True).is_valid
):
reasons.append(
f'Invalid MX exchange "{exchange}" is not '
'a valid FQDN.'
)
elif not exchange.endswith('.'):
reasons.append(f'MX value "{exchange}" missing trailing .')
except KeyError:
@ -1143,10 +1183,7 @@ class MxValue(EqualityTupleMixin):
@property
def data(self):
return {
'preference': self.preference,
'exchange': self.exchange,
}
return {'preference': self.preference, 'exchange': self.exchange}
def __hash__(self):
return hash((self.preference, self.exchange))
@ -1228,15 +1265,23 @@ class NaptrValue(EqualityTupleMixin):
return hash(self.__repr__())
def _equality_tuple(self):
return (self.order, self.preference, self.flags, self.service,
self.regexp, self.replacement)
return (
self.order,
self.preference,
self.flags,
self.service,
self.regexp,
self.replacement,
)
def __repr__(self):
flags = self.flags if self.flags is not None else ''
service = self.service if self.service is not None else ''
regexp = self.regexp if self.regexp is not None else ''
return f"'{self.order} {self.preference} \"{flags}\" \"{service}\" " \
return (
f"'{self.order} {self.preference} \"{flags}\" \"{service}\" "
f"\"{regexp}\" {self.replacement}'"
)
class NaptrRecord(ValuesMixin, Record):
@ -1248,7 +1293,6 @@ Record.register_type(NaptrRecord)
class _NsValue(object):
@classmethod
def validate(cls, data, _type):
if not data:
@ -1258,8 +1302,9 @@ class _NsValue(object):
reasons = []
for value in data:
if not FQDN(str(value), allow_underscores=True).is_valid:
reasons.append(f'Invalid NS value "{value}" is not '
'a valid FQDN.')
reasons.append(
f'Invalid NS value "{value}" is not ' 'a valid FQDN.'
)
elif not value.endswith('.'):
reasons.append(f'NS value "{value}" missing trailing .')
return reasons
@ -1278,7 +1323,6 @@ Record.register_type(NsRecord)
class PtrValue(_TargetValue):
@classmethod
def validate(cls, values, _type):
if not isinstance(values, list):
@ -1334,13 +1378,15 @@ class SshfpValue(EqualityTupleMixin):
try:
fingerprint_type = int(value['fingerprint_type'])
if fingerprint_type not in cls.VALID_FINGERPRINT_TYPES:
reasons.append('unrecognized fingerprint_type '
f'"{fingerprint_type}"')
reasons.append(
'unrecognized fingerprint_type ' f'"{fingerprint_type}"'
)
except KeyError:
reasons.append('missing fingerprint_type')
except ValueError:
reasons.append('invalid fingerprint_type '
f'"{value["fingerprint_type"]}"')
reasons.append(
'invalid fingerprint_type ' f'"{value["fingerprint_type"]}"'
)
if 'fingerprint' not in value:
reasons.append('missing fingerprint')
return reasons
@ -1386,8 +1432,10 @@ class _ChunkedValuesMixin(ValuesMixin):
def chunked_value(self, value):
value = value.replace('"', '\\"')
vs = [value[i:i + self.CHUNK_SIZE]
for i in range(0, len(value), self.CHUNK_SIZE)]
vs = [
value[i : i + self.CHUNK_SIZE]
for i in range(0, len(value), self.CHUNK_SIZE)
]
vs = '" "'.join(vs)
return f'"{vs}"'
@ -1433,7 +1481,6 @@ Record.register_type(SpfRecord)
class SrvValue(EqualityTupleMixin):
@classmethod
def validate(cls, data, _type):
if not isinstance(data, (list, tuple)):
@ -1463,10 +1510,13 @@ class SrvValue(EqualityTupleMixin):
target = value['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:
reasons.append(f'Invalid SRV target "{target}" is not '
'a valid FQDN.')
if (
target != '.'
and not FQDN(str(target), allow_underscores=True).is_valid
):
reasons.append(
f'Invalid SRV target "{target}" is not ' 'a valid FQDN.'
)
except KeyError:
reasons.append('missing target')
return reasons
@ -1518,7 +1568,6 @@ Record.register_type(SrvRecord)
class TlsaValue(EqualityTupleMixin):
@classmethod
def validate(cls, data, _type):
if not isinstance(data, (list, tuple)):
@ -1528,11 +1577,14 @@ class TlsaValue(EqualityTupleMixin):
try:
certificate_usage = int(value.get('certificate_usage', 0))
if certificate_usage < 0 or certificate_usage > 3:
reasons.append(f'invalid certificate_usage '
f'"{certificate_usage}"')
reasons.append(
f'invalid certificate_usage ' f'"{certificate_usage}"'
)
except ValueError:
reasons.append(f'invalid certificate_usage '
f'"{value["certificate_usage"]}"')
reasons.append(
f'invalid certificate_usage '
f'"{value["certificate_usage"]}"'
)
try:
selector = int(value.get('selector', 0))
@ -1546,8 +1598,9 @@ class TlsaValue(EqualityTupleMixin):
if matching_type < 0 or matching_type > 2:
reasons.append(f'invalid matching_type "{matching_type}"')
except ValueError:
reasons.append(f'invalid matching_type '
f'"{value["matching_type"]}"')
reasons.append(
f'invalid matching_type ' f'"{value["matching_type"]}"'
)
if 'certificate_usage' not in value:
reasons.append('missing certificate_usage')
@ -1567,8 +1620,9 @@ class TlsaValue(EqualityTupleMixin):
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']
self.certificate_association_data = value[
'certificate_association_data'
]
@property
def data(self):
@ -1580,12 +1634,18 @@ class TlsaValue(EqualityTupleMixin):
}
def _equality_tuple(self):
return (self.certificate_usage, self.selector, self.matching_type,
self.certificate_association_data)
return (
self.certificate_usage,
self.selector,
self.matching_type,
self.certificate_association_data,
)
def __repr__(self):
return f"'{self.certificate_usage} {self.selector} '" \
f"'{self.matching_type} {self.certificate_association_data}'"
return (
f"'{self.certificate_usage} {self.selector} '"
f"'{self.matching_type} {self.certificate_association_data}'"
)
class TlsaRecord(ValuesMixin, Record):
@ -1676,8 +1736,10 @@ class UrlfwdValue(EqualityTupleMixin):
return (self.path, self.target, self.code, self.masking, self.query)
def __repr__(self):
return f'"{self.path}" "{self.target}" {self.code} ' \
return (
f'"{self.path}" "{self.target}" {self.code} '
f'{self.masking} {self.query}'
)
class UrlfwdRecord(ValuesMixin, Record):

View File

@ -29,8 +29,10 @@ class GeoCodes(object):
reasons.append(f'{prefix}unknown continent code "{code}"')
elif n > 1 and pieces[1] not in geo_data[pieces[0]]:
reasons.append(f'{prefix}unknown country code "{code}"')
elif n > 2 and \
pieces[2] not in geo_data[pieces[0]][pieces[1]]['provinces']:
elif (
n > 2
and pieces[2] not in geo_data[pieces[0]][pieces[1]]['provinces']
):
reasons.append(f'{prefix}unknown province code "{code}"')
return reasons
@ -64,10 +66,13 @@ class GeoCodes(object):
def province_to_code(cls, province):
# We cheat on this one a little since we only support provinces in
# NA-US, NA-CA
if (province not in geo_data['NA']['US']['provinces'] and
province not in geo_data['NA']['CA']['provinces']):
cls.log.warning('country_to_code: unrecognized province "%s"',
province)
if (
province not in geo_data['NA']['US']['provinces']
and province not in geo_data['NA']['CA']['provinces']
):
cls.log.warning(
'country_to_code: unrecognized province "%s"', province
)
return
if province in geo_data['NA']['US']['provinces']:
country = 'US'

View File

@ -6,324 +6,346 @@
# do not modify it directly
#
geo_data = \
{'AF': {'AO': {'name': 'Angola'},
'BF': {'name': 'Burkina Faso'},
'BI': {'name': 'Burundi'},
'BJ': {'name': 'Benin'},
'BW': {'name': 'Botswana'},
'CD': {'name': 'Congo, The Democratic Republic of the'},
'CF': {'name': 'Central African Republic'},
'CG': {'name': 'Congo'},
'CI': {'name': "Côte d'Ivoire"},
'CM': {'name': 'Cameroon'},
'CV': {'name': 'Cabo Verde'},
'DJ': {'name': 'Djibouti'},
'DZ': {'name': 'Algeria'},
'EG': {'name': 'Egypt'},
'EH': {'name': 'Western Sahara'},
'ER': {'name': 'Eritrea'},
'ET': {'name': 'Ethiopia'},
'GA': {'name': 'Gabon'},
'GH': {'name': 'Ghana'},
'GM': {'name': 'Gambia'},
'GN': {'name': 'Guinea'},
'GQ': {'name': 'Equatorial Guinea'},
'GW': {'name': 'Guinea-Bissau'},
'KE': {'name': 'Kenya'},
'KM': {'name': 'Comoros'},
'LR': {'name': 'Liberia'},
'LS': {'name': 'Lesotho'},
'LY': {'name': 'Libya'},
'MA': {'name': 'Morocco'},
'MG': {'name': 'Madagascar'},
'ML': {'name': 'Mali'},
'MR': {'name': 'Mauritania'},
'MU': {'name': 'Mauritius'},
'MW': {'name': 'Malawi'},
'MZ': {'name': 'Mozambique'},
'NA': {'name': 'Namibia'},
'NE': {'name': 'Niger'},
'NG': {'name': 'Nigeria'},
'RE': {'name': 'Réunion'},
'RW': {'name': 'Rwanda'},
'SC': {'name': 'Seychelles'},
'SD': {'name': 'Sudan'},
'SH': {'name': 'Saint Helena, Ascension and Tristan da Cunha'},
'SL': {'name': 'Sierra Leone'},
'SN': {'name': 'Senegal'},
'SO': {'name': 'Somalia'},
'SS': {'name': 'South Sudan'},
'ST': {'name': 'Sao Tome and Principe'},
'SZ': {'name': 'Eswatini'},
'TD': {'name': 'Chad'},
'TG': {'name': 'Togo'},
'TN': {'name': 'Tunisia'},
'TZ': {'name': 'Tanzania, United Republic of'},
'UG': {'name': 'Uganda'},
'YT': {'name': 'Mayotte'},
'ZA': {'name': 'South Africa'},
'ZM': {'name': 'Zambia'},
'ZW': {'name': 'Zimbabwe'}},
'AN': {'AQ': {'name': 'Antarctica'},
'BV': {'name': 'Bouvet Island'},
'HM': {'name': 'Heard Island and McDonald Islands'},
'TF': {'name': 'French Southern Territories'}},
'AS': {'AE': {'name': 'United Arab Emirates'},
'AF': {'name': 'Afghanistan'},
'AM': {'name': 'Armenia'},
'AZ': {'name': 'Azerbaijan'},
'BD': {'name': 'Bangladesh'},
'BH': {'name': 'Bahrain'},
'BN': {'name': 'Brunei Darussalam'},
'BT': {'name': 'Bhutan'},
'CC': {'name': 'Cocos (Keeling) Islands'},
'CN': {'name': 'China'},
'CX': {'name': 'Christmas Island'},
'CY': {'name': 'Cyprus'},
'GE': {'name': 'Georgia'},
'HK': {'name': 'Hong Kong'},
'ID': {'name': 'Indonesia'},
'IL': {'name': 'Israel'},
'IN': {'name': 'India'},
'IO': {'name': 'British Indian Ocean Territory'},
'IQ': {'name': 'Iraq'},
'IR': {'name': 'Iran, Islamic Republic of'},
'JO': {'name': 'Jordan'},
'JP': {'name': 'Japan'},
'KG': {'name': 'Kyrgyzstan'},
'KH': {'name': 'Cambodia'},
'KP': {'name': "Korea, Democratic People's Republic of"},
'KR': {'name': 'Korea, Republic of'},
'KW': {'name': 'Kuwait'},
'KZ': {'name': 'Kazakhstan'},
'LA': {'name': "Lao People's Democratic Republic"},
'LB': {'name': 'Lebanon'},
'LK': {'name': 'Sri Lanka'},
'MM': {'name': 'Myanmar'},
'MN': {'name': 'Mongolia'},
'MO': {'name': 'Macao'},
'MV': {'name': 'Maldives'},
'MY': {'name': 'Malaysia'},
'NP': {'name': 'Nepal'},
'OM': {'name': 'Oman'},
'PH': {'name': 'Philippines'},
'PK': {'name': 'Pakistan'},
'PS': {'name': 'Palestine, State of'},
'QA': {'name': 'Qatar'},
'SA': {'name': 'Saudi Arabia'},
'SG': {'name': 'Singapore'},
'SY': {'name': 'Syrian Arab Republic'},
'TH': {'name': 'Thailand'},
'TJ': {'name': 'Tajikistan'},
'TL': {'name': 'Timor-Leste'},
'TM': {'name': 'Turkmenistan'},
'TR': {'name': 'Turkey'},
'TW': {'name': 'Taiwan, Province of China'},
'UZ': {'name': 'Uzbekistan'},
'VN': {'name': 'Viet Nam'},
'YE': {'name': 'Yemen'}},
'EU': {'AD': {'name': 'Andorra'},
'AL': {'name': 'Albania'},
'AT': {'name': 'Austria'},
'AX': {'name': 'Åland Islands'},
'BA': {'name': 'Bosnia and Herzegovina'},
'BE': {'name': 'Belgium'},
'BG': {'name': 'Bulgaria'},
'BY': {'name': 'Belarus'},
'CH': {'name': 'Switzerland'},
'CZ': {'name': 'Czechia'},
'DE': {'name': 'Germany'},
'DK': {'name': 'Denmark'},
'EE': {'name': 'Estonia'},
'ES': {'name': 'Spain'},
'FI': {'name': 'Finland'},
'FO': {'name': 'Faroe Islands'},
'FR': {'name': 'France'},
'GB': {'name': 'United Kingdom'},
'GG': {'name': 'Guernsey'},
'GI': {'name': 'Gibraltar'},
'GR': {'name': 'Greece'},
'HR': {'name': 'Croatia'},
'HU': {'name': 'Hungary'},
'IE': {'name': 'Ireland'},
'IM': {'name': 'Isle of Man'},
'IS': {'name': 'Iceland'},
'IT': {'name': 'Italy'},
'JE': {'name': 'Jersey'},
'LI': {'name': 'Liechtenstein'},
'LT': {'name': 'Lithuania'},
'LU': {'name': 'Luxembourg'},
'LV': {'name': 'Latvia'},
'MC': {'name': 'Monaco'},
'MD': {'name': 'Moldova, Republic of'},
'ME': {'name': 'Montenegro'},
'MK': {'name': 'North Macedonia'},
'MT': {'name': 'Malta'},
'NL': {'name': 'Netherlands'},
'NO': {'name': 'Norway'},
'PL': {'name': 'Poland'},
'PT': {'name': 'Portugal'},
'RO': {'name': 'Romania'},
'RS': {'name': 'Serbia'},
'RU': {'name': 'Russian Federation'},
'SE': {'name': 'Sweden'},
'SI': {'name': 'Slovenia'},
'SJ': {'name': 'Svalbard and Jan Mayen'},
'SK': {'name': 'Slovakia'},
'SM': {'name': 'San Marino'},
'UA': {'name': 'Ukraine'},
'VA': {'name': 'Holy See (Vatican City State)'}},
'NA': {'AG': {'name': 'Antigua and Barbuda'},
'AI': {'name': 'Anguilla'},
'AW': {'name': 'Aruba'},
'BB': {'name': 'Barbados'},
'BL': {'name': 'Saint Barthélemy'},
'BM': {'name': 'Bermuda'},
'BQ': {'name': 'Bonaire, Sint Eustatius and Saba'},
'BS': {'name': 'Bahamas'},
'BZ': {'name': 'Belize'},
'CA': {'name': 'Canada',
'provinces': {'AB': {'name': 'Alberta'},
'BC': {'name': 'British Columbia'},
'MB': {'name': 'Manitoba'},
'NB': {'name': 'New Brunswick'},
'NL': {'name': 'Newfoundland and Labrador'},
'NS': {'name': 'Nova Scotia'},
'NT': {'name': 'Northwest Territories'},
'NU': {'name': 'Nunavut'},
'ON': {'name': 'Ontario'},
'PE': {'name': 'Prince Edward Island'},
'QC': {'name': 'Quebec'},
'SK': {'name': 'Saskatchewan'},
'YT': {'name': 'Yukon Territory'}}},
'CR': {'name': 'Costa Rica'},
'CU': {'name': 'Cuba'},
'CW': {'name': 'Curaçao'},
'DM': {'name': 'Dominica'},
'DO': {'name': 'Dominican Republic'},
'GD': {'name': 'Grenada'},
'GL': {'name': 'Greenland'},
'GP': {'name': 'Guadeloupe'},
'GT': {'name': 'Guatemala'},
'HN': {'name': 'Honduras'},
'HT': {'name': 'Haiti'},
'JM': {'name': 'Jamaica'},
'KN': {'name': 'Saint Kitts and Nevis'},
'KY': {'name': 'Cayman Islands'},
'LC': {'name': 'Saint Lucia'},
'MF': {'name': 'Saint Martin (French part)'},
'MQ': {'name': 'Martinique'},
'MS': {'name': 'Montserrat'},
'MX': {'name': 'Mexico'},
'NI': {'name': 'Nicaragua'},
'PA': {'name': 'Panama'},
'PM': {'name': 'Saint Pierre and Miquelon'},
'PR': {'name': 'Puerto Rico'},
'SV': {'name': 'El Salvador'},
'SX': {'name': 'Sint Maarten (Dutch part)'},
'TC': {'name': 'Turks and Caicos Islands'},
'TT': {'name': 'Trinidad and Tobago'},
'US': {'name': 'United States',
'provinces': {'AK': {'name': 'Alaska'},
'AL': {'name': 'Alabama'},
'AR': {'name': 'Arkansas'},
'AS': {'name': 'American Samoa'},
'AZ': {'name': 'Arizona'},
'CA': {'name': 'California'},
'CO': {'name': 'Colorado'},
'CT': {'name': 'Connecticut'},
'DC': {'name': 'District of Columbia'},
'DE': {'name': 'Delaware'},
'FL': {'name': 'Florida'},
'GA': {'name': 'Georgia'},
'GU': {'name': 'Guam'},
'HI': {'name': 'Hawaii'},
'IA': {'name': 'Iowa'},
'ID': {'name': 'Idaho'},
'IL': {'name': 'Illinois'},
'IN': {'name': 'Indiana'},
'KS': {'name': 'Kansas'},
'KY': {'name': 'Kentucky'},
'LA': {'name': 'Louisiana'},
'MA': {'name': 'Massachusetts'},
'MD': {'name': 'Maryland'},
'ME': {'name': 'Maine'},
'MI': {'name': 'Michigan'},
'MN': {'name': 'Minnesota'},
'MO': {'name': 'Missouri'},
'MP': {'name': 'Northern Mariana Islands'},
'MS': {'name': 'Mississippi'},
'MT': {'name': 'Montana'},
'NC': {'name': 'North Carolina'},
'ND': {'name': 'North Dakota'},
'NE': {'name': 'Nebraska'},
'NH': {'name': 'New Hampshire'},
'NJ': {'name': 'New Jersey'},
'NM': {'name': 'New Mexico'},
'NV': {'name': 'Nevada'},
'NY': {'name': 'New York'},
'OH': {'name': 'Ohio'},
'OK': {'name': 'Oklahoma'},
'OR': {'name': 'Oregon'},
'PA': {'name': 'Pennsylvania'},
'PR': {'name': 'Puerto Rico'},
'RI': {'name': 'Rhode Island'},
'SC': {'name': 'South Carolina'},
'SD': {'name': 'South Dakota'},
'TN': {'name': 'Tennessee'},
'TX': {'name': 'Texas'},
'UM': {'name': 'United States Minor Outlying '
'Islands'},
'UT': {'name': 'Utah'},
'VA': {'name': 'Virginia'},
'VI': {'name': 'Virgin Islands'},
'VT': {'name': 'Vermont'},
'WA': {'name': 'Washington'},
'WI': {'name': 'Wisconsin'},
'WV': {'name': 'West Virginia'},
'WY': {'name': 'Wyoming'}}},
'VC': {'name': 'Saint Vincent and the Grenadines'},
'VG': {'name': 'Virgin Islands, British'},
'VI': {'name': 'Virgin Islands, U.S.'}},
'OC': {'AS': {'name': 'American Samoa'},
'AU': {'name': 'Australia'},
'CK': {'name': 'Cook Islands'},
'FJ': {'name': 'Fiji'},
'FM': {'name': 'Micronesia, Federated States of'},
'GU': {'name': 'Guam'},
'KI': {'name': 'Kiribati'},
'MH': {'name': 'Marshall Islands'},
'MP': {'name': 'Northern Mariana Islands'},
'NC': {'name': 'New Caledonia'},
'NF': {'name': 'Norfolk Island'},
'NR': {'name': 'Nauru'},
'NU': {'name': 'Niue'},
'NZ': {'name': 'New Zealand'},
'PF': {'name': 'French Polynesia'},
'PG': {'name': 'Papua New Guinea'},
'PN': {'name': 'Pitcairn'},
'PW': {'name': 'Palau'},
'SB': {'name': 'Solomon Islands'},
'TK': {'name': 'Tokelau'},
'TO': {'name': 'Tonga'},
'TV': {'name': 'Tuvalu'},
'UM': {'name': 'United States Minor Outlying Islands'},
'VU': {'name': 'Vanuatu'},
'WF': {'name': 'Wallis and Futuna'},
'WS': {'name': 'Samoa'}},
'SA': {'AR': {'name': 'Argentina'},
'BO': {'name': 'Bolivia, Plurinational State of'},
'BR': {'name': 'Brazil'},
'CL': {'name': 'Chile'},
'CO': {'name': 'Colombia'},
'EC': {'name': 'Ecuador'},
'FK': {'name': 'Falkland Islands (Malvinas)'},
'GF': {'name': 'French Guiana'},
'GS': {'name': 'South Georgia and the South Sandwich Islands'},
'GY': {'name': 'Guyana'},
'PE': {'name': 'Peru'},
'PY': {'name': 'Paraguay'},
'SR': {'name': 'Suriname'},
'UY': {'name': 'Uruguay'},
'VE': {'name': 'Venezuela, Bolivarian Republic of'}}}
geo_data = {
'AF': {
'AO': {'name': 'Angola'},
'BF': {'name': 'Burkina Faso'},
'BI': {'name': 'Burundi'},
'BJ': {'name': 'Benin'},
'BW': {'name': 'Botswana'},
'CD': {'name': 'Congo, The Democratic Republic of the'},
'CF': {'name': 'Central African Republic'},
'CG': {'name': 'Congo'},
'CI': {'name': "Côte d'Ivoire"},
'CM': {'name': 'Cameroon'},
'CV': {'name': 'Cabo Verde'},
'DJ': {'name': 'Djibouti'},
'DZ': {'name': 'Algeria'},
'EG': {'name': 'Egypt'},
'EH': {'name': 'Western Sahara'},
'ER': {'name': 'Eritrea'},
'ET': {'name': 'Ethiopia'},
'GA': {'name': 'Gabon'},
'GH': {'name': 'Ghana'},
'GM': {'name': 'Gambia'},
'GN': {'name': 'Guinea'},
'GQ': {'name': 'Equatorial Guinea'},
'GW': {'name': 'Guinea-Bissau'},
'KE': {'name': 'Kenya'},
'KM': {'name': 'Comoros'},
'LR': {'name': 'Liberia'},
'LS': {'name': 'Lesotho'},
'LY': {'name': 'Libya'},
'MA': {'name': 'Morocco'},
'MG': {'name': 'Madagascar'},
'ML': {'name': 'Mali'},
'MR': {'name': 'Mauritania'},
'MU': {'name': 'Mauritius'},
'MW': {'name': 'Malawi'},
'MZ': {'name': 'Mozambique'},
'NA': {'name': 'Namibia'},
'NE': {'name': 'Niger'},
'NG': {'name': 'Nigeria'},
'RE': {'name': 'Réunion'},
'RW': {'name': 'Rwanda'},
'SC': {'name': 'Seychelles'},
'SD': {'name': 'Sudan'},
'SH': {'name': 'Saint Helena, Ascension and Tristan da Cunha'},
'SL': {'name': 'Sierra Leone'},
'SN': {'name': 'Senegal'},
'SO': {'name': 'Somalia'},
'SS': {'name': 'South Sudan'},
'ST': {'name': 'Sao Tome and Principe'},
'SZ': {'name': 'Eswatini'},
'TD': {'name': 'Chad'},
'TG': {'name': 'Togo'},
'TN': {'name': 'Tunisia'},
'TZ': {'name': 'Tanzania, United Republic of'},
'UG': {'name': 'Uganda'},
'YT': {'name': 'Mayotte'},
'ZA': {'name': 'South Africa'},
'ZM': {'name': 'Zambia'},
'ZW': {'name': 'Zimbabwe'},
},
'AN': {
'AQ': {'name': 'Antarctica'},
'BV': {'name': 'Bouvet Island'},
'HM': {'name': 'Heard Island and McDonald Islands'},
'TF': {'name': 'French Southern Territories'},
},
'AS': {
'AE': {'name': 'United Arab Emirates'},
'AF': {'name': 'Afghanistan'},
'AM': {'name': 'Armenia'},
'AZ': {'name': 'Azerbaijan'},
'BD': {'name': 'Bangladesh'},
'BH': {'name': 'Bahrain'},
'BN': {'name': 'Brunei Darussalam'},
'BT': {'name': 'Bhutan'},
'CC': {'name': 'Cocos (Keeling) Islands'},
'CN': {'name': 'China'},
'CX': {'name': 'Christmas Island'},
'CY': {'name': 'Cyprus'},
'GE': {'name': 'Georgia'},
'HK': {'name': 'Hong Kong'},
'ID': {'name': 'Indonesia'},
'IL': {'name': 'Israel'},
'IN': {'name': 'India'},
'IO': {'name': 'British Indian Ocean Territory'},
'IQ': {'name': 'Iraq'},
'IR': {'name': 'Iran, Islamic Republic of'},
'JO': {'name': 'Jordan'},
'JP': {'name': 'Japan'},
'KG': {'name': 'Kyrgyzstan'},
'KH': {'name': 'Cambodia'},
'KP': {'name': "Korea, Democratic People's Republic of"},
'KR': {'name': 'Korea, Republic of'},
'KW': {'name': 'Kuwait'},
'KZ': {'name': 'Kazakhstan'},
'LA': {'name': "Lao People's Democratic Republic"},
'LB': {'name': 'Lebanon'},
'LK': {'name': 'Sri Lanka'},
'MM': {'name': 'Myanmar'},
'MN': {'name': 'Mongolia'},
'MO': {'name': 'Macao'},
'MV': {'name': 'Maldives'},
'MY': {'name': 'Malaysia'},
'NP': {'name': 'Nepal'},
'OM': {'name': 'Oman'},
'PH': {'name': 'Philippines'},
'PK': {'name': 'Pakistan'},
'PS': {'name': 'Palestine, State of'},
'QA': {'name': 'Qatar'},
'SA': {'name': 'Saudi Arabia'},
'SG': {'name': 'Singapore'},
'SY': {'name': 'Syrian Arab Republic'},
'TH': {'name': 'Thailand'},
'TJ': {'name': 'Tajikistan'},
'TL': {'name': 'Timor-Leste'},
'TM': {'name': 'Turkmenistan'},
'TR': {'name': 'Turkey'},
'TW': {'name': 'Taiwan, Province of China'},
'UZ': {'name': 'Uzbekistan'},
'VN': {'name': 'Viet Nam'},
'YE': {'name': 'Yemen'},
},
'EU': {
'AD': {'name': 'Andorra'},
'AL': {'name': 'Albania'},
'AT': {'name': 'Austria'},
'AX': {'name': 'Åland Islands'},
'BA': {'name': 'Bosnia and Herzegovina'},
'BE': {'name': 'Belgium'},
'BG': {'name': 'Bulgaria'},
'BY': {'name': 'Belarus'},
'CH': {'name': 'Switzerland'},
'CZ': {'name': 'Czechia'},
'DE': {'name': 'Germany'},
'DK': {'name': 'Denmark'},
'EE': {'name': 'Estonia'},
'ES': {'name': 'Spain'},
'FI': {'name': 'Finland'},
'FO': {'name': 'Faroe Islands'},
'FR': {'name': 'France'},
'GB': {'name': 'United Kingdom'},
'GG': {'name': 'Guernsey'},
'GI': {'name': 'Gibraltar'},
'GR': {'name': 'Greece'},
'HR': {'name': 'Croatia'},
'HU': {'name': 'Hungary'},
'IE': {'name': 'Ireland'},
'IM': {'name': 'Isle of Man'},
'IS': {'name': 'Iceland'},
'IT': {'name': 'Italy'},
'JE': {'name': 'Jersey'},
'LI': {'name': 'Liechtenstein'},
'LT': {'name': 'Lithuania'},
'LU': {'name': 'Luxembourg'},
'LV': {'name': 'Latvia'},
'MC': {'name': 'Monaco'},
'MD': {'name': 'Moldova, Republic of'},
'ME': {'name': 'Montenegro'},
'MK': {'name': 'North Macedonia'},
'MT': {'name': 'Malta'},
'NL': {'name': 'Netherlands'},
'NO': {'name': 'Norway'},
'PL': {'name': 'Poland'},
'PT': {'name': 'Portugal'},
'RO': {'name': 'Romania'},
'RS': {'name': 'Serbia'},
'RU': {'name': 'Russian Federation'},
'SE': {'name': 'Sweden'},
'SI': {'name': 'Slovenia'},
'SJ': {'name': 'Svalbard and Jan Mayen'},
'SK': {'name': 'Slovakia'},
'SM': {'name': 'San Marino'},
'UA': {'name': 'Ukraine'},
'VA': {'name': 'Holy See (Vatican City State)'},
},
'NA': {
'AG': {'name': 'Antigua and Barbuda'},
'AI': {'name': 'Anguilla'},
'AW': {'name': 'Aruba'},
'BB': {'name': 'Barbados'},
'BL': {'name': 'Saint Barthélemy'},
'BM': {'name': 'Bermuda'},
'BQ': {'name': 'Bonaire, Sint Eustatius and Saba'},
'BS': {'name': 'Bahamas'},
'BZ': {'name': 'Belize'},
'CA': {
'name': 'Canada',
'provinces': {
'AB': {'name': 'Alberta'},
'BC': {'name': 'British Columbia'},
'MB': {'name': 'Manitoba'},
'NB': {'name': 'New Brunswick'},
'NL': {'name': 'Newfoundland and Labrador'},
'NS': {'name': 'Nova Scotia'},
'NT': {'name': 'Northwest Territories'},
'NU': {'name': 'Nunavut'},
'ON': {'name': 'Ontario'},
'PE': {'name': 'Prince Edward Island'},
'QC': {'name': 'Quebec'},
'SK': {'name': 'Saskatchewan'},
'YT': {'name': 'Yukon Territory'},
},
},
'CR': {'name': 'Costa Rica'},
'CU': {'name': 'Cuba'},
'CW': {'name': 'Curaçao'},
'DM': {'name': 'Dominica'},
'DO': {'name': 'Dominican Republic'},
'GD': {'name': 'Grenada'},
'GL': {'name': 'Greenland'},
'GP': {'name': 'Guadeloupe'},
'GT': {'name': 'Guatemala'},
'HN': {'name': 'Honduras'},
'HT': {'name': 'Haiti'},
'JM': {'name': 'Jamaica'},
'KN': {'name': 'Saint Kitts and Nevis'},
'KY': {'name': 'Cayman Islands'},
'LC': {'name': 'Saint Lucia'},
'MF': {'name': 'Saint Martin (French part)'},
'MQ': {'name': 'Martinique'},
'MS': {'name': 'Montserrat'},
'MX': {'name': 'Mexico'},
'NI': {'name': 'Nicaragua'},
'PA': {'name': 'Panama'},
'PM': {'name': 'Saint Pierre and Miquelon'},
'PR': {'name': 'Puerto Rico'},
'SV': {'name': 'El Salvador'},
'SX': {'name': 'Sint Maarten (Dutch part)'},
'TC': {'name': 'Turks and Caicos Islands'},
'TT': {'name': 'Trinidad and Tobago'},
'US': {
'name': 'United States',
'provinces': {
'AK': {'name': 'Alaska'},
'AL': {'name': 'Alabama'},
'AR': {'name': 'Arkansas'},
'AS': {'name': 'American Samoa'},
'AZ': {'name': 'Arizona'},
'CA': {'name': 'California'},
'CO': {'name': 'Colorado'},
'CT': {'name': 'Connecticut'},
'DC': {'name': 'District of Columbia'},
'DE': {'name': 'Delaware'},
'FL': {'name': 'Florida'},
'GA': {'name': 'Georgia'},
'GU': {'name': 'Guam'},
'HI': {'name': 'Hawaii'},
'IA': {'name': 'Iowa'},
'ID': {'name': 'Idaho'},
'IL': {'name': 'Illinois'},
'IN': {'name': 'Indiana'},
'KS': {'name': 'Kansas'},
'KY': {'name': 'Kentucky'},
'LA': {'name': 'Louisiana'},
'MA': {'name': 'Massachusetts'},
'MD': {'name': 'Maryland'},
'ME': {'name': 'Maine'},
'MI': {'name': 'Michigan'},
'MN': {'name': 'Minnesota'},
'MO': {'name': 'Missouri'},
'MP': {'name': 'Northern Mariana Islands'},
'MS': {'name': 'Mississippi'},
'MT': {'name': 'Montana'},
'NC': {'name': 'North Carolina'},
'ND': {'name': 'North Dakota'},
'NE': {'name': 'Nebraska'},
'NH': {'name': 'New Hampshire'},
'NJ': {'name': 'New Jersey'},
'NM': {'name': 'New Mexico'},
'NV': {'name': 'Nevada'},
'NY': {'name': 'New York'},
'OH': {'name': 'Ohio'},
'OK': {'name': 'Oklahoma'},
'OR': {'name': 'Oregon'},
'PA': {'name': 'Pennsylvania'},
'PR': {'name': 'Puerto Rico'},
'RI': {'name': 'Rhode Island'},
'SC': {'name': 'South Carolina'},
'SD': {'name': 'South Dakota'},
'TN': {'name': 'Tennessee'},
'TX': {'name': 'Texas'},
'UM': {'name': 'United States Minor Outlying ' 'Islands'},
'UT': {'name': 'Utah'},
'VA': {'name': 'Virginia'},
'VI': {'name': 'Virgin Islands'},
'VT': {'name': 'Vermont'},
'WA': {'name': 'Washington'},
'WI': {'name': 'Wisconsin'},
'WV': {'name': 'West Virginia'},
'WY': {'name': 'Wyoming'},
},
},
'VC': {'name': 'Saint Vincent and the Grenadines'},
'VG': {'name': 'Virgin Islands, British'},
'VI': {'name': 'Virgin Islands, U.S.'},
},
'OC': {
'AS': {'name': 'American Samoa'},
'AU': {'name': 'Australia'},
'CK': {'name': 'Cook Islands'},
'FJ': {'name': 'Fiji'},
'FM': {'name': 'Micronesia, Federated States of'},
'GU': {'name': 'Guam'},
'KI': {'name': 'Kiribati'},
'MH': {'name': 'Marshall Islands'},
'MP': {'name': 'Northern Mariana Islands'},
'NC': {'name': 'New Caledonia'},
'NF': {'name': 'Norfolk Island'},
'NR': {'name': 'Nauru'},
'NU': {'name': 'Niue'},
'NZ': {'name': 'New Zealand'},
'PF': {'name': 'French Polynesia'},
'PG': {'name': 'Papua New Guinea'},
'PN': {'name': 'Pitcairn'},
'PW': {'name': 'Palau'},
'SB': {'name': 'Solomon Islands'},
'TK': {'name': 'Tokelau'},
'TO': {'name': 'Tonga'},
'TV': {'name': 'Tuvalu'},
'UM': {'name': 'United States Minor Outlying Islands'},
'VU': {'name': 'Vanuatu'},
'WF': {'name': 'Wallis and Futuna'},
'WS': {'name': 'Samoa'},
},
'SA': {
'AR': {'name': 'Argentina'},
'BO': {'name': 'Bolivia, Plurinational State of'},
'BR': {'name': 'Brazil'},
'CL': {'name': 'Chile'},
'CO': {'name': 'Colombia'},
'EC': {'name': 'Ecuador'},
'FK': {'name': 'Falkland Islands (Malvinas)'},
'GF': {'name': 'French Guiana'},
'GS': {'name': 'South Georgia and the South Sandwich Islands'},
'GY': {'name': 'Guyana'},
'PE': {'name': 'Peru'},
'PY': {'name': 'Paraguay'},
'SR': {'name': 'Suriname'},
'UY': {'name': 'Uruguay'},
'VE': {'name': 'Venezuela, Bolivarian Republic of'},
},
}

View File

@ -2,5 +2,9 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
import dns.name
import dns.query
@ -25,8 +29,22 @@ class AxfrBaseSource(BaseSource):
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'LOC', 'MX', 'NS', 'PTR',
'SPF', 'SRV', 'SSHFP', 'TXT'))
SUPPORTS = set(
(
'A',
'AAAA',
'CAA',
'CNAME',
'LOC',
'MX',
'NS',
'PTR',
'SPF',
'SRV',
'SSHFP',
'TXT',
)
)
def __init__(self, id):
super(AxfrBaseSource, self).__init__(id)
@ -35,7 +53,7 @@ class AxfrBaseSource(BaseSource):
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': [r['value'] for r in records]
'values': [r['value'] for r in records],
}
_data_for_A = _data_for_multiple
@ -46,75 +64,62 @@ class AxfrBaseSource(BaseSource):
values = []
for record in records:
flags, tag, value = record['value'].split(' ', 2)
values.append({
'flags': flags,
'tag': tag,
'value': value.replace('"', '')
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
values.append(
{'flags': flags, 'tag': tag, 'value': value.replace('"', '')}
)
return {'ttl': records[0]['ttl'], 'type': _type, 'values': values}
def _data_for_LOC(self, _type, records):
values = []
for record in records:
lat_degrees, lat_minutes, lat_seconds, lat_direction, \
long_degrees, long_minutes, long_seconds, long_direction, \
altitude, size, precision_horz, precision_vert = \
record['value'].replace('m', '').split(' ', 11)
values.append({
'lat_degrees': lat_degrees,
'lat_minutes': lat_minutes,
'lat_seconds': lat_seconds,
'lat_direction': lat_direction,
'long_degrees': long_degrees,
'long_minutes': long_minutes,
'long_seconds': long_seconds,
'long_direction': long_direction,
'altitude': altitude,
'size': size,
'precision_horz': precision_horz,
'precision_vert': precision_vert,
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
(
lat_degrees,
lat_minutes,
lat_seconds,
lat_direction,
long_degrees,
long_minutes,
long_seconds,
long_direction,
altitude,
size,
precision_horz,
precision_vert,
) = (record['value'].replace('m', '').split(' ', 11))
values.append(
{
'lat_degrees': lat_degrees,
'lat_minutes': lat_minutes,
'lat_seconds': lat_seconds,
'lat_direction': lat_direction,
'long_degrees': long_degrees,
'long_minutes': long_minutes,
'long_seconds': long_seconds,
'long_direction': long_direction,
'altitude': altitude,
'size': size,
'precision_horz': precision_horz,
'precision_vert': precision_vert,
}
)
return {'ttl': records[0]['ttl'], 'type': _type, 'values': values}
def _data_for_MX(self, _type, records):
values = []
for record in records:
preference, exchange = record['value'].split(' ', 1)
values.append({
'preference': preference,
'exchange': exchange,
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
values.append({'preference': preference, 'exchange': exchange})
return {'ttl': records[0]['ttl'], 'type': _type, 'values': values}
def _data_for_TXT(self, _type, records):
values = [value['value'].replace(';', '\\;') for value in records]
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
return {'ttl': records[0]['ttl'], 'type': _type, 'values': values}
_data_for_SPF = _data_for_TXT
def _data_for_single(self, _type, records):
record = records[0]
return {
'ttl': record['ttl'],
'type': _type,
'value': record['value']
}
return {'ttl': record['ttl'], 'type': _type, 'value': record['value']}
_data_for_CNAME = _data_for_single
_data_for_PTR = _data_for_single
@ -123,37 +128,38 @@ class AxfrBaseSource(BaseSource):
values = []
for record in records:
priority, weight, port, target = record['value'].split(' ', 3)
values.append({
'priority': priority,
'weight': weight,
'port': port,
'target': target,
})
return {
'type': _type,
'ttl': records[0]['ttl'],
'values': values
}
values.append(
{
'priority': priority,
'weight': weight,
'port': port,
'target': target,
}
)
return {'type': _type, 'ttl': records[0]['ttl'], 'values': values}
def _data_for_SSHFP(self, _type, records):
values = []
for record in records:
algorithm, fingerprint_type, fingerprint = \
record['value'].split(' ', 2)
values.append({
'algorithm': algorithm,
'fingerprint_type': fingerprint_type,
'fingerprint': fingerprint,
})
return {
'type': _type,
'ttl': records[0]['ttl'],
'values': values
}
algorithm, fingerprint_type, fingerprint = record['value'].split(
' ', 2
)
values.append(
{
'algorithm': algorithm,
'fingerprint_type': fingerprint_type,
'fingerprint': fingerprint,
}
)
return {'type': _type, 'ttl': records[0]['ttl'], 'values': values}
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug(
'populate: name=%s, target=%s, lenient=%s',
zone.name,
target,
lenient,
)
values = defaultdict(lambda: defaultdict(list))
for record in self.zone_records(zone):
@ -167,12 +173,18 @@ class AxfrBaseSource(BaseSource):
for name, types in values.items():
for _type, records in types.items():
data_for = getattr(self, f'_data_for_{_type}')
record = Record.new(zone, name, data_for(_type, records),
source=self, lenient=lenient)
record = Record.new(
zone,
name,
data_for(_type, records),
source=self,
lenient=lenient,
)
zone.add_record(record, lenient=lenient)
self.log.info('populate: found %s records',
len(zone.records) - before)
self.log.info(
'populate: found %s records', len(zone.records) - before
)
class AxfrSourceException(Exception):
@ -180,10 +192,10 @@ class AxfrSourceException(Exception):
class AxfrSourceZoneTransferFailed(AxfrSourceException):
def __init__(self):
super(AxfrSourceZoneTransferFailed, self).__init__(
'Unable to Perform Zone Transfer')
'Unable to Perform Zone Transfer'
)
class AxfrSource(AxfrBaseSource):
@ -195,6 +207,7 @@ class AxfrSource(AxfrBaseSource):
# The address of nameserver to perform zone transfer against
master: ns1.example.com
'''
def __init__(self, id, master):
self.log = logging.getLogger(f'AxfrSource[{id}]')
self.log.debug('__init__: id=%s, master=%s', id, master)
@ -203,9 +216,10 @@ class AxfrSource(AxfrBaseSource):
def zone_records(self, zone):
try:
z = dns.zone.from_xfr(dns.query.xfr(self.master, zone.name,
relativize=False),
relativize=False)
z = dns.zone.from_xfr(
dns.query.xfr(self.master, zone.name, relativize=False),
relativize=False,
)
except DNSException:
raise AxfrSourceZoneTransferFailed()
@ -213,12 +227,14 @@ class AxfrSource(AxfrBaseSource):
for (name, ttl, rdata) in z.iterate_rdatas():
rdtype = dns.rdatatype.to_text(rdata.rdtype)
records.append({
"name": name.to_text(),
"ttl": ttl,
"type": rdtype,
"value": rdata.to_text()
})
records.append(
{
"name": name.to_text(),
"ttl": ttl,
"type": rdtype,
"value": rdata.to_text(),
}
)
return records
@ -228,14 +244,11 @@ class ZoneFileSourceException(Exception):
class ZoneFileSourceNotFound(ZoneFileSourceException):
def __init__(self):
super(ZoneFileSourceNotFound, self).__init__(
'Zone file not found')
super(ZoneFileSourceNotFound, self).__init__('Zone file not found')
class ZoneFileSourceLoadFailure(ZoneFileSourceException):
def __init__(self, error):
super(ZoneFileSourceLoadFailure, self).__init__(str(error))
@ -258,11 +271,17 @@ class ZoneFileSource(AxfrBaseSource):
# (optional, default true)
check_origin: false
'''
def __init__(self, id, directory, file_extension='.', check_origin=True):
self.log = logging.getLogger(f'ZoneFileSource[{id}]')
self.log.debug('__init__: id=%s, directory=%s, file_extension=%s, '
'check_origin=%s', id,
directory, file_extension, check_origin)
self.log.debug(
'__init__: id=%s, directory=%s, file_extension=%s, '
'check_origin=%s',
id,
directory,
file_extension,
check_origin,
)
super(ZoneFileSource, self).__init__(id)
self.directory = directory
self.file_extension = file_extension
@ -275,9 +294,12 @@ class ZoneFileSource(AxfrBaseSource):
zonefiles = listdir(self.directory)
if zone_filename in zonefiles:
try:
z = dns.zone.from_file(join(self.directory, zone_filename),
zone_name, relativize=False,
check_origin=self.check_origin)
z = dns.zone.from_file(
join(self.directory, zone_filename),
zone_name,
relativize=False,
check_origin=self.check_origin,
)
except DNSException as error:
raise ZoneFileSourceLoadFailure(error)
else:
@ -292,12 +314,14 @@ class ZoneFileSource(AxfrBaseSource):
records = []
for (name, ttl, rdata) in z.iterate_rdatas():
rdtype = dns.rdatatype.to_text(rdata.rdtype)
records.append({
"name": name.to_text(),
"ttl": ttl,
"type": rdtype,
"value": rdata.to_text()
})
records.append(
{
"name": name.to_text(),
"ttl": ttl,
"type": rdtype,
"value": rdata.to_text(),
}
)
self._zone_records[zone.name] = records
except ZoneFileSourceNotFound:

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
class BaseSource(object):
@ -15,14 +19,17 @@ class BaseSource(object):
def __init__(self, id):
self.id = id
if not getattr(self, 'log', False):
raise NotImplementedError('Abstract base class, log property '
'missing')
raise NotImplementedError(
'Abstract base class, log property ' 'missing'
)
if not hasattr(self, 'SUPPORTS_GEO'):
raise NotImplementedError('Abstract base class, SUPPORTS_GEO '
'property missing')
raise NotImplementedError(
'Abstract base class, SUPPORTS_GEO ' 'property missing'
)
if not hasattr(self, 'SUPPORTS'):
raise NotImplementedError('Abstract base class, SUPPORTS '
'property missing')
raise NotImplementedError(
'Abstract base class, SUPPORTS ' 'property missing'
)
@property
def SUPPORTS_DYNAMIC(self):
@ -43,8 +50,9 @@ class BaseSource(object):
When target is True (loading current state) this method should return
True if the zone exists or False if it does not.
'''
raise NotImplementedError('Abstract base class, populate method '
'missing')
raise NotImplementedError(
'Abstract base class, populate method ' 'missing'
)
def supports(self, record):
return record._type in self.SUPPORTS

View File

@ -1,4 +1,3 @@
import logging
import os
@ -13,7 +12,8 @@ class EnvVarSourceException(Exception):
class EnvironmentVariableNotFoundException(EnvVarSourceException):
def __init__(self, data):
super(EnvironmentVariableNotFoundException, self).__init__(
f'Unknown environment variable {data}')
f'Unknown environment variable {data}'
)
class EnvVarSource(BaseSource):
@ -56,6 +56,7 @@ class EnvVarSource(BaseSource):
- ultra
- ns1
'''
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('TXT'))
@ -65,8 +66,13 @@ class EnvVarSource(BaseSource):
def __init__(self, id, variable, name, ttl=DEFAULT_TTL):
klass = self.__class__.__name__
self.log = logging.getLogger(f'{klass}[{id}]')
self.log.debug('__init__: id=%s, variable=%s, name=%s, '
'ttl=%d', id, variable, name, ttl)
self.log.debug(
'__init__: id=%s, variable=%s, name=%s, ' 'ttl=%d',
id,
variable,
name,
ttl,
)
super(EnvVarSource, self).__init__(id)
self.envvar = variable
self.name = name
@ -77,13 +83,20 @@ class EnvVarSource(BaseSource):
if value is None:
raise EnvironmentVariableNotFoundException(self.envvar)
self.log.debug('_read_variable: successfully loaded var=%s val=%s',
self.envvar, value)
self.log.debug(
'_read_variable: successfully loaded var=%s val=%s',
self.envvar,
value,
)
return value
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug(
'populate: name=%s, target=%s, lenient=%s',
zone.name,
target,
lenient,
)
before = len(zone.records)
@ -92,9 +105,12 @@ class EnvVarSource(BaseSource):
# We don't need to worry about conflicting records here because the
# manager will deconflict sources on our behalf.
payload = {'ttl': self.ttl, 'type': 'TXT', 'values': [value]}
record = Record.new(zone, self.name, payload, source=self,
lenient=lenient)
record = Record.new(
zone, self.name, payload, source=self, lenient=lenient
)
zone.add_record(record, lenient=lenient)
self.log.info('populate: found %s records, exists=False',
len(zone.records) - before)
self.log.info(
'populate: found %s records, exists=False',
len(zone.records) - before,
)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from collections import defaultdict
from ipaddress import ip_address
@ -40,11 +44,7 @@ class TinyDnsBaseSource(BaseSource):
ttl = records[0][1]
except IndexError:
ttl = self.default_ttl
return {
'ttl': ttl,
'type': _type,
'values': values,
}
return {'ttl': ttl, 'type': _type, 'values': values}
def _data_for_AAAA(self, _type, records):
values = []
@ -57,29 +57,25 @@ class TinyDnsBaseSource(BaseSource):
ttl = records[0][1]
except IndexError:
ttl = self.default_ttl
return {
'ttl': ttl,
'type': _type,
'values': values,
}
return {'ttl': ttl, 'type': _type, 'values': values}
def _data_for_TXT(self, _type, records):
values = []
for record in records:
new_value = record[0].encode('latin1').decode('unicode-escape') \
new_value = (
record[0]
.encode('latin1')
.decode('unicode-escape')
.replace(";", "\\;")
)
values.append(new_value)
try:
ttl = records[0][1]
except IndexError:
ttl = self.default_ttl
return {
'ttl': ttl,
'type': _type,
'values': values,
}
return {'ttl': ttl, 'type': _type, 'values': values}
def _data_for_CNAME(self, _type, records):
first = records[0]
@ -87,11 +83,7 @@ class TinyDnsBaseSource(BaseSource):
ttl = first[1]
except IndexError:
ttl = self.default_ttl
return {
'ttl': ttl,
'type': _type,
'value': f'{first[0]}.'
}
return {'ttl': ttl, 'type': _type, 'value': f'{first[0]}.'}
def _data_for_MX(self, _type, records):
try:
@ -101,10 +93,9 @@ class TinyDnsBaseSource(BaseSource):
return {
'ttl': ttl,
'type': _type,
'values': [{
'preference': r[1],
'exchange': f'{r[0]}.'
} for r in records]
'values': [
{'preference': r[1], 'exchange': f'{r[0]}.'} for r in records
],
}
def _data_for_NS(self, _type, records):
@ -115,12 +106,16 @@ class TinyDnsBaseSource(BaseSource):
return {
'ttl': ttl,
'type': _type,
'values': [f'{r[0]}.' for r in records]
'values': [f'{r[0]}.' for r in records],
}
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug(
'populate: name=%s, target=%s, lenient=%s',
zone.name,
target,
lenient,
)
before = len(zone.records)
@ -129,8 +124,9 @@ class TinyDnsBaseSource(BaseSource):
else:
self._populate_normal(zone, lenient)
self.log.info('populate: found %s records',
len(zone.records) - before)
self.log.info(
'populate: found %s records', len(zone.records) - before
)
def _populate_normal(self, zone, lenient):
type_map = {
@ -171,13 +167,16 @@ class TinyDnsBaseSource(BaseSource):
data_for = getattr(self, f'_data_for_{_type}')
data = data_for(_type, d)
if data:
record = Record.new(zone, name, data, source=self,
lenient=lenient)
record = Record.new(
zone, name, data, source=self, lenient=lenient
)
try:
zone.add_record(record, lenient=lenient)
except SubzoneRecordException:
self.log.debug('_populate_normal: skipping subzone '
'record=%s', record)
self.log.debug(
'_populate_normal: skipping subzone ' 'record=%s',
record,
)
def _populate_in_addr_arpa(self, zone, lenient):
name_re = re.compile(fr'(?P<name>.+)\.{zone.name[:-1]}$')
@ -209,11 +208,13 @@ class TinyDnsBaseSource(BaseSource):
ttl = self.default_ttl
name = match.group('name')
record = Record.new(zone, name, {
'ttl': ttl,
'type': 'PTR',
'value': value
}, source=self, lenient=lenient)
record = Record.new(
zone,
name,
{'ttl': ttl, 'type': 'PTR', 'value': value},
source=self,
lenient=lenient,
)
try:
zone.add_record(record, lenient=lenient)
except DuplicateRecordException:
@ -236,10 +237,15 @@ class TinyDnsFileSource(TinyDnsBaseSource):
NOTE: timestamps & lo fields are ignored if present.
'''
def __init__(self, id, directory, default_ttl=3600):
self.log = logging.getLogger(f'TinyDnsFileSource[{id}]')
self.log.debug('__init__: id=%s, directory=%s, default_ttl=%d', id,
directory, default_ttl)
self.log.debug(
'__init__: id=%s, directory=%s, default_ttl=%d',
id,
directory,
default_ttl,
)
super(TinyDnsFileSource, self).__init__(id, default_ttl)
self.directory = directory
self._cache = None

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from natsort import natsort_keygen
from yaml import SafeDumper, SafeLoader, load, dump
@ -16,7 +20,6 @@ _natsort_key = natsort_keygen()
# Found http://stackoverflow.com/a/21912744 which guided me on how to hook in
# here
class SortEnforcingLoader(SafeLoader):
def _construct(self, node):
self.flatten_mapping(node)
ret = self.construct_pairs(node)
@ -25,14 +28,18 @@ class SortEnforcingLoader(SafeLoader):
for key in keys:
expected = keys_sorted.pop(0)
if key != expected:
raise ConstructorError(None, None, 'keys out of order: '
f'expected {expected} got {key} at ' +
str(node.start_mark))
raise ConstructorError(
None,
None,
'keys out of order: '
f'expected {expected} got {key} at ' + str(node.start_mark),
)
return dict(ret)
SortEnforcingLoader.add_constructor(SortEnforcingLoader.DEFAULT_MAPPING_TAG,
SortEnforcingLoader._construct)
SortEnforcingLoader.add_constructor(
SortEnforcingLoader.DEFAULT_MAPPING_TAG, SortEnforcingLoader._construct
)
def safe_load(stream, enforce_order=True):
@ -62,7 +69,7 @@ def safe_dump(data, fh, **options):
'indent': 2,
'default_style': '',
'default_flow_style': False,
'explicit_start': True
'explicit_start': True,
}
kwargs.update(options)
dump(data, fh, SortingDumper, **kwargs)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from collections import defaultdict
from logging import getLogger
@ -72,13 +76,16 @@ class Zone(object):
if not lenient and any((name.endswith(sz) for sz in self.sub_zones)):
if name not in self.sub_zones:
# it's a record for something under a sub-zone
raise SubzoneRecordException(f'Record {record.fqdn} is under '
'a managed subzone')
raise SubzoneRecordException(
f'Record {record.fqdn} is under ' 'a managed subzone'
)
elif record._type != 'NS':
# It's a non NS record for exactly a sub-zone
raise SubzoneRecordException(f'Record {record.fqdn} a '
'managed sub-zone and not of '
'type NS')
raise SubzoneRecordException(
f'Record {record.fqdn} a '
'managed sub-zone and not of '
'type NS'
)
if replace:
# will remove it if it exists
@ -87,15 +94,20 @@ class Zone(object):
node = self._records[name]
if record in node:
# We already have a record at this node of this type
raise DuplicateRecordException(f'Duplicate record {record.fqdn}, '
f'type {record._type}')
elif not lenient and ((record._type == 'CNAME' and len(node) > 0) or
('CNAME' in [r._type for r in node])):
raise DuplicateRecordException(
f'Duplicate record {record.fqdn}, ' f'type {record._type}'
)
elif not lenient and (
(record._type == 'CNAME' and len(node) > 0)
or ('CNAME' in [r._type for r in node])
):
# We're adding a CNAME to existing records or adding to an existing
# CNAME
raise InvalidNodeException('Invalid state, CNAME at '
f'{record.fqdn} cannot coexist with '
'other records')
raise InvalidNodeException(
'Invalid state, CNAME at '
f'{record.fqdn} cannot coexist with '
'other records'
)
if record._type == 'NS' and record.name == '':
self._root_ns = record
@ -128,49 +140,69 @@ class Zone(object):
for record in self.records:
if record.ignored:
continue
elif len(record.included) > 0 and \
target.id not in record.included:
self.log.debug('changes: skipping record=%s %s - %s not'
' included ', record.fqdn, record._type,
target.id)
elif len(record.included) > 0 and target.id not in record.included:
self.log.debug(
'changes: skipping record=%s %s - %s not' ' included ',
record.fqdn,
record._type,
target.id,
)
continue
elif target.id in record.excluded:
self.log.debug('changes: skipping record=%s %s - %s '
'excluded ', record.fqdn, record._type,
target.id)
self.log.debug(
'changes: skipping record=%s %s - %s ' 'excluded ',
record.fqdn,
record._type,
target.id,
)
continue
try:
desired_record = desired_records[record]
if desired_record.ignored:
continue
elif len(desired_record.included) > 0 and \
target.id not in desired_record.included:
self.log.debug('changes: skipping record=%s %s - %s'
'not included ', record.fqdn, record._type,
target.id)
elif (
len(desired_record.included) > 0
and target.id not in desired_record.included
):
self.log.debug(
'changes: skipping record=%s %s - %s' 'not included ',
record.fqdn,
record._type,
target.id,
)
continue
elif target.id in desired_record.excluded:
continue
except KeyError:
if not target.supports(record):
self.log.debug('changes: skipping record=%s %s - %s does '
'not support it', record.fqdn, record._type,
target.id)
self.log.debug(
'changes: skipping record=%s %s - %s does '
'not support it',
record.fqdn,
record._type,
target.id,
)
continue
# record has been removed
self.log.debug('changes: zone=%s, removed record=%s', self,
record)
self.log.debug(
'changes: zone=%s, removed record=%s', self, record
)
changes.append(Delete(record))
else:
change = record.changes(desired_record, target)
if change:
self.log.debug('changes: zone=%s, modified\n'
' existing=%s,\n desired=%s', self,
record, desired_record)
self.log.debug(
'changes: zone=%s, modified\n'
' existing=%s,\n desired=%s',
self,
record,
desired_record,
)
changes.append(change)
else:
self.log.debug('changes: zone=%s, n.c. record=%s', self,
record)
self.log.debug(
'changes: zone=%s, n.c. record=%s', self, record
)
# Find additions, things that are in desired, but missing in ourselves.
# This uses set math and our special __hash__ and __cmp__ functions as
@ -178,22 +210,31 @@ class Zone(object):
for record in desired.records - self.records:
if record.ignored:
continue
elif len(record.included) > 0 and \
target.id not in record.included:
self.log.debug('changes: skipping record=%s %s - %s not'
' included ', record.fqdn, record._type,
target.id)
elif len(record.included) > 0 and target.id not in record.included:
self.log.debug(
'changes: skipping record=%s %s - %s not' ' included ',
record.fqdn,
record._type,
target.id,
)
continue
elif target.id in record.excluded:
self.log.debug('changes: skipping record=%s %s - %s '
'excluded ', record.fqdn, record._type,
target.id)
self.log.debug(
'changes: skipping record=%s %s - %s ' 'excluded ',
record.fqdn,
record._type,
target.id,
)
continue
if not target.supports(record):
self.log.debug('changes: skipping record=%s %s - %s does not '
'support it', record.fqdn, record._type,
target.id)
self.log.debug(
'changes: skipping record=%s %s - %s does not '
'support it',
record.fqdn,
record._type,
target.id,
)
continue
self.log.debug('changes: zone=%s, create record=%s', self, record)
changes.append(Create(record))

View File

@ -1,10 +1,12 @@
Pygments==2.11.2
attrs==21.4.0
black==22.3.0
bleach==4.1.0
build==0.7.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
click>=8.0.0
cmarkgfm==0.8.0
colorama==0.4.4
coverage==6.3.2
@ -13,13 +15,15 @@ idna==3.3
importlib-metadata==4.11.2
iniconfig==1.1.1
keyring==23.5.0
mypy-extensions>=0.4.3
packaging==21.3
pathspec>=0.9.0
pep517==0.12.0
pkginfo==1.8.2
platformdirs>=2
pluggy==1.0.0
pprintpp==0.4.0
py==1.11.0
pycodestyle==2.8.0
pycountry-convert==0.7.2
pycountry==22.3.5
pycparser==2.21
@ -37,6 +41,7 @@ rfc3986==2.0.0
tomli==2.0.1
tqdm==4.63.0
twine==3.8.0
typing-extensions>=3.10.0.0
urllib3==1.26.8
webencodings==0.5.1
zipp==3.7.0

View File

@ -24,6 +24,8 @@ echo "## begin #################################################################
# For now it's just lint...
echo "## lint ########################################################################"
script/lint
echo "## formatting ##################################################################"
script/format --check || (echo "Formatting check failed, run ./script/format" && exit 1)
echo "## tests/coverage ##############################################################"
script/coverage
echo "## complete ####################################################################"

9
script/format Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
SOURCES=$(find *.py octodns tests -name "*.py")
. env/bin/activate
black --line-length=80 --skip-string-normalization --skip-magic-trailing-comma "$@" $SOURCES

View File

@ -17,5 +17,4 @@ fi
SOURCES="*.py octodns/*.py octodns/*/*.py tests/*.py"
pycodestyle --ignore=E221,E241,E251,E722,E741,W504 $SOURCES
pyflakes $SOURCES

View File

@ -11,14 +11,7 @@ try:
except ImportError:
from distutils.core import find_packages, setup
cmds = (
'compare',
'dump',
'report',
'sync',
'validate',
'versions',
)
cmds = ('compare', 'dump', 'report', 'sync', 'validate', 'versions')
cmds_dir = join(dirname(__file__), 'octodns', 'cmds')
console_scripts = {
'octodns-{name} = octodns.cmds.{name}:main'.format(name=name)
@ -68,29 +61,24 @@ def version():
return f'{octodns.__VERSION__}+{sha}'
tests_require = (
'pytest>=6.2.5',
'pytest-cov>=3.0.0',
'pytest-network>=0.0.1',
)
tests_require = ('pytest>=6.2.5', 'pytest-cov>=3.0.0', 'pytest-network>=0.0.1')
setup(
author='Ross McFarland',
author_email='rwmcfa1@gmail.com',
description=octodns.__doc__,
entry_points={
'console_scripts': console_scripts,
},
entry_points={'console_scripts': console_scripts},
extras_require={
'dev': tests_require + (
'dev': tests_require
+ (
'black>=22.3.0',
'build>=0.7.0',
'pycodestyle>=2.6.0',
'pycountry>=19.8.18',
'pycountry-convert>=0.7.2',
'pyflakes>=2.2.0',
'readme_renderer[md]>=26.0',
'twine>=3.4.2',
),
)
},
install_requires=(
'PyYaml>=4.2b1',

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from shutil import rmtree
from tempfile import mkdtemp
@ -15,7 +19,6 @@ from octodns.provider.yaml import YamlProvider
class SimpleSource(object):
def __init__(self, id='test'):
pass
@ -78,13 +81,11 @@ class DynamicProvider(object):
class NoSshFpProvider(SimpleProvider):
def supports(self, record):
return record._type != 'SSHFP'
class TemporaryDirectory(object):
def __init__(self, delete_on_exit=True):
self.delete_on_exit = delete_on_exit
@ -100,7 +101,6 @@ class TemporaryDirectory(object):
class WantsConfigProcessor(BaseProcessor):
def __init__(self, name, some_config):
super(WantsConfigProcessor, self).__init__(name)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
@ -11,11 +15,8 @@ from octodns.equality import EqualityTupleMixin
class TestEqualityTupleMixin(TestCase):
def test_basics(self):
class Simple(EqualityTupleMixin):
def __init__(self, a, b, c):
self.a = a
self.b = b
@ -60,7 +61,6 @@ class TestEqualityTupleMixin(TestCase):
self.assertTrue(one >= same)
def test_not_implemented(self):
class MissingMethod(EqualityTupleMixin):
pass

View File

@ -2,15 +2,23 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from os import environ
from os.path import dirname, join
from octodns import __VERSION__
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \
ManagerException
from octodns.manager import (
_AggregateTarget,
MainThreadExecutor,
Manager,
ManagerException,
)
from octodns.processor.base import BaseProcessor
from octodns.record import Create, Delete, Record
from octodns.yaml import safe_load
@ -19,8 +27,14 @@ from octodns.zone import Zone
from unittest import TestCase
from unittest.mock import MagicMock, patch
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \
PlannableProvider, SimpleProvider, TemporaryDirectory
from helpers import (
DynamicProvider,
GeoProvider,
NoSshFpProvider,
PlannableProvider,
SimpleProvider,
TemporaryDirectory,
)
config_dir = join(dirname(__file__), 'config')
@ -30,7 +44,6 @@ def get_config_filename(which):
class TestManager(TestCase):
def test_missing_provider_class(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('missing-provider-class.yaml')).sync()
@ -43,14 +56,16 @@ class TestManager(TestCase):
def test_bad_provider_class_module(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('bad-provider-class-module.yaml')) \
.sync()
Manager(
get_config_filename('bad-provider-class-module.yaml')
).sync()
self.assertTrue('Unknown provider class' in str(ctx.exception))
def test_bad_provider_class_no_module(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('bad-provider-class-no-module.yaml')) \
.sync()
Manager(
get_config_filename('bad-provider-class-no-module.yaml')
).sync()
self.assertTrue('Unknown provider class' in str(ctx.exception))
def test_missing_provider_config(self):
@ -66,141 +81,160 @@ class TestManager(TestCase):
def test_missing_source(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')) \
.sync(['missing.sources.'])
Manager(get_config_filename('provider-problems.yaml')).sync(
['missing.sources.']
)
self.assertTrue('missing sources' in str(ctx.exception))
def test_missing_targets(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')) \
.sync(['missing.targets.'])
Manager(get_config_filename('provider-problems.yaml')).sync(
['missing.targets.']
)
self.assertTrue('missing targets' in str(ctx.exception))
def test_unknown_source(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')) \
.sync(['unknown.source.'])
Manager(get_config_filename('provider-problems.yaml')).sync(
['unknown.source.']
)
self.assertTrue('unknown source' in str(ctx.exception))
def test_unknown_target(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')) \
.sync(['unknown.target.'])
Manager(get_config_filename('provider-problems.yaml')).sync(
['unknown.target.']
)
self.assertTrue('unknown target' in str(ctx.exception))
def test_bad_plan_output_class(self):
with self.assertRaises(ManagerException) as ctx:
name = 'bad-plan-output-missing-class.yaml'
Manager(get_config_filename(name)).sync()
self.assertEqual('plan_output bad is missing class',
str(ctx.exception))
self.assertEqual('plan_output bad is missing class', str(ctx.exception))
def test_bad_plan_output_config(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('bad-plan-output-config.yaml')).sync()
self.assertEqual('Incorrect plan_output config for bad',
str(ctx.exception))
self.assertEqual(
'Incorrect plan_output config for bad', str(ctx.exception)
)
def test_source_only_as_a_target(self):
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')) \
.sync(['not.targetable.'])
self.assertTrue('does not support targeting' in
str(ctx.exception))
Manager(get_config_filename('provider-problems.yaml')).sync(
['not.targetable.']
)
self.assertTrue('does not support targeting' in str(ctx.exception))
def test_always_dry_run(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
tc = Manager(get_config_filename('always-dry-run.yaml')) \
.sync(dry_run=False)
tc = Manager(get_config_filename('always-dry-run.yaml')).sync(
dry_run=False
)
# only the stuff from subzone, unit.tests. is always-dry-run
self.assertEqual(3, tc)
def test_simple(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False)
tc = Manager(get_config_filename('simple.yaml')).sync(dry_run=False)
self.assertEqual(28, tc)
# try with just one of the zones
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False, eligible_zones=['unit.tests.'])
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, eligible_zones=['unit.tests.']
)
self.assertEqual(22, tc)
# the subzone, with 2 targets
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False, eligible_zones=['subzone.unit.tests.'])
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, eligible_zones=['subzone.unit.tests.']
)
self.assertEqual(6, tc)
# and finally the empty zone
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False, eligible_zones=['empty.'])
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, eligible_zones=['empty.']
)
self.assertEqual(0, tc)
# Again with force
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False, force=True)
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, force=True
)
self.assertEqual(28, tc)
# Again with max_workers = 1
tc = Manager(get_config_filename('simple.yaml'), max_workers=1) \
.sync(dry_run=False, force=True)
tc = Manager(
get_config_filename('simple.yaml'), max_workers=1
).sync(dry_run=False, force=True)
self.assertEqual(28, tc)
# Include meta
tc = Manager(get_config_filename('simple.yaml'), max_workers=1,
include_meta=True) \
.sync(dry_run=False, force=True)
tc = Manager(
get_config_filename('simple.yaml'),
max_workers=1,
include_meta=True,
).sync(dry_run=False, force=True)
self.assertEqual(33, tc)
def test_eligible_sources(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
# Only allow a target that doesn't exist
tc = Manager(get_config_filename('simple.yaml')) \
.sync(eligible_sources=['foo'])
tc = Manager(get_config_filename('simple.yaml')).sync(
eligible_sources=['foo']
)
self.assertEqual(0, tc)
def test_eligible_targets(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
# Only allow a target that doesn't exist
tc = Manager(get_config_filename('simple.yaml')) \
.sync(eligible_targets=['foo'])
tc = Manager(get_config_filename('simple.yaml')).sync(
eligible_targets=['foo']
)
self.assertEqual(0, tc)
def test_aliases(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
# Alias zones with a valid target.
tc = Manager(get_config_filename('simple-alias-zone.yaml')) \
.sync()
tc = Manager(get_config_filename('simple-alias-zone.yaml')).sync()
self.assertEqual(0, tc)
# Alias zone with an invalid target.
with self.assertRaises(ManagerException) as ctx:
tc = Manager(get_config_filename('unknown-source-zone.yaml')) \
.sync()
self.assertEqual('Invalid alias zone alias.tests.: source zone '
'does-not-exists.tests. does not exist',
str(ctx.exception))
tc = Manager(
get_config_filename('unknown-source-zone.yaml')
).sync()
self.assertEqual(
'Invalid alias zone alias.tests.: source zone '
'does-not-exists.tests. does not exist',
str(ctx.exception),
)
# Alias zone that points to another alias zone.
with self.assertRaises(ManagerException) as ctx:
tc = Manager(get_config_filename('alias-zone-loop.yaml')) \
.sync()
self.assertEqual('Invalid alias zone alias-loop.tests.: source '
'zone alias.tests. is an alias zone',
str(ctx.exception))
tc = Manager(get_config_filename('alias-zone-loop.yaml')).sync()
self.assertEqual(
'Invalid alias zone alias-loop.tests.: source '
'zone alias.tests. is an alias zone',
str(ctx.exception),
)
# Sync an alias without the zone it refers to
with self.assertRaises(ManagerException) as ctx:
tc = Manager(get_config_filename('simple-alias-zone.yaml')) \
.sync(eligible_zones=["alias.tests."])
self.assertEqual('Zone alias.tests. cannot be sync without zone '
'unit.tests. sinced it is aliased',
str(ctx.exception))
tc = Manager(
get_config_filename('simple-alias-zone.yaml')
).sync(eligible_zones=["alias.tests."])
self.assertEqual(
'Zone alias.tests. cannot be sync without zone '
'unit.tests. sinced it is aliased',
str(ctx.exception),
)
def test_compare(self):
with TemporaryDirectory() as tmpdir:
@ -223,9 +257,9 @@ class TestManager(TestCase):
self.assertEqual(23, len(changes))
# Compound sources with varying support
changes = manager.compare(['in', 'nosshfp'],
['dump'],
'unit.tests.')
changes = manager.compare(
['in', 'nosshfp'], ['dump'], 'unit.tests.'
)
self.assertEqual(22, len(changes))
with self.assertRaises(ManagerException) as ctx:
@ -249,8 +283,9 @@ class TestManager(TestCase):
# exception
with self.assertRaises(AttributeError) as ctx:
at.FOO
self.assertEqual('_AggregateTarget object has no attribute FOO',
str(ctx.exception))
self.assertEqual(
'_AggregateTarget object has no attribute FOO', str(ctx.exception)
)
self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_GEO)
self.assertFalse(_AggregateTarget([simple, geo]).SUPPORTS_GEO)
@ -263,15 +298,19 @@ class TestManager(TestCase):
self.assertTrue(_AggregateTarget([dynamic, dynamic]).SUPPORTS_DYNAMIC)
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'sshfp', {
'ttl': 60,
'type': 'SSHFP',
'value': {
'algorithm': 1,
'fingerprint_type': 1,
'fingerprint': 'abcdefg',
record = Record.new(
zone,
'sshfp',
{
'ttl': 60,
'type': 'SSHFP',
'value': {
'algorithm': 1,
'fingerprint_type': 1,
'fingerprint': 'abcdefg',
},
},
})
)
self.assertTrue(simple.supports(record))
self.assertFalse(nosshfp.supports(record))
self.assertTrue(_AggregateTarget([simple, simple]).supports(record))
@ -283,8 +322,9 @@ class TestManager(TestCase):
manager = Manager(get_config_filename('simple.yaml'))
with self.assertRaises(ManagerException) as ctx:
manager.dump('unit.tests.', tmpdir.dirname, False, False,
'nope')
manager.dump(
'unit.tests.', tmpdir.dirname, False, False, 'nope'
)
self.assertEqual('Unknown source: nope', str(ctx.exception))
manager.dump('unit.tests.', tmpdir.dirname, False, False, 'in')
@ -292,8 +332,9 @@ class TestManager(TestCase):
# make sure this fails with an IOError and not a KeyError when
# trying to find sub zones
with self.assertRaises(IOError):
manager.dump('unknown.zone.', tmpdir.dirname, False, False,
'in')
manager.dump(
'unknown.zone.', tmpdir.dirname, False, False, 'in'
)
def test_dump_empty(self):
with TemporaryDirectory() as tmpdir:
@ -312,8 +353,7 @@ class TestManager(TestCase):
manager = Manager(get_config_filename('simple-split.yaml'))
with self.assertRaises(ManagerException) as ctx:
manager.dump('unit.tests.', tmpdir.dirname, False, True,
'nope')
manager.dump('unit.tests.', tmpdir.dirname, False, True, 'nope')
self.assertEqual('Unknown source: nope', str(ctx.exception))
manager.dump('unit.tests.', tmpdir.dirname, False, True, 'in')
@ -321,41 +361,46 @@ class TestManager(TestCase):
# make sure this fails with an OSError and not a KeyError when
# trying to find sub zones
with self.assertRaises(OSError):
manager.dump('unknown.zone.', tmpdir.dirname, False, True,
'in')
manager.dump('unknown.zone.', tmpdir.dirname, False, True, 'in')
def test_validate_configs(self):
Manager(get_config_filename('simple-validate.yaml')).validate_configs()
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('missing-sources.yaml')) \
.validate_configs()
Manager(
get_config_filename('missing-sources.yaml')
).validate_configs()
self.assertTrue('missing sources' in str(ctx.exception))
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('unknown-provider.yaml')) \
.validate_configs()
Manager(
get_config_filename('unknown-provider.yaml')
).validate_configs()
self.assertTrue('unknown source' in str(ctx.exception))
# Alias zone using an invalid source zone.
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('unknown-source-zone.yaml')) \
.validate_configs()
Manager(
get_config_filename('unknown-source-zone.yaml')
).validate_configs()
self.assertTrue('does not exist' in str(ctx.exception))
# Alias zone that points to another alias zone.
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('alias-zone-loop.yaml')) \
.validate_configs()
Manager(
get_config_filename('alias-zone-loop.yaml')
).validate_configs()
self.assertTrue('is an alias zone' in str(ctx.exception))
# Valid config file using an alias zone.
Manager(get_config_filename('simple-alias-zone.yaml')) \
.validate_configs()
Manager(
get_config_filename('simple-alias-zone.yaml')
).validate_configs()
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('unknown-processor.yaml')) \
.validate_configs()
Manager(
get_config_filename('unknown-processor.yaml')
).validate_configs()
self.assertTrue('unknown processor' in str(ctx.exception))
def test_get_zone(self):
@ -366,8 +411,9 @@ class TestManager(TestCase):
self.assertTrue('missing ending dot' in str(ctx.exception))
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('simple.yaml')) \
.get_zone('unknown-zone.tests.')
Manager(get_config_filename('simple.yaml')).get_zone(
'unknown-zone.tests.'
)
self.assertTrue('Unknown zone name' in str(ctx.exception))
def test_populate_lenient_fallback(self):
@ -377,7 +423,6 @@ class TestManager(TestCase):
manager = Manager(get_config_filename('simple.yaml'))
class NoLenient(SimpleProvider):
def populate(self, zone):
pass
@ -385,14 +430,12 @@ class TestManager(TestCase):
manager._populate_and_plan('unit.tests.', [], [NoLenient()], [])
class OtherType(SimpleProvider):
def populate(self, zone, lenient=False):
raise TypeError('something else')
# This will blow up, we don't fallback for source
with self.assertRaises(TypeError) as ctx:
manager._populate_and_plan('unit.tests.', [], [OtherType()],
[])
manager._populate_and_plan('unit.tests.', [], [OtherType()], [])
self.assertEqual('something else', str(ctx.exception))
def test_plan_processors_fallback(self):
@ -402,23 +445,19 @@ class TestManager(TestCase):
manager = Manager(get_config_filename('simple.yaml'))
class NoProcessors(SimpleProvider):
def plan(self, zone):
pass
# This should be ok, we'll fall back to not passing it
manager._populate_and_plan('unit.tests.', [], [],
[NoProcessors()])
manager._populate_and_plan('unit.tests.', [], [], [NoProcessors()])
class OtherType(SimpleProvider):
def plan(self, zone, processors):
raise TypeError('something else')
# This will blow up, we don't fallback for source
with self.assertRaises(TypeError) as ctx:
manager._populate_and_plan('unit.tests.', [], [],
[OtherType()])
manager._populate_and_plan('unit.tests.', [], [], [OtherType()])
self.assertEqual('something else', str(ctx.exception))
@patch('octodns.manager.Manager._get_named_class')
@ -429,8 +468,9 @@ class TestManager(TestCase):
mock.return_value = (plan_output_class_mock, 'ignored', 'ignored')
fh_mock = MagicMock()
Manager(get_config_filename('plan-output-filehandle.yaml')
).sync(plan_output_fh=fh_mock)
Manager(get_config_filename('plan-output-filehandle.yaml')).sync(
plan_output_fh=fh_mock
)
# Since we only care about the fh kwarg, and different _PlanOutputs are
# are free to require arbitrary kwargs anyway, we concern ourselves
@ -449,18 +489,22 @@ class TestManager(TestCase):
with self.assertRaises(ManagerException) as ctx:
# This zone specifies a non-existent processor
manager.sync(['bad.unit.tests.'])
self.assertTrue('Zone bad.unit.tests., unknown processor: '
'doesnt-exist' in str(ctx.exception))
self.assertTrue(
'Zone bad.unit.tests., unknown processor: '
'doesnt-exist' in str(ctx.exception)
)
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('processors-missing-class.yaml'))
self.assertTrue('Processor no-class is missing class' in
str(ctx.exception))
self.assertTrue(
'Processor no-class is missing class' in str(ctx.exception)
)
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('processors-wants-config.yaml'))
self.assertTrue('Incorrect processor config for wants-config' in
str(ctx.exception))
self.assertTrue(
'Incorrect processor config for wants-config' in str(ctx.exception)
)
def test_processors(self):
manager = Manager(get_config_filename('simple.yaml'))
@ -468,23 +512,21 @@ class TestManager(TestCase):
targets = [PlannableProvider('prov')]
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
# muck with sources
class MockProcessor(BaseProcessor):
def process_source_zone(self, zone, sources):
zone = zone.copy()
zone.add_record(record)
return zone
mock = MockProcessor('mock')
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [],
targets)
plans, zone = manager._populate_and_plan(
'unit.tests.', [mock], [], targets
)
# Our mock was called and added the record
self.assertEqual(record, list(zone.records)[0])
# We got a create for the thing added to the expected state (source)
@ -492,15 +534,15 @@ class TestManager(TestCase):
# muck with targets
class MockProcessor(BaseProcessor):
def process_target_zone(self, zone, target):
zone = zone.copy()
zone.add_record(record)
return zone
mock = MockProcessor('mock')
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [],
targets)
plans, zone = manager._populate_and_plan(
'unit.tests.', [mock], [], targets
)
# No record added since it's target this time
self.assertFalse(zone.records)
# We got a delete for the thing added to the existing state (target)
@ -508,7 +550,6 @@ class TestManager(TestCase):
# muck with plans
class MockProcessor(BaseProcessor):
def process_target_zone(self, zone, target):
zone = zone.copy()
zone.add_record(record)
@ -519,8 +560,9 @@ class TestManager(TestCase):
plans.changes.pop(0)
mock = MockProcessor('mock')
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [],
targets)
plans, zone = manager._populate_and_plan(
'unit.tests.', [mock], [], targets
)
# We planned a delete again, but this time removed it from the plan, so
# no plans
self.assertFalse(plans)
@ -534,23 +576,28 @@ class TestManager(TestCase):
dummy_module = DummyModule()
# use importlib.metadata.version
self.assertTrue(__VERSION__,
manager._try_version('octodns',
module=dummy_module,
version='1.2.3'))
self.assertTrue(
__VERSION__,
manager._try_version(
'octodns', module=dummy_module, version='1.2.3'
),
)
# use module
self.assertTrue(manager._try_version('doesnt-exist',
module=dummy_module))
self.assertTrue(
manager._try_version('doesnt-exist', module=dummy_module)
)
# fall back to version, preferred over module
self.assertEqual('1.2.3', manager._try_version('doesnt-exist',
module=dummy_module,
version='1.2.3', ))
self.assertEqual(
'1.2.3',
manager._try_version(
'doesnt-exist', module=dummy_module, version='1.2.3'
),
)
class TestMainThreadExecutor(TestCase):
def test_success(self):
mte = MainThreadExecutor()

View File

@ -2,15 +2,25 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from io import StringIO
from logging import getLogger
from unittest import TestCase
from octodns.provider.plan import Plan, PlanHtml, PlanLogger, PlanMarkdown, \
RootNsChange, TooMuchChange
from octodns.provider.plan import (
Plan,
PlanHtml,
PlanLogger,
PlanMarkdown,
RootNsChange,
TooMuchChange,
)
from octodns.record import Create, Delete, Record, Update
from octodns.zone import Zone
@ -19,32 +29,41 @@ from helpers import SimpleProvider
simple = SimpleProvider()
zone = Zone('unit.tests.', [])
existing = Record.new(zone, 'a', {
'ttl': 300,
'type': 'A',
# This matches the zone data above, one to swap, one to leave
'values': ['1.1.1.1', '2.2.2.2'],
})
new = Record.new(zone, 'a', {
'geo': {
'AF': ['5.5.5.5'],
'NA-US': ['6.6.6.6']
existing = Record.new(
zone,
'a',
{
'ttl': 300,
'type': 'A',
# This matches the zone data above, one to swap, one to leave
'values': ['1.1.1.1', '2.2.2.2'],
},
'ttl': 300,
'type': 'A',
# This leaves one, swaps ones, and adds one
'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'],
}, simple)
create = Create(Record.new(zone, 'b', {
'ttl': 60,
'type': 'CNAME',
'value': 'foo.unit.tests.'
}, simple))
create2 = Create(Record.new(zone, 'c', {
'ttl': 60,
'type': 'CNAME',
'value': 'foo.unit.tests.'
}))
)
new = Record.new(
zone,
'a',
{
'geo': {'AF': ['5.5.5.5'], 'NA-US': ['6.6.6.6']},
'ttl': 300,
'type': 'A',
# This leaves one, swaps ones, and adds one
'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'],
},
simple,
)
create = Create(
Record.new(
zone,
'b',
{'ttl': 60, 'type': 'CNAME', 'value': 'foo.unit.tests.'},
simple,
)
)
create2 = Create(
Record.new(
zone, 'c', {'ttl': 60, 'type': 'CNAME', 'value': 'foo.unit.tests.'}
)
)
update = Update(existing, new)
delete = Delete(new)
changes = [create, create2, delete, update]
@ -55,23 +74,18 @@ plans = [
class TestPlanSortsChanges(TestCase):
def test_plan_sorts_changes_pass_to_it(self):
# we aren't worried about the details of the sorting, that's tested in
# test_octodns_record's TestChanges. We just want to make sure that the
# changes are sorted at all.
zone = Zone('unit.tests.', [])
record_a_1 = Record.new(zone, '1', {
'type': 'A',
'ttl': 30,
'value': '1.2.3.4',
})
record_a_1 = Record.new(
zone, '1', {'type': 'A', 'ttl': 30, 'value': '1.2.3.4'}
)
create_a_1 = Create(record_a_1)
record_a_2 = Record.new(zone, '2', {
'type': 'A',
'ttl': 30,
'value': '1.2.3.4',
})
record_a_2 = Record.new(
zone, '2', {'type': 'A', 'ttl': 30, 'value': '1.2.3.4'}
)
create_a_2 = Create(record_a_2)
# passed in reverse of expected order
@ -80,16 +94,13 @@ class TestPlanSortsChanges(TestCase):
class TestPlanLogger(TestCase):
def test_invalid_level(self):
with self.assertRaises(Exception) as ctx:
PlanLogger('invalid', 'not-a-level')
self.assertEqual('Unsupported level: not-a-level', str(ctx.exception))
def test_create(self):
class MockLogger(object):
def __init__(self):
self.out = StringIO()
@ -99,8 +110,10 @@ class TestPlanLogger(TestCase):
log = MockLogger()
PlanLogger('logger').run(log, plans)
out = log.out.getvalue()
self.assertTrue('Summary: Creates=2, Updates=1, '
'Deletes=1, Existing Records=0' in out)
self.assertTrue(
'Summary: Creates=2, Updates=1, '
'Deletes=1, Existing Records=0' in out
)
class TestPlanHtml(TestCase):
@ -115,8 +128,10 @@ class TestPlanHtml(TestCase):
out = StringIO()
PlanHtml('html').run(plans, fh=out)
out = out.getvalue()
self.assertTrue(' <td colspan=6>Summary: Creates=2, Updates=1, '
'Deletes=1, Existing Records=0</td>' in out)
self.assertTrue(
' <td colspan=6>Summary: Creates=2, Updates=1, '
'Deletes=1, Existing Records=0</td>' in out
)
class TestPlanMarkdown(TestCase):
@ -139,7 +154,6 @@ class TestPlanMarkdown(TestCase):
class HelperPlan(Plan):
def __init__(self, *args, min_existing=0, **kwargs):
super().__init__(*args, **kwargs)
self.MIN_EXISTING_RECORDS = min_existing
@ -147,26 +161,18 @@ class HelperPlan(Plan):
class TestPlanSafety(TestCase):
existing = Zone('unit.tests.', [])
record_1 = Record.new(existing, '1', data={
'type': 'A',
'ttl': 42,
'value': '1.2.3.4',
})
record_2 = Record.new(existing, '2', data={
'type': 'A',
'ttl': 42,
'value': '1.2.3.4',
})
record_3 = Record.new(existing, '3', data={
'type': 'A',
'ttl': 42,
'value': '1.2.3.4',
})
record_4 = Record.new(existing, '4', data={
'type': 'A',
'ttl': 42,
'value': '1.2.3.4',
})
record_1 = Record.new(
existing, '1', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
)
record_2 = Record.new(
existing, '2', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
)
record_3 = Record.new(
existing, '3', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
)
record_4 = Record.new(
existing, '4', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
)
def test_too_many_updates(self):
existing = self.existing.copy()
@ -267,11 +273,15 @@ class TestPlanSafety(TestCase):
plan.raise_if_unsafe()
# Add a change to a non-root NS record, we're OK
ns_record = Record.new(existing, 'sub', data={
'type': 'NS',
'ttl': 43,
'values': ('ns1.unit.tests.', 'ns1.unit.tests.'),
})
ns_record = Record.new(
existing,
'sub',
data={
'type': 'NS',
'ttl': 43,
'values': ('ns1.unit.tests.', 'ns1.unit.tests.'),
},
)
changes.append(Delete(ns_record))
plan = HelperPlan(existing, None, changes, True)
plan.raise_if_unsafe()
@ -279,11 +289,15 @@ class TestPlanSafety(TestCase):
changes.pop(-1)
# Delete the root NS record and we get an unsafe
root_ns_record = Record.new(existing, '', data={
'type': 'NS',
'ttl': 43,
'values': ('ns3.unit.tests.', 'ns4.unit.tests.'),
})
root_ns_record = Record.new(
existing,
'',
data={
'type': 'NS',
'ttl': 43,
'values': ('ns3.unit.tests.', 'ns4.unit.tests.'),
},
)
changes.append(Delete(root_ns_record))
plan = HelperPlan(existing, None, changes, True)
with self.assertRaises(RootNsChange) as ctx:

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
@ -13,46 +17,43 @@ from octodns.zone import Zone
zone = Zone('unit.tests.', [])
records = {
'root-unowned': Record.new(zone, '_acme-challenge', {
'ttl': 30,
'type': 'TXT',
'value': 'magic bit',
}),
'sub-unowned': Record.new(zone, '_acme-challenge.sub-unowned', {
'ttl': 30,
'type': 'TXT',
'value': 'magic bit',
}),
'not-txt': Record.new(zone, '_acme-challenge.not-txt', {
'ttl': 30,
'type': 'AAAA',
'value': '::1',
}),
'not-acme': Record.new(zone, 'not-acme', {
'ttl': 30,
'type': 'TXT',
'value': 'Hello World!',
}),
'managed': Record.new(zone, '_acme-challenge.managed', {
'ttl': 30,
'type': 'TXT',
'value': 'magic bit',
}),
'owned': Record.new(zone, '_acme-challenge.owned', {
'ttl': 30,
'type': 'TXT',
'values': ['*octoDNS*', 'magic bit'],
}),
'going-away': Record.new(zone, '_acme-challenge.going-away', {
'ttl': 30,
'type': 'TXT',
'values': ['*octoDNS*', 'magic bit'],
}),
'root-unowned': Record.new(
zone,
'_acme-challenge',
{'ttl': 30, 'type': 'TXT', 'value': 'magic bit'},
),
'sub-unowned': Record.new(
zone,
'_acme-challenge.sub-unowned',
{'ttl': 30, 'type': 'TXT', 'value': 'magic bit'},
),
'not-txt': Record.new(
zone,
'_acme-challenge.not-txt',
{'ttl': 30, 'type': 'AAAA', 'value': '::1'},
),
'not-acme': Record.new(
zone, 'not-acme', {'ttl': 30, 'type': 'TXT', 'value': 'Hello World!'}
),
'managed': Record.new(
zone,
'_acme-challenge.managed',
{'ttl': 30, 'type': 'TXT', 'value': 'magic bit'},
),
'owned': Record.new(
zone,
'_acme-challenge.owned',
{'ttl': 30, 'type': 'TXT', 'values': ['*octoDNS*', 'magic bit']},
),
'going-away': Record.new(
zone,
'_acme-challenge.going-away',
{'ttl': 30, 'type': 'TXT', 'values': ['*octoDNS*', 'magic bit']},
),
}
class TestAcmeMangingProcessor(TestCase):
def test_process_zones(self):
acme = AcmeMangingProcessor('acme')
@ -64,11 +65,10 @@ class TestAcmeMangingProcessor(TestCase):
source.add_record(records['managed'])
got = acme.process_source_zone(source)
self.assertEqual([
'_acme-challenge.managed',
'_acme-challenge.not-txt',
'not-acme',
], sorted([r.name for r in got.records]))
self.assertEqual(
['_acme-challenge.managed', '_acme-challenge.not-txt', 'not-acme'],
sorted([r.name for r in got.records]),
)
managed = None
for record in got.records:
if record.name.endswith('managed'):
@ -93,10 +93,13 @@ class TestAcmeMangingProcessor(TestCase):
existing.add_record(records['going-away'])
got = acme.process_target_zone(existing)
self.assertEqual([
'_acme-challenge.going-away',
'_acme-challenge.managed',
'_acme-challenge.not-txt',
'_acme-challenge.owned',
'not-acme'
], sorted([r.name for r in got.records]))
self.assertEqual(
[
'_acme-challenge.going-away',
'_acme-challenge.managed',
'_acme-challenge.not-txt',
'_acme-challenge.owned',
'not-acme',
],
sorted([r.name for r in got.records]),
)

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestAwsAcmMangingProcessor(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.processor.awsacm import AwsAcmMangingProcessor
AwsAcmMangingProcessor

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
@ -13,37 +17,20 @@ from octodns.zone import Zone
zone = Zone('unit.tests.', [])
for record in [
Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
}),
Record.new(zone, 'aaaa', {
'ttl': 30,
'type': 'AAAA',
'value': '::1',
}),
Record.new(zone, 'txt', {
'ttl': 30,
'type': 'TXT',
'value': 'Hello World!',
}),
Record.new(zone, 'a2', {
'ttl': 30,
'type': 'A',
'value': '2.3.4.5',
}),
Record.new(zone, 'txt2', {
'ttl': 30,
'type': 'TXT',
'value': 'That will do',
}),
Record.new(zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}),
Record.new(zone, 'aaaa', {'ttl': 30, 'type': 'AAAA', 'value': '::1'}),
Record.new(
zone, 'txt', {'ttl': 30, 'type': 'TXT', 'value': 'Hello World!'}
),
Record.new(zone, 'a2', {'ttl': 30, 'type': 'A', 'value': '2.3.4.5'}),
Record.new(
zone, 'txt2', {'ttl': 30, 'type': 'TXT', 'value': 'That will do'}
),
]:
zone.add_record(record)
class TestTypeAllowListFilter(TestCase):
def test_basics(self):
filter_a = TypeAllowlistFilter('only-a', set(('A')))
@ -56,35 +43,36 @@ class TestTypeAllowListFilter(TestCase):
filter_txt = TypeAllowlistFilter('only-txt', ['TXT'])
got = filter_txt.process_target_zone(zone.copy())
self.assertEqual(['txt', 'txt2'],
sorted([r.name for r in got.records]))
self.assertEqual(['txt', 'txt2'], sorted([r.name for r in got.records]))
filter_a_aaaa = TypeAllowlistFilter('only-aaaa', set(('A', 'AAAA')))
got = filter_a_aaaa.process_target_zone(zone.copy())
self.assertEqual(['a', 'a2', 'aaaa'],
sorted([r.name for r in got.records]))
self.assertEqual(
['a', 'a2', 'aaaa'], sorted([r.name for r in got.records])
)
class TestTypeRejectListFilter(TestCase):
def test_basics(self):
filter_a = TypeRejectlistFilter('not-a', set(('A')))
got = filter_a.process_source_zone(zone.copy())
self.assertEqual(['aaaa', 'txt', 'txt2'],
sorted([r.name for r in got.records]))
self.assertEqual(
['aaaa', 'txt', 'txt2'], sorted([r.name for r in got.records])
)
filter_aaaa = TypeRejectlistFilter('not-aaaa', ('AAAA',))
got = filter_aaaa.process_source_zone(zone.copy())
self.assertEqual(['a', 'a2', 'txt', 'txt2'],
sorted([r.name for r in got.records]))
self.assertEqual(
['a', 'a2', 'txt', 'txt2'], sorted([r.name for r in got.records])
)
filter_txt = TypeRejectlistFilter('not-txt', ['TXT'])
got = filter_txt.process_target_zone(zone.copy())
self.assertEqual(['a', 'a2', 'aaaa'],
sorted([r.name for r in got.records]))
self.assertEqual(
['a', 'a2', 'aaaa'], sorted([r.name for r in got.records])
)
filter_a_aaaa = TypeRejectlistFilter('not-a-aaaa', set(('A', 'AAAA')))
got = filter_a_aaaa.process_target_zone(zone.copy())
self.assertEqual(['txt', 'txt2'],
sorted([r.name for r in got.records]))
self.assertEqual(['txt', 'txt2'], sorted([r.name for r in got.records]))

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
@ -17,57 +21,40 @@ from helpers import PlannableProvider
zone = Zone('unit.tests.', [])
records = {}
for record in [
Record.new(zone, '', {
'ttl': 30,
'type': 'A',
'values': [
'1.2.3.4',
'5.6.7.8',
],
}),
Record.new(zone, 'the-a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
}),
Record.new(zone, 'the-aaaa', {
'ttl': 30,
'type': 'AAAA',
'value': '::1',
}),
Record.new(zone, 'the-txt', {
'ttl': 30,
'type': 'TXT',
'value': 'Hello World!',
}),
Record.new(zone, '*', {
'ttl': 30,
'type': 'A',
'value': '4.3.2.1',
}),
Record.new(
zone, '', {'ttl': 30, 'type': 'A', 'values': ['1.2.3.4', '5.6.7.8']}
),
Record.new(zone, 'the-a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}),
Record.new(zone, 'the-aaaa', {'ttl': 30, 'type': 'AAAA', 'value': '::1'}),
Record.new(
zone, 'the-txt', {'ttl': 30, 'type': 'TXT', 'value': 'Hello World!'}
),
Record.new(zone, '*', {'ttl': 30, 'type': 'A', 'value': '4.3.2.1'}),
]:
records[record.name] = record
zone.add_record(record)
class TestOwnershipProcessor(TestCase):
def test_process_source_zone(self):
ownership = OwnershipProcessor('ownership')
got = ownership.process_source_zone(zone.copy())
self.assertEqual([
'',
'*',
'_owner.a',
'_owner.a._wildcard',
'_owner.a.the-a',
'_owner.aaaa.the-aaaa',
'_owner.txt.the-txt',
'the-a',
'the-aaaa',
'the-txt',
], sorted([r.name for r in got.records]))
self.assertEqual(
[
'',
'*',
'_owner.a',
'_owner.a._wildcard',
'_owner.a.the-a',
'_owner.aaaa.the-aaaa',
'_owner.txt.the-txt',
'the-a',
'the-aaaa',
'the-txt',
],
sorted([r.name for r in got.records]),
)
found = False
for record in got.records:
@ -101,11 +88,9 @@ class TestOwnershipProcessor(TestCase):
# Something extra exists and doesn't have ownership TXT, leave it
# alone, we don't own it.
extra_a = Record.new(zone, 'extra-a', {
'ttl': 30,
'type': 'A',
'value': '4.4.4.4',
})
extra_a = Record.new(
zone, 'extra-a', {'ttl': 30, 'type': 'A', 'value': '4.4.4.4'}
)
plan.existing.add_record(extra_a)
# If we'd done a "real" plan we'd have a delete for the extra thing.
plan.changes.append(Delete(extra_a))
@ -130,11 +115,9 @@ class TestOwnershipProcessor(TestCase):
the_a = records['the-a']
plan.existing.add_record(the_a)
name = f'{ownership.txt_name}.a.the-a'
the_a_ownership = Record.new(zone, name, {
'ttl': 30,
'type': 'TXT',
'value': ownership.txt_value,
})
the_a_ownership = Record.new(
zone, name, {'ttl': 30, 'type': 'TXT', 'value': ownership.txt_value}
)
plan.existing.add_record(the_a_ownership)
plan.changes.append(Delete(the_a))
plan.changes.append(Delete(the_a_ownership))

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestAzureShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.azuredns import AzureProvider
AzureProvider

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from logging import getLogger
from unittest import TestCase
@ -27,8 +31,12 @@ class HelperProvider(BaseProvider):
id = 'test'
strict_supports = False
def __init__(self, extra_changes=[], apply_disabled=False,
include_change_callback=None):
def __init__(
self,
extra_changes=[],
apply_disabled=False,
include_change_callback=None,
):
self.__extra_changes = extra_changes
self.apply_disabled = apply_disabled
self.include_change_callback = include_change_callback
@ -39,8 +47,9 @@ class HelperProvider(BaseProvider):
return True
def _include_change(self, change):
return not self.include_change_callback or \
self.include_change_callback(change)
return not self.include_change_callback or self.include_change_callback(
change
)
def _extra_changes(self, **kwargs):
return self.__extra_changes
@ -50,7 +59,6 @@ class HelperProvider(BaseProvider):
class TrickyProcessor(BaseProcessor):
def __init__(self, name, add_during_process_target_zone):
super(TrickyProcessor, self).__init__(name)
self.add_during_process_target_zone = add_during_process_target_zone
@ -73,20 +81,22 @@ class TrickyProcessor(BaseProcessor):
class TestBaseProvider(TestCase):
def test_base_provider(self):
with self.assertRaises(NotImplementedError) as ctx:
BaseProvider('base')
self.assertEqual('Abstract base class, log property missing',
str(ctx.exception))
self.assertEqual(
'Abstract base class, log property missing', str(ctx.exception)
)
class HasLog(BaseProvider):
log = getLogger('HasLog')
with self.assertRaises(NotImplementedError) as ctx:
HasLog('haslog')
self.assertEqual('Abstract base class, SUPPORTS_GEO property missing',
str(ctx.exception))
self.assertEqual(
'Abstract base class, SUPPORTS_GEO property missing',
str(ctx.exception),
)
class HasSupportsGeo(HasLog):
SUPPORTS_GEO = False
@ -94,15 +104,18 @@ class TestBaseProvider(TestCase):
zone = Zone('unit.tests.', ['sub'])
with self.assertRaises(NotImplementedError) as ctx:
HasSupportsGeo('hassupportsgeo').populate(zone)
self.assertEqual('Abstract base class, SUPPORTS property missing',
str(ctx.exception))
self.assertEqual(
'Abstract base class, SUPPORTS property missing', str(ctx.exception)
)
class HasSupports(HasSupportsGeo):
SUPPORTS = set(('A',))
with self.assertRaises(NotImplementedError) as ctx:
HasSupports('hassupports').populate(zone)
self.assertEqual('Abstract base class, populate method missing',
str(ctx.exception))
self.assertEqual(
'Abstract base class, populate method missing', str(ctx.exception)
)
# SUPPORTS_DYNAMIC has a default/fallback
self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC)
@ -111,44 +124,51 @@ class TestBaseProvider(TestCase):
class HasSupportsDyanmic(HasSupports):
SUPPORTS_DYNAMIC = True
self.assertTrue(HasSupportsDyanmic('hassupportsdynamic')
.SUPPORTS_DYNAMIC)
self.assertTrue(
HasSupportsDyanmic('hassupportsdynamic').SUPPORTS_DYNAMIC
)
class HasPopulate(HasSupports):
def populate(self, zone, target=False, lenient=False):
zone.add_record(Record.new(zone, '', {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}), lenient=lenient)
zone.add_record(Record.new(zone, 'going', {
'ttl': 60,
'type': 'A',
'value': '3.4.5.6'
}), lenient=lenient)
zone.add_record(Record.new(zone, 'foo.sub', {
'ttl': 61,
'type': 'A',
'value': '4.5.6.7'
}), lenient=lenient)
zone.add_record(
Record.new(
zone, '', {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
),
lenient=lenient,
)
zone.add_record(
Record.new(
zone,
'going',
{'ttl': 60, 'type': 'A', 'value': '3.4.5.6'},
),
lenient=lenient,
)
zone.add_record(
Record.new(
zone,
'foo.sub',
{'ttl': 61, 'type': 'A', 'value': '4.5.6.7'},
),
lenient=lenient,
)
zone.add_record(Record.new(zone, '', {
'ttl': 60,
'type': 'A',
'value': '1.2.3.4'
}))
zone.add_record(
Record.new(zone, '', {'ttl': 60, 'type': 'A', 'value': '1.2.3.4'})
)
self.assertTrue(HasSupports('hassupportsgeo')
.supports(list(zone.records)[0]))
self.assertTrue(
HasSupports('hassupportsgeo').supports(list(zone.records)[0])
)
plan = HasPopulate('haspopulate').plan(zone)
self.assertEqual(3, len(plan.changes))
with self.assertRaises(NotImplementedError) as ctx:
HasPopulate('haspopulate').apply(plan)
self.assertEqual('Abstract base class, _apply method missing',
str(ctx.exception))
self.assertEqual(
'Abstract base class, _apply method missing', str(ctx.exception)
)
def test_plan(self):
ignored = Zone('unit.tests.', [])
@ -157,11 +177,9 @@ class TestBaseProvider(TestCase):
provider = HelperProvider([])
self.assertEqual(None, provider.plan(ignored))
record = Record.new(ignored, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
ignored, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
provider = HelperProvider([Create(record)])
plan = provider.plan(ignored)
self.assertTrue(plan)
@ -171,7 +189,6 @@ class TestBaseProvider(TestCase):
ignored = Zone('unit.tests.', [])
class OldApiProvider(HelperProvider):
def _process_desired_zone(self, desired):
return desired
@ -180,7 +197,6 @@ class TestBaseProvider(TestCase):
self.assertEqual(None, provider.plan(ignored))
class OtherTypeErrorProvider(HelperProvider):
def _process_desired_zone(self, desired, exists=False):
raise TypeError('foo')
@ -193,18 +209,20 @@ class TestBaseProvider(TestCase):
zone = Zone('unit.tests.', [])
# supported
supported = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
supported = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
zone.add_record(supported)
# not supported
not_supported = Record.new(zone, 'aaaa', {
'ttl': 30,
'type': 'AAAA',
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
})
not_supported = Record.new(
zone,
'aaaa',
{
'ttl': 30,
'type': 'AAAA',
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
},
)
zone.add_record(not_supported)
provider = HelperProvider()
plan = provider.plan(zone)
@ -215,11 +233,9 @@ class TestBaseProvider(TestCase):
def test_plan_with_processors(self):
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
provider = HelperProvider()
# Processor that adds a record to the zone, which planning will then
# delete since it won't know anything about it
@ -233,11 +249,9 @@ class TestBaseProvider(TestCase):
self.assertEqual(zone.name, tricky.existing.name)
# Chain of processors happen one after the other
other = Record.new(zone, 'b', {
'ttl': 30,
'type': 'A',
'value': '5.6.7.8',
})
other = Record.new(
zone, 'b', {'ttl': 30, 'type': 'A', 'value': '5.6.7.8'}
)
# Another processor will add its record, thus 2 deletes
another = TrickyProcessor('tricky', [other])
plan = provider.plan(zone, processors=[tricky, another])
@ -253,11 +267,9 @@ class TestBaseProvider(TestCase):
def test_plan_with_root_ns(self):
zone = Zone('unit.tests.', [])
record = Record.new(zone, '', {
'ttl': 30,
'type': 'NS',
'value': '1.2.3.4.',
})
record = Record.new(
zone, '', {'ttl': 30, 'type': 'NS', 'value': '1.2.3.4.'}
)
zone.add_record(record)
# No root NS support, no change, thus no plan
@ -273,11 +285,9 @@ class TestBaseProvider(TestCase):
def test_apply(self):
ignored = Zone('unit.tests.', [])
record = Record.new(ignored, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
ignored, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
provider = HelperProvider([Create(record)], apply_disabled=True)
plan = provider.plan(ignored)
provider.apply(plan)
@ -288,11 +298,9 @@ class TestBaseProvider(TestCase):
def test_include_change(self):
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
zone.add_record(record)
provider = HelperProvider([], include_change_callback=lambda c: False)
plan = provider.plan(zone)
@ -300,7 +308,6 @@ class TestBaseProvider(TestCase):
self.assertFalse(plan)
def test_plan_order_of_operations(self):
class MockProvider(BaseProvider):
log = getLogger('mock-provider')
SUPPORTS = set(('A',))
@ -327,8 +334,10 @@ class TestBaseProvider(TestCase):
self.assertFalse(provider.plan(zone))
# ensure the calls were made in the expected order, populate comes
# first, then desired, then existing
self.assertEqual(['populate', '_process_desired_zone',
'_process_existing_zone'], provider.calls)
self.assertEqual(
['populate', '_process_desired_zone', '_process_existing_zone'],
provider.calls,
)
def test_process_desired_zone(self):
provider = HelperProvider('test')
@ -336,11 +345,11 @@ class TestBaseProvider(TestCase):
# SUPPORTS_MULTIVALUE_PTR
provider.SUPPORTS_MULTIVALUE_PTR = False
zone1 = Zone('unit.tests.', [])
record1 = Record.new(zone1, 'ptr', {
'type': 'PTR',
'ttl': 3600,
'values': ['foo.com.', 'bar.com.'],
})
record1 = Record.new(
zone1,
'ptr',
{'type': 'PTR', 'ttl': 3600, 'values': ['foo.com.', 'bar.com.']},
)
zone1.add_record(record1)
zone2 = provider._process_desired_zone(zone1.copy())
@ -355,23 +364,19 @@ class TestBaseProvider(TestCase):
# SUPPORTS_DYNAMIC
provider.SUPPORTS_DYNAMIC = False
zone1 = Zone('unit.tests.', [])
record1 = Record.new(zone1, 'a', {
'dynamic': {
'pools': {
'one': {
'values': [{
'value': '1.1.1.1',
}],
},
record1 = Record.new(
zone1,
'a',
{
'dynamic': {
'pools': {'one': {'values': [{'value': '1.1.1.1'}]}},
'rules': [{'pool': 'one'}],
},
'rules': [{
'pool': 'one',
}],
'type': 'A',
'ttl': 3600,
'values': ['2.2.2.2'],
},
'type': 'A',
'ttl': 3600,
'values': ['2.2.2.2'],
})
)
self.assertTrue(record1.dynamic)
zone1.add_record(record1)
@ -393,18 +398,17 @@ class TestBaseProvider(TestCase):
zone2 = provider._process_desired_zone(zone1.copy())
record2 = list(zone2.records)[0]
self.assertEqual(
record2.dynamic.pools['one'].data['values'][0]['status'],
'obey'
record2.dynamic.pools['one'].data['values'][0]['status'], 'obey'
)
# SUPPORTS_ROOT_NS
provider.SUPPORTS_ROOT_NS = False
zone1 = Zone('unit.tests.', [])
record1 = Record.new(zone1, '', {
'type': 'NS',
'ttl': 3600,
'values': ['foo.com.', 'bar.com.'],
})
record1 = Record.new(
zone1,
'',
{'type': 'NS', 'ttl': 3600, 'values': ['foo.com.', 'bar.com.']},
)
zone1.add_record(record1)
zone2 = provider._process_desired_zone(zone1.copy())
@ -421,11 +425,11 @@ class TestBaseProvider(TestCase):
# SUPPORTS_ROOT_NS
provider.SUPPORTS_ROOT_NS = False
zone1 = Zone('unit.tests.', [])
record1 = Record.new(zone1, '', {
'type': 'NS',
'ttl': 3600,
'values': ['foo.com.', 'bar.com.'],
})
record1 = Record.new(
zone1,
'',
{'type': 'NS', 'ttl': 3600, 'values': ['foo.com.', 'bar.com.']},
)
zone1.add_record(record1)
zone2 = provider._process_existing_zone(zone1.copy(), zone1)
@ -444,42 +448,38 @@ class TestBaseProvider(TestCase):
# Creates are safe when existing records is under MIN_EXISTING_RECORDS
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
Plan(zone, zone, [Create(record) for i in range(10)], True) \
.raise_if_unsafe()
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
Plan(
zone, zone, [Create(record) for i in range(10)], True
).raise_if_unsafe()
def test_safe_min_existing_creates(self):
# Creates are safe when existing records is over MIN_EXISTING_RECORDS
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
Plan(zone, zone, [Create(record) for i in range(10)], True) \
.raise_if_unsafe()
Plan(
zone, zone, [Create(record) for i in range(10)], True
).raise_if_unsafe()
def test_safe_no_existing(self):
# existing records fewer than MIN_EXISTING_RECORDS is safe
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
updates = [Update(record, record), Update(record, record)]
Plan(zone, zone, updates, True).raise_if_unsafe()
@ -488,22 +488,23 @@ class TestBaseProvider(TestCase):
# MAX_SAFE_UPDATE_PCENT+1 fails when more
# than MIN_EXISTING_RECORDS exist
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
changes = [Update(record, record)
for i in range(int(Plan.MIN_EXISTING_RECORDS *
Plan.MAX_SAFE_UPDATE_PCENT) + 1)]
changes = [
Update(record, record)
for i in range(
int(Plan.MIN_EXISTING_RECORDS * Plan.MAX_SAFE_UPDATE_PCENT) + 1
)
]
with self.assertRaises(UnsafePlan) as ctx:
Plan(zone, zone, changes, True).raise_if_unsafe()
@ -514,21 +515,22 @@ class TestBaseProvider(TestCase):
# MAX_SAFE_UPDATE_PCENT is safe when more
# than MIN_EXISTING_RECORDS exist
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
changes = [Update(record, record)
for i in range(int(Plan.MIN_EXISTING_RECORDS *
Plan.MAX_SAFE_UPDATE_PCENT))]
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
changes = [
Update(record, record)
for i in range(
int(Plan.MIN_EXISTING_RECORDS * Plan.MAX_SAFE_UPDATE_PCENT)
)
]
Plan(zone, zone, changes, True).raise_if_unsafe()
@ -536,22 +538,23 @@ class TestBaseProvider(TestCase):
# MAX_SAFE_DELETE_PCENT+1 fails when more
# than MIN_EXISTING_RECORDS exist
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
changes = [Delete(record)
for i in range(int(Plan.MIN_EXISTING_RECORDS *
Plan.MAX_SAFE_DELETE_PCENT) + 1)]
changes = [
Delete(record)
for i in range(
int(Plan.MIN_EXISTING_RECORDS * Plan.MAX_SAFE_DELETE_PCENT) + 1
)
]
with self.assertRaises(UnsafePlan) as ctx:
Plan(zone, zone, changes, True).raise_if_unsafe()
@ -562,77 +565,78 @@ class TestBaseProvider(TestCase):
# MAX_SAFE_DELETE_PCENT is safe when more
# than MIN_EXISTING_RECORDS exist
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
changes = [Delete(record)
for i in range(int(Plan.MIN_EXISTING_RECORDS *
Plan.MAX_SAFE_DELETE_PCENT))]
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
changes = [
Delete(record)
for i in range(
int(Plan.MIN_EXISTING_RECORDS * Plan.MAX_SAFE_DELETE_PCENT)
)
]
Plan(zone, zone, changes, True).raise_if_unsafe()
def test_safe_updates_min_existing_override(self):
safe_pcent = .4
safe_pcent = 0.4
# 40% + 1 fails when more
# than MIN_EXISTING_RECORDS exist
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
changes = [Update(record, record)
for i in range(int(Plan.MIN_EXISTING_RECORDS *
safe_pcent) + 1)]
changes = [
Update(record, record)
for i in range(int(Plan.MIN_EXISTING_RECORDS * safe_pcent) + 1)
]
with self.assertRaises(UnsafePlan) as ctx:
Plan(zone, zone, changes, True,
update_pcent_threshold=safe_pcent).raise_if_unsafe()
Plan(
zone, zone, changes, True, update_pcent_threshold=safe_pcent
).raise_if_unsafe()
self.assertTrue('Too many updates' in str(ctx.exception))
def test_safe_deletes_min_existing_override(self):
safe_pcent = .4
safe_pcent = 0.4
# 40% + 1 fails when more
# than MIN_EXISTING_RECORDS exist
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
record = Record.new(
zone, 'a', {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'}
)
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
zone.add_record(Record.new(zone, str(i), {
'ttl': 60,
'type': 'A',
'value': '2.3.4.5'
}))
zone.add_record(
Record.new(
zone, str(i), {'ttl': 60, 'type': 'A', 'value': '2.3.4.5'}
)
)
changes = [Delete(record)
for i in range(int(Plan.MIN_EXISTING_RECORDS *
safe_pcent) + 1)]
changes = [
Delete(record)
for i in range(int(Plan.MIN_EXISTING_RECORDS * safe_pcent) + 1)
]
with self.assertRaises(UnsafePlan) as ctx:
Plan(zone, zone, changes, True,
delete_pcent_threshold=safe_pcent).raise_if_unsafe()
Plan(
zone, zone, changes, True, delete_pcent_threshold=safe_pcent
).raise_if_unsafe()
self.assertTrue('Too many deletes' in str(ctx.exception))
@ -649,9 +653,9 @@ class TestBaseProvider(TestCase):
# Should log and not expect
normal.supports_warn_or_except('Hello World!', 'Goodbye')
normal.log.warning.assert_called_once()
normal.log.warning.assert_has_calls([
call('%s; %s', 'Hello World!', 'Goodbye')
])
normal.log.warning.assert_has_calls(
[call('%s; %s', 'Hello World!', 'Goodbye')]
)
strict = MinimalProvider(strict_supports=True)
# Should log and not expect
@ -662,7 +666,6 @@ class TestBaseProvider(TestCase):
class TestBaseProviderSupportsRootNs(TestCase):
class Provider(BaseProvider):
log = getLogger('Provider')
@ -684,33 +687,33 @@ class TestBaseProviderSupportsRootNs(TestCase):
return False
zone = Zone('unit.tests.', [])
a_record = Record.new(zone, 'ptr', {
'type': 'A',
'ttl': 3600,
'values': ['1.2.3.4', '2.3.4.5'],
})
ns_record = Record.new(zone, 'sub', {
'type': 'NS',
'ttl': 3600,
'values': ['ns2.foo.com.', 'ns2.bar.com.'],
})
a_record = Record.new(
zone,
'ptr',
{'type': 'A', 'ttl': 3600, 'values': ['1.2.3.4', '2.3.4.5']},
)
ns_record = Record.new(
zone,
'sub',
{'type': 'NS', 'ttl': 3600, 'values': ['ns2.foo.com.', 'ns2.bar.com.']},
)
no_root = zone.copy()
no_root.add_record(a_record)
no_root.add_record(ns_record)
root_ns_record = Record.new(zone, '', {
'type': 'NS',
'ttl': 3600,
'values': ['ns1.foo.com.', 'ns1.bar.com.'],
})
root_ns_record = Record.new(
zone,
'',
{'type': 'NS', 'ttl': 3600, 'values': ['ns1.foo.com.', 'ns1.bar.com.']},
)
has_root = no_root.copy()
has_root.add_record(root_ns_record)
other_root_ns_record = Record.new(zone, '', {
'type': 'NS',
'ttl': 3600,
'values': ['ns4.foo.com.', 'ns4.bar.com.'],
})
other_root_ns_record = Record.new(
zone,
'',
{'type': 'NS', 'ttl': 3600, 'values': ['ns4.foo.com.', 'ns4.bar.com.']},
)
different_root = no_root.copy()
different_root.add_record(other_root_ns_record)
@ -733,8 +736,10 @@ class TestBaseProviderSupportsRootNs(TestCase):
provider.strict_supports = True
with self.assertRaises(SupportsException) as ctx:
provider.plan(self.has_root)
self.assertEqual('test: root NS record not supported for unit.tests.',
str(ctx.exception))
self.assertEqual(
'test: root NS record not supported for unit.tests.',
str(ctx.exception),
)
def test_supports_root_ns_false_different(self):
# provider has a non-matching existing record
@ -754,8 +759,10 @@ class TestBaseProviderSupportsRootNs(TestCase):
provider.strict_supports = True
with self.assertRaises(SupportsException) as ctx:
provider.plan(self.has_root)
self.assertEqual('test: root NS record not supported for unit.tests.',
str(ctx.exception))
self.assertEqual(
'test: root NS record not supported for unit.tests.',
str(ctx.exception),
)
def test_supports_root_ns_false_missing(self):
# provider has an existing record
@ -792,8 +799,10 @@ class TestBaseProviderSupportsRootNs(TestCase):
provider.strict_supports = True
with self.assertRaises(SupportsException) as ctx:
provider.plan(self.has_root)
self.assertEqual('test: root NS record not supported for unit.tests.',
str(ctx.exception))
self.assertEqual(
'test: root NS record not supported for unit.tests.',
str(ctx.exception),
)
def test_supports_root_ns_false_create_zone_missing(self):
# provider has no existing records (create)
@ -889,8 +898,9 @@ class TestBaseProviderSupportsRootNs(TestCase):
# we'll get a plan that creates everything, including it
self.assertTrue(plan)
self.assertEqual(3, len(plan.changes))
change = [c for c in plan.changes
if c.new.name == '' and c.new._type == 'NS'][0]
change = [
c for c in plan.changes if c.new.name == '' and c.new._type == 'NS'
][0]
self.assertFalse(change.existing)
self.assertEqual(self.root_ns_record, change.new)
@ -900,8 +910,9 @@ class TestBaseProviderSupportsRootNs(TestCase):
plan = provider.plan(self.has_root)
self.assertTrue(plan)
self.assertEqual(3, len(plan.changes))
change = [c for c in plan.changes
if c.new.name == '' and c.new._type == 'NS'][0]
change = [
c for c in plan.changes if c.new.name == '' and c.new._type == 'NS'
][0]
self.assertFalse(change.existing)
self.assertEqual(self.root_ns_record, change.new)

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestCloudflareShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.cloudflare import CloudflareProvider
CloudflareProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestConstellixShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.constellix import ConstellixProvider
ConstellixProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestDigitalOceanShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.digitalocean import DigitalOceanProvider
DigitalOceanProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestDnsimpleShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.dnsimple import DnsimpleProvider
DnsimpleProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestDnsMadeEasyShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.dnsmadeeasy import DnsMadeEasyProvider
DnsMadeEasyProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestDynShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.dyn import DynProvider
DynProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestEasyDnsShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.easydns import EasyDnsProvider
EasyDnsProvider

View File

@ -2,20 +2,25 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
# Just for coverage
import octodns.provider.fastdns
# Quell warnings
octodns.provider.fastdns
class TestAkamaiShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.edgedns import AkamaiProvider
AkamaiProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestEtcHostsShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.etc_hosts import EtcHostsProvider
EtcHostsProvider

View File

@ -2,20 +2,25 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
# Just for coverage
import octodns.provider.fastdns
# Quell warnings
octodns.provider.fastdns
class TestGandiShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.gandi import GandiProvider
GandiProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestGCoreShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.gcore import GCoreProvider
GCoreProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestGoogleCloudShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.googlecloud import GoogleCloudProvider
GoogleCloudProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestHetznerShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.hetzner import HetznerProvider
HetznerProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestMythicBeastsShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.mythicbeasts import MythicBeastsProvider
MythicBeastsProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestNs1Provider(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.ns1 import Ns1Provider
Ns1Provider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestOvhShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.ovh import OvhProvider
OvhProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestPowerDnsShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.powerdns import PowerDnsProvider
PowerDnsProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestRackspaceShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.rackspace import RackspaceProvider
RackspaceProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestRoute53Provider(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.route53 import Route53Provider
Route53Provider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestSelectelShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.selectel import SelectelProvider
SelectelProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestTransipShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.transip import TransipProvider
TransipProvider

View File

@ -2,15 +2,19 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
class TestUltraShim(TestCase):
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.ultra import UltraProvider
UltraProvider

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from os import makedirs
from os.path import basename, dirname, isdir, isfile, join
@ -13,15 +17,17 @@ from yaml.constructor import ConstructorError
from octodns.record import Create
from octodns.provider.base import Plan
from octodns.provider.yaml import _list_all_yaml_files, \
SplitYamlProvider, YamlProvider
from octodns.provider.yaml import (
_list_all_yaml_files,
SplitYamlProvider,
YamlProvider,
)
from octodns.zone import SubzoneRecordException, Zone
from helpers import TemporaryDirectory
class TestYamlProvider(TestCase):
def test_provider(self):
source = YamlProvider('test', join(dirname(__file__), 'config'))
@ -57,8 +63,9 @@ class TestYamlProvider(TestCase):
# We add everything
plan = target.plan(zone)
self.assertEqual(22, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEqual(
22, len([c for c in plan.changes if isinstance(c, Create)])
)
self.assertFalse(isfile(yaml_file))
# Now actually do it
@ -67,8 +74,9 @@ class TestYamlProvider(TestCase):
# Dynamic plan
plan = target.plan(dynamic_zone)
self.assertEqual(6, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEqual(
6, len([c for c in plan.changes if isinstance(c, Create)])
)
self.assertFalse(isfile(dynamic_yaml_file))
# Apply it
self.assertEqual(6, target.apply(plan))
@ -79,8 +87,10 @@ class TestYamlProvider(TestCase):
target.populate(reloaded)
self.assertDictEqual(
{'included': ['test']},
[x for x in reloaded.records
if x.name == 'included'][0]._octodns)
[x for x in reloaded.records if x.name == 'included'][
0
]._octodns,
)
# manually copy over the root since it will have been ignored
# when things were written out
@ -90,8 +100,9 @@ class TestYamlProvider(TestCase):
# A 2nd sync should still create everything
plan = target.plan(zone)
self.assertEqual(22, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEqual(
22, len([c for c in plan.changes if isinstance(c, Create)])
)
with open(yaml_file) as fh:
data = safe_load(fh.read())
@ -100,7 +111,7 @@ class TestYamlProvider(TestCase):
roots = sorted(data.pop(''), key=lambda r: r['type'])
self.assertTrue('values' in roots[0]) # A
self.assertTrue('geo' in roots[0]) # geo made the trip
self.assertTrue('value' in roots[1]) # CAA
self.assertTrue('value' in roots[1]) # CAA
self.assertTrue('values' in roots[2]) # SSHFP
# these are stored as plural 'values'
@ -162,8 +173,9 @@ class TestYamlProvider(TestCase):
self.assertEqual([], list(data.keys()))
def test_empty(self):
source = YamlProvider('test', join(dirname(__file__), 'config'),
supports_root_ns=False)
source = YamlProvider(
'test', join(dirname(__file__), 'config'), supports_root_ns=False
)
zone = Zone('empty.', [])
@ -172,34 +184,41 @@ class TestYamlProvider(TestCase):
self.assertEqual(0, len(zone.records))
def test_unsorted(self):
source = YamlProvider('test', join(dirname(__file__), 'config'),
supports_root_ns=False)
source = YamlProvider(
'test', join(dirname(__file__), 'config'), supports_root_ns=False
)
zone = Zone('unordered.', [])
with self.assertRaises(ConstructorError):
source.populate(zone)
source = YamlProvider('test', join(dirname(__file__), 'config'),
enforce_order=False, supports_root_ns=False)
source = YamlProvider(
'test',
join(dirname(__file__), 'config'),
enforce_order=False,
supports_root_ns=False,
)
# no exception
source.populate(zone)
self.assertEqual(2, len(zone.records))
def test_subzone_handling(self):
source = YamlProvider('test', join(dirname(__file__), 'config'),
supports_root_ns=False)
source = YamlProvider(
'test', join(dirname(__file__), 'config'), supports_root_ns=False
)
# If we add `sub` as a sub-zone we'll reject `www.sub`
zone = Zone('unit.tests.', ['sub'])
with self.assertRaises(SubzoneRecordException) as ctx:
source.populate(zone)
self.assertEqual('Record www.sub.unit.tests. is under a managed '
'subzone', str(ctx.exception))
self.assertEqual(
'Record www.sub.unit.tests. is under a managed ' 'subzone',
str(ctx.exception),
)
class TestSplitYamlProvider(TestCase):
def test_list_all_yaml_files(self):
yaml_files = ('foo.yaml', '1.yaml', '$unit.tests.yaml')
all_files = ('something', 'else', '1', '$$', '-f') + yaml_files
@ -223,19 +242,21 @@ class TestSplitYamlProvider(TestCase):
def test_zone_directory(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
'test', join(dirname(__file__), 'config/split'), extension='.tst'
)
zone = Zone('unit.tests.', [])
self.assertEqual(
join(dirname(__file__), 'config/split', 'unit.tests.tst'),
source._zone_directory(zone))
source._zone_directory(zone),
)
def test_apply_handles_existing_zone_directory(self):
with TemporaryDirectory() as td:
provider = SplitYamlProvider('test', join(td.dirname, 'config'),
extension='.tst')
provider = SplitYamlProvider(
'test', join(td.dirname, 'config'), extension='.tst'
)
makedirs(join(td.dirname, 'config', 'does.exist.tst'))
zone = Zone('does.exist.', [])
@ -245,8 +266,8 @@ class TestSplitYamlProvider(TestCase):
def test_provider(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
'test', join(dirname(__file__), 'config/split'), extension='.tst'
)
zone = Zone('unit.tests.', [])
dynamic_zone = Zone('dynamic.tests.', [])
@ -267,14 +288,15 @@ class TestSplitYamlProvider(TestCase):
directory = join(td.dirname, 'sub', 'dir')
zone_dir = join(directory, 'unit.tests.tst')
dynamic_zone_dir = join(directory, 'dynamic.tests.tst')
target = SplitYamlProvider('test', directory,
extension='.tst',
supports_root_ns=False)
target = SplitYamlProvider(
'test', directory, extension='.tst', supports_root_ns=False
)
# We add everything
plan = target.plan(zone)
self.assertEqual(17, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEqual(
17, len([c for c in plan.changes if isinstance(c, Create)])
)
self.assertFalse(isdir(zone_dir))
# Now actually do it
@ -282,8 +304,9 @@ class TestSplitYamlProvider(TestCase):
# Dynamic plan
plan = target.plan(dynamic_zone)
self.assertEqual(5, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEqual(
5, len([c for c in plan.changes if isinstance(c, Create)])
)
self.assertFalse(isdir(dynamic_zone_dir))
# Apply it
self.assertEqual(5, target.apply(plan))
@ -294,8 +317,10 @@ class TestSplitYamlProvider(TestCase):
target.populate(reloaded)
self.assertDictEqual(
{'included': ['test']},
[x for x in reloaded.records
if x.name == 'included'][0]._octodns)
[x for x in reloaded.records if x.name == 'included'][
0
]._octodns,
)
# manually copy over the root since it will have been ignored
# when things were written out
@ -305,8 +330,9 @@ class TestSplitYamlProvider(TestCase):
# A 2nd sync should still create everything
plan = target.plan(zone)
self.assertEqual(17, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEqual(
17, len([c for c in plan.changes if isinstance(c, Create)])
)
yaml_file = join(zone_dir, '$unit.tests.yaml')
self.assertTrue(isfile(yaml_file))
@ -315,13 +341,19 @@ class TestSplitYamlProvider(TestCase):
roots = sorted(data.pop(''), key=lambda r: r['type'])
self.assertTrue('values' in roots[0]) # A
self.assertTrue('geo' in roots[0]) # geo made the trip
self.assertTrue('value' in roots[1]) # CAA
self.assertTrue('value' in roots[1]) # CAA
self.assertTrue('values' in roots[2]) # SSHFP
# These records are stored as plural "values." Check each file to
# ensure correctness.
for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt',
'urlfwd'):
for record_name in (
'_srv._tcp',
'mx',
'naptr',
'sub',
'txt',
'urlfwd',
):
yaml_file = join(zone_dir, f'{record_name}.yaml')
self.assertTrue(isfile(yaml_file))
with open(yaml_file) as fh:
@ -329,8 +361,16 @@ class TestSplitYamlProvider(TestCase):
self.assertTrue('values' in data.pop(record_name))
# These are stored as singular "value." Again, check each file.
for record_name in ('aaaa', 'cname', 'dname', 'included', 'ptr',
'spf', 'www.sub', 'www'):
for record_name in (
'aaaa',
'cname',
'dname',
'included',
'ptr',
'spf',
'www.sub',
'www',
):
yaml_file = join(zone_dir, f'{record_name}.yaml')
self.assertTrue(isfile(yaml_file))
with open(yaml_file) as fh:
@ -339,8 +379,7 @@ class TestSplitYamlProvider(TestCase):
# Again with the plural, this time checking dynamic.tests.
for record_name in ('a', 'aaaa', 'real-ish-a'):
yaml_file = join(
dynamic_zone_dir, f'{record_name}.yaml')
yaml_file = join(dynamic_zone_dir, f'{record_name}.yaml')
self.assertTrue(isfile(yaml_file))
with open(yaml_file) as fh:
data = safe_load(fh.read())
@ -360,8 +399,8 @@ class TestSplitYamlProvider(TestCase):
def test_empty(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
'test', join(dirname(__file__), 'config/split'), extension='.tst'
)
zone = Zone('empty.', [])
@ -371,8 +410,8 @@ class TestSplitYamlProvider(TestCase):
def test_unsorted(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
'test', join(dirname(__file__), 'config/split'), extension='.tst'
)
zone = Zone('unordered.', [])
@ -382,35 +421,46 @@ class TestSplitYamlProvider(TestCase):
zone = Zone('unordered.', [])
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
extension='.tst', enforce_order=False)
'test',
join(dirname(__file__), 'config/split'),
extension='.tst',
enforce_order=False,
)
# no exception
source.populate(zone)
self.assertEqual(2, len(zone.records))
def test_subzone_handling(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
'test', join(dirname(__file__), 'config/split'), extension='.tst'
)
# If we add `sub` as a sub-zone we'll reject `www.sub`
zone = Zone('unit.tests.', ['sub'])
with self.assertRaises(SubzoneRecordException) as ctx:
source.populate(zone)
self.assertEqual('Record www.sub.unit.tests. is under a managed '
'subzone', str(ctx.exception))
self.assertEqual(
'Record www.sub.unit.tests. is under a managed ' 'subzone',
str(ctx.exception),
)
class TestOverridingYamlProvider(TestCase):
def test_provider(self):
config = join(dirname(__file__), 'config')
override_config = join(dirname(__file__), 'config', 'override')
base = YamlProvider('base', config, populate_should_replace=False,
supports_root_ns=False)
override = YamlProvider('test', override_config,
populate_should_replace=True,
supports_root_ns=False)
base = YamlProvider(
'base',
config,
populate_should_replace=False,
supports_root_ns=False,
)
override = YamlProvider(
'test',
override_config,
populate_should_replace=True,
supports_root_ns=False,
)
zone = Zone('dynamic.tests.', [])
@ -428,9 +478,8 @@ class TestOverridingYamlProvider(TestCase):
got = {r.name: r for r in zone.records}
self.assertEqual(7, len(got))
# 'a' was replaced with a generic record
self.assertEqual({
'ttl': 3600,
'values': ['4.4.4.4', '5.5.5.5']
}, got['a'].data)
self.assertEqual(
{'ttl': 3600, 'values': ['4.4.4.4', '5.5.5.5']}, got['a'].data
)
# And we have the new one
self.assertTrue('added' in got)

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
@ -11,7 +15,6 @@ from octodns.record.geo import GeoCodes
class TestRecordGeoCodes(TestCase):
def test_validate(self):
prefix = 'xyz '
@ -21,53 +24,81 @@ class TestRecordGeoCodes(TestCase):
self.assertEqual([], GeoCodes.validate('NA-US-OR', prefix))
# Just plain bad
self.assertEqual(['xyz invalid geo code "XX-YY-ZZ-AA"'],
GeoCodes.validate('XX-YY-ZZ-AA', prefix))
self.assertEqual(['xyz unknown continent code "X-Y-Z"'],
GeoCodes.validate('X-Y-Z', prefix))
self.assertEqual(['xyz unknown continent code "XXX-Y-Z"'],
GeoCodes.validate('XXX-Y-Z', prefix))
self.assertEqual(
['xyz invalid geo code "XX-YY-ZZ-AA"'],
GeoCodes.validate('XX-YY-ZZ-AA', prefix),
)
self.assertEqual(
['xyz unknown continent code "X-Y-Z"'],
GeoCodes.validate('X-Y-Z', prefix),
)
self.assertEqual(
['xyz unknown continent code "XXX-Y-Z"'],
GeoCodes.validate('XXX-Y-Z', prefix),
)
# Bad continent
self.assertEqual(['xyz unknown continent code "XX"'],
GeoCodes.validate('XX', prefix))
self.assertEqual(
['xyz unknown continent code "XX"'], GeoCodes.validate('XX', prefix)
)
# Bad continent good country
self.assertEqual(['xyz unknown continent code "XX-US"'],
GeoCodes.validate('XX-US', prefix))
self.assertEqual(
['xyz unknown continent code "XX-US"'],
GeoCodes.validate('XX-US', prefix),
)
# Bad continent good country and province
self.assertEqual(['xyz unknown continent code "XX-US-OR"'],
GeoCodes.validate('XX-US-OR', prefix))
self.assertEqual(
['xyz unknown continent code "XX-US-OR"'],
GeoCodes.validate('XX-US-OR', prefix),
)
# Bad country, good continent
self.assertEqual(['xyz unknown country code "NA-XX"'],
GeoCodes.validate('NA-XX', prefix))
self.assertEqual(
['xyz unknown country code "NA-XX"'],
GeoCodes.validate('NA-XX', prefix),
)
# Bad country, good continent and state
self.assertEqual(['xyz unknown country code "NA-XX-OR"'],
GeoCodes.validate('NA-XX-OR', prefix))
self.assertEqual(
['xyz unknown country code "NA-XX-OR"'],
GeoCodes.validate('NA-XX-OR', prefix),
)
# Good country, good continent, but bad match
self.assertEqual(['xyz unknown country code "NA-GB"'],
GeoCodes.validate('NA-GB', prefix))
self.assertEqual(
['xyz unknown country code "NA-GB"'],
GeoCodes.validate('NA-GB', prefix),
)
# Bad province code, good continent and country
self.assertEqual(['xyz unknown province code "NA-US-XX"'],
GeoCodes.validate('NA-US-XX', prefix))
self.assertEqual(
['xyz unknown province code "NA-US-XX"'],
GeoCodes.validate('NA-US-XX', prefix),
)
def test_parse(self):
self.assertEqual({
'continent_code': 'NA',
'country_code': None,
'province_code': None,
}, GeoCodes.parse('NA'))
self.assertEqual({
'continent_code': 'NA',
'country_code': 'US',
'province_code': None,
}, GeoCodes.parse('NA-US'))
self.assertEqual({
'continent_code': 'NA',
'country_code': 'US',
'province_code': 'CA',
}, GeoCodes.parse('NA-US-CA'))
self.assertEqual(
{
'continent_code': 'NA',
'country_code': None,
'province_code': None,
},
GeoCodes.parse('NA'),
)
self.assertEqual(
{
'continent_code': 'NA',
'country_code': 'US',
'province_code': None,
},
GeoCodes.parse('NA-US'),
)
self.assertEqual(
{
'continent_code': 'NA',
'country_code': 'US',
'province_code': 'CA',
},
GeoCodes.parse('NA-US-CA'),
)
def test_country_to_code(self):
self.assertEqual('NA-US', GeoCodes.country_to_code('US'))

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
import dns.zone
from dns.exception import DNSException
@ -13,8 +17,12 @@ from shutil import copyfile
from unittest import TestCase
from unittest.mock import patch
from octodns.source.axfr import AxfrSource, AxfrSourceZoneTransferFailed, \
ZoneFileSource, ZoneFileSourceLoadFailure
from octodns.source.axfr import (
AxfrSource,
AxfrSourceZoneTransferFailed,
ZoneFileSource,
ZoneFileSourceLoadFailure,
)
from octodns.zone import Zone
from octodns.record import ValidationError
@ -22,17 +30,15 @@ from octodns.record import ValidationError
class TestAxfrSource(TestCase):
source = AxfrSource('test', 'localhost')
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.tst',
'unit.tests', relativize=False)
forward_zonefile = dns.zone.from_file(
'./tests/zones/unit.tests.tst', 'unit.tests', relativize=False
)
@patch('dns.zone.from_xfr')
def test_populate(self, from_xfr_mock):
got = Zone('unit.tests.', [])
from_xfr_mock.side_effect = [
self.forward_zonefile,
DNSException
]
from_xfr_mock.side_effect = [self.forward_zonefile, DNSException]
self.source.populate(got)
self.assertEqual(16, len(got.records))
@ -40,8 +46,7 @@ class TestAxfrSource(TestCase):
with self.assertRaises(AxfrSourceZoneTransferFailed) as ctx:
zone = Zone('unit.tests.', [])
self.source.populate(zone)
self.assertEqual('Unable to Perform Zone Transfer',
str(ctx.exception))
self.assertEqual('Unable to Perform Zone Transfer', str(ctx.exception))
class TestZoneFileSource(TestCase):
@ -65,8 +70,10 @@ class TestZoneFileSource(TestCase):
# It did so we need to skip this test, that means windows won't
# have full code coverage, but skipping the test is going out of
# our way enough for a os-specific/oddball case.
self.skipTest('Unable to create unit.tests. (ending with .) so '
'skipping default filename testing.')
self.skipTest(
'Unable to create unit.tests. (ending with .) so '
'skipping default filename testing.'
)
source = ZoneFileSource('test', './tests/zones')
# Load zonefiles without a specified file extension
@ -97,16 +104,19 @@ class TestZoneFileSource(TestCase):
with self.assertRaises(ZoneFileSourceLoadFailure) as ctx:
zone = Zone('invalid.zone.', [])
self.source.populate(zone)
self.assertEqual('The DNS zone has no NS RRset at its origin.',
str(ctx.exception))
self.assertEqual(
'The DNS zone has no NS RRset at its origin.', str(ctx.exception)
)
# Records are not to RFC (lenient=False)
with self.assertRaises(ValidationError) as ctx:
zone = Zone('invalid.records.', [])
self.source.populate(zone)
self.assertEqual('Invalid record _invalid.invalid.records.\n'
' - invalid name for SRV record',
str(ctx.exception))
self.assertEqual(
'Invalid record _invalid.invalid.records.\n'
' - invalid name for SRV record',
str(ctx.exception),
)
# Records are not to RFC, but load anyhow (lenient=True)
invalid = Zone('invalid.records.', [])

View File

@ -7,7 +7,6 @@ from octodns.zone import Zone
class TestEnvVarSource(TestCase):
def test_read_variable(self):
envvar = 'OCTODNS_TEST_ENVIRONMENT_VARIABLE'
source = EnvVarSource('testid', envvar, 'recordname', ttl=120)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from unittest import TestCase
@ -24,103 +28,96 @@ class TestTinyDnsFileSource(TestCase):
expected = Zone('example.com.', [])
for name, data in (
('', {
'type': 'A',
'ttl': 30,
'values': ['10.2.3.4', '10.2.3.5'],
}),
('', {
'type': 'NS',
'ttl': 3600,
'values': ['ns1.ns.com.', 'ns2.ns.com.'],
}),
('sub', {
'type': 'NS',
'ttl': 30,
'values': ['ns3.ns.com.', 'ns4.ns.com.'],
}),
('www', {
'type': 'A',
'ttl': 3600,
'value': '10.2.3.6',
}),
('cname', {
'type': 'CNAME',
'ttl': 3600,
'value': 'www.example.com.',
}),
('some-host-abc123', {
'type': 'A',
'ttl': 1800,
'value': '10.2.3.7',
}),
('has-dup-def123', {
'type': 'A',
'ttl': 3600,
'value': '10.2.3.8',
}),
('www.sub', {
'type': 'A',
'ttl': 3600,
'value': '1.2.3.4',
}),
('has-dup-def456', {
'type': 'A',
'ttl': 3600,
'value': '10.2.3.8',
}),
('', {
'type': 'MX',
'ttl': 3600,
'values': [{
'preference': 10,
'exchange': 'smtp-1-host.example.com.',
}, {
'preference': 20,
'exchange': 'smtp-2-host.example.com.',
}]
}),
('smtp', {
'type': 'MX',
'ttl': 1800,
'values': [{
'preference': 30,
'exchange': 'smtp-1-host.example.com.',
}, {
'preference': 40,
'exchange': 'smtp-2-host.example.com.',
}]
}),
('', {
'type': 'TXT',
'ttl': 300,
'value': 'test TXT',
}),
('colon', {
'type': 'TXT',
'ttl': 300,
'value': 'test : TXT',
}),
('nottl', {
'type': 'TXT',
'ttl': 3600,
'value': 'nottl test TXT',
}),
('ipv6-3', {
'type': 'AAAA',
'ttl': 300,
'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5742',
}),
('ipv6-6', {
'type': 'AAAA',
'ttl': 3600,
'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5743',
}),
('semicolon', {
'type': 'TXT',
'ttl': 300,
'value': 'v=DKIM1\\; k=rsa\\; p=blah',
}),
('', {'type': 'A', 'ttl': 30, 'values': ['10.2.3.4', '10.2.3.5']}),
(
'',
{
'type': 'NS',
'ttl': 3600,
'values': ['ns1.ns.com.', 'ns2.ns.com.'],
},
),
(
'sub',
{
'type': 'NS',
'ttl': 30,
'values': ['ns3.ns.com.', 'ns4.ns.com.'],
},
),
('www', {'type': 'A', 'ttl': 3600, 'value': '10.2.3.6'}),
(
'cname',
{'type': 'CNAME', 'ttl': 3600, 'value': 'www.example.com.'},
),
(
'some-host-abc123',
{'type': 'A', 'ttl': 1800, 'value': '10.2.3.7'},
),
('has-dup-def123', {'type': 'A', 'ttl': 3600, 'value': '10.2.3.8'}),
('www.sub', {'type': 'A', 'ttl': 3600, 'value': '1.2.3.4'}),
('has-dup-def456', {'type': 'A', 'ttl': 3600, 'value': '10.2.3.8'}),
(
'',
{
'type': 'MX',
'ttl': 3600,
'values': [
{
'preference': 10,
'exchange': 'smtp-1-host.example.com.',
},
{
'preference': 20,
'exchange': 'smtp-2-host.example.com.',
},
],
},
),
(
'smtp',
{
'type': 'MX',
'ttl': 1800,
'values': [
{
'preference': 30,
'exchange': 'smtp-1-host.example.com.',
},
{
'preference': 40,
'exchange': 'smtp-2-host.example.com.',
},
],
},
),
('', {'type': 'TXT', 'ttl': 300, 'value': 'test TXT'}),
('colon', {'type': 'TXT', 'ttl': 300, 'value': 'test : TXT'}),
('nottl', {'type': 'TXT', 'ttl': 3600, 'value': 'nottl test TXT'}),
(
'ipv6-3',
{
'type': 'AAAA',
'ttl': 300,
'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5742',
},
),
(
'ipv6-6',
{
'type': 'AAAA',
'ttl': 3600,
'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5743',
},
),
(
'semicolon',
{
'type': 'TXT',
'ttl': 300,
'value': 'v=DKIM1\\; k=rsa\\; p=blah',
},
),
):
record = Record.new(expected, name, data)
expected.add_record(record)
@ -135,11 +132,7 @@ class TestTinyDnsFileSource(TestCase):
expected = Zone('asdf.subtest.com.', [])
for name, data in (
('a3', {
'type': 'A',
'ttl': 3600,
'values': ['10.2.3.7'],
}),
('a3', {'type': 'A', 'ttl': 3600, 'values': ['10.2.3.7']}),
):
record = Record.new(expected, name, data)
expected.add_record(record)
@ -154,16 +147,8 @@ class TestTinyDnsFileSource(TestCase):
expected = Zone('sub-asdf.subtest.com.', [])
for name, data in (
('a1', {
'type': 'A',
'ttl': 3600,
'values': ['10.2.3.5'],
}),
('a2', {
'type': 'A',
'ttl': 3600,
'values': ['10.2.3.6'],
}),
('a1', {'type': 'A', 'ttl': 3600, 'values': ['10.2.3.5']}),
('a2', {'type': 'A', 'ttl': 3600, 'values': ['10.2.3.6']}),
):
record = Record.new(expected, name, data)
expected.add_record(record)
@ -178,26 +163,24 @@ class TestTinyDnsFileSource(TestCase):
expected = Zone('3.2.10.in-addr.arpa.', [])
for name, data in (
('10', {
'type': 'PTR',
'ttl': 3600,
'value': 'a-ptr.example.com.'
}),
('11', {
'type': 'PTR',
'ttl': 30,
'value': 'a-ptr-2.example.com.'
}),
('8', {
'type': 'PTR',
'ttl': 3600,
'value': 'has-dup-def123.example.com.'
}),
('7', {
'type': 'PTR',
'ttl': 1800,
'value': 'some-host-abc123.example.com.'
}),
('10', {'type': 'PTR', 'ttl': 3600, 'value': 'a-ptr.example.com.'}),
('11', {'type': 'PTR', 'ttl': 30, 'value': 'a-ptr-2.example.com.'}),
(
'8',
{
'type': 'PTR',
'ttl': 3600,
'value': 'has-dup-def123.example.com.',
},
),
(
'7',
{
'type': 'PTR',
'ttl': 1800,
'value': 'some-host-abc123.example.com.',
},
),
):
record = Record.new(expected, name, data)
expected.add_record(record)

View File

@ -2,8 +2,12 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from io import StringIO
from unittest import TestCase
@ -13,58 +17,54 @@ from octodns.yaml import safe_dump, safe_load
class TestYaml(TestCase):
def test_stuff(self):
self.assertEqual({
1: 'a',
2: 'b',
'3': 'c',
10: 'd',
'11': 'e',
}, safe_load('''
self.assertEqual(
{1: 'a', 2: 'b', '3': 'c', 10: 'd', '11': 'e'},
safe_load(
'''
1: a
2: b
'3': c
10: d
'11': e
'''))
'''
),
)
self.assertEqual({
'*.1.2': 'a',
'*.2.2': 'b',
'*.10.1': 'c',
'*.11.2': 'd',
}, safe_load('''
self.assertEqual(
{'*.1.2': 'a', '*.2.2': 'b', '*.10.1': 'c', '*.11.2': 'd'},
safe_load(
'''
'*.1.2': 'a'
'*.2.2': 'b'
'*.10.1': 'c'
'*.11.2': 'd'
'''))
'''
),
)
with self.assertRaises(ConstructorError) as ctx:
safe_load('''
safe_load(
'''
'*.2.2': 'b'
'*.1.2': 'a'
'*.11.2': 'd'
'*.10.1': 'c'
''')
self.assertTrue('keys out of order: expected *.1.2 got *.2.2 at' in
ctx.exception.problem)
'''
)
self.assertTrue(
'keys out of order: expected *.1.2 got *.2.2 at'
in ctx.exception.problem
)
buf = StringIO()
safe_dump({
'*.1.1': 42,
'*.11.1': 43,
'*.2.1': 44,
}, buf)
self.assertEqual("---\n'*.1.1': 42\n'*.2.1': 44\n'*.11.1': 43\n",
buf.getvalue())
safe_dump({'*.1.1': 42, '*.11.1': 43, '*.2.1': 44}, buf)
self.assertEqual(
"---\n'*.1.1': 42\n'*.2.1': 44\n'*.11.1': 43\n", buf.getvalue()
)
# hex sorting isn't ideal, not treated as hex, this make sure we don't
# change the behavior
buf = StringIO()
safe_dump({
'45a03129': 42,
'45a0392a': 43,
}, buf)
safe_dump({'45a03129': 42, '45a0392a': 43}, buf)
self.assertEqual("---\n45a0392a: 43\n45a03129: 42\n", buf.getvalue())

Some files were not shown because too many files have changed in this diff Show More