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:
@ -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"
|
||||
|
@ -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'
|
||||
|
@ -2,5 +2,9 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
from __future__ import (
|
||||
absolute_import,
|
||||
division,
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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__':
|
||||
|
@ -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)
|
||||
|
@ -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__':
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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:
|
||||
|
@ -2,5 +2,9 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
from __future__ import (
|
||||
absolute_import,
|
||||
division,
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.'
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
|
@ -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'},
|
||||
},
|
||||
}
|
||||
|
@ -2,5 +2,9 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
from __future__ import (
|
||||
absolute_import,
|
||||
division,
|
||||
print_function,
|
||||
unicode_literals,
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
137
octodns/zone.py
137
octodns/zone.py
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
9
script/format
Executable 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
|
@ -17,5 +17,4 @@ fi
|
||||
|
||||
SOURCES="*.py octodns/*.py octodns/*/*.py tests/*.py"
|
||||
|
||||
pycodestyle --ignore=E221,E241,E251,E722,E741,W504 $SOURCES
|
||||
pyflakes $SOURCES
|
||||
|
26
setup.py
26
setup.py
@ -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',
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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]),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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]))
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
@ -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'))
|
||||
|
@ -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.', [])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
Reference in New Issue
Block a user