From 3d0f5aeca0fcce677303d2a4663b859c104cb1b2 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 2 Dec 2017 11:40:55 -0800 Subject: [PATCH] Config-based plan_output Refactors the provider class lookup and kwarg processing so that it can be reused for plan_output. --- octodns/manager.py | 81 ++++++++++++------- octodns/provider/plan.py | 29 +++++-- tests/config/bad-plan-output-config.yaml | 7 ++ .../config/bad-plan-output-missing-class.yaml | 5 ++ tests/test_octodns_manager.py | 13 +++ tests/test_octodns_plan.py | 19 +++++ 6 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 tests/config/bad-plan-output-config.yaml create mode 100644 tests/config/bad-plan-output-missing-class.yaml create mode 100644 tests/test_octodns_plan.py diff --git a/octodns/manager.py b/octodns/manager.py index a737aa4..d4debf6 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -11,7 +11,7 @@ from os import environ import logging from .provider.base import BaseProvider -from .provider.plan import Plan, PlanLogger +from .provider.plan import Plan from .provider.yaml import YamlProvider from .record import Record from .yaml import safe_load @@ -68,8 +68,6 @@ class Manager(object): def __init__(self, config_file, max_workers=None, include_meta=False): self.log.info('__init__: config_file=%s', config_file) - self.plan_outputs = [PlanLogger(self.log)] - # Read our config file with open(config_file, 'r') as fh: self.config = safe_load(fh, enforce_order=False) @@ -97,23 +95,8 @@ class Manager(object): self.log.exception('Invalid provider class') raise Exception('Provider {} is missing class' .format(provider_name)) - _class = self._get_provider_class(_class) - # Build up the arguments we need to pass to the provider - kwargs = {} - for k, v in provider_config.items(): - try: - if v.startswith('env/'): - try: - env_var = v[4:] - v = environ[env_var] - except KeyError: - self.log.exception('Invalid provider config') - raise Exception('Incorrect provider config, ' - 'missing env var {}' - .format(env_var)) - except AttributeError: - pass - kwargs[k] = v + _class = self._get_named_class('provider', _class) + kwargs = self._build_kwargs(provider_config) try: self.providers[provider_name] = _class(provider_name, **kwargs) except TypeError: @@ -141,20 +124,64 @@ class Manager(object): where = where[piece] self.zone_tree = zone_tree - def _get_provider_class(self, _class): + self.plan_outputs = {} + 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 Exception('plan_output {} is missing class' + .format(plan_output_name)) + _class = 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) + except TypeError: + self.log.exception('Invalid plan_output config') + raise Exception('Incorrect plan_output config for {}' + .format(plan_output_name)) + + def _get_named_class(self, _type, _class): try: module_name, class_name = _class.rsplit('.', 1) module = import_module(module_name) except (ImportError, ValueError): - self.log.exception('_get_provider_class: Unable to import ' + self.log.exception('_get_{}_class: Unable to import ' 'module %s', _class) - raise Exception('Unknown provider class: {}'.format(_class)) + raise Exception('Unknown {} class: {}'.format(_type, _class)) try: return getattr(module, class_name) except AttributeError: - self.log.exception('_get_provider_class: Unable to get class %s ' + self.log.exception('_get_{}_class: Unable to get class %s ' 'from module %s', class_name, module) - raise Exception('Unknown provider class: {}'.format(_class)) + raise Exception('Unknown {} class: {}'.format(_type, _class)) + + def _build_kwargs(self, source): + # Build up the arguments we need to pass to the provider + kwargs = {} + for k, v in source.items(): + try: + if v.startswith('env/'): + try: + env_var = v[4:] + v = environ[env_var] + except KeyError: + self.log.exception('Invalid provider config') + raise Exception('Incorrect provider config, ' + 'missing env var {}' + .format(env_var)) + except AttributeError: + pass + kwargs[k] = v + + return kwargs def configured_sub_zones(self, zone_name): # Reversed pieces of the zone name @@ -261,8 +288,8 @@ class Manager(object): # plan pairs. plans = [p for f in futures for p in f.result()] - for output in self.plan_outputs: - output.run(plans) + for output in self.plan_outputs.values(): + output.run(plans=plans, log=self.log) if not force: self.log.debug('sync: checking safety') diff --git a/octodns/provider/plan.py b/octodns/provider/plan.py index 009202f..b49c200 100644 --- a/octodns/provider/plan.py +++ b/octodns/provider/plan.py @@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals from StringIO import StringIO -from logging import INFO, getLogger +from logging import DEBUG, ERROR, INFO, WARN, getLogger class UnsafePlan(Exception): @@ -80,13 +80,28 @@ class Plan(object): len(self.existing.records)) -class PlanLogger(object): +class _PlanOutput(object): - def __init__(self, log, level=INFO): - self.log = log - self.level = level + def __init__(self, name): + self.name = name - def run(self, plans): + +class PlanLogger(_PlanOutput): + + def __init__(self, name, level='info'): + super(PlanLogger, self).__init__(name) + try: + self.level = { + 'debug': DEBUG, + 'info': INFO, + 'warn': WARN, + 'warning': WARN, + 'error': ERROR + }[level.lower()] + except (AttributeError, KeyError): + raise Exception('Unsupported level: {}'.format(level)) + + def run(self, log, plans, *args, **kwargs): hr = '*************************************************************' \ '*******************\n' buf = StringIO() @@ -119,4 +134,4 @@ class PlanLogger(object): buf.write('No changes were planned\n') buf.write(hr) buf.write('\n') - self.log.log(self.level, buf.getvalue()) + log.log(self.level, buf.getvalue()) diff --git a/tests/config/bad-plan-output-config.yaml b/tests/config/bad-plan-output-config.yaml new file mode 100644 index 0000000..f345f89 --- /dev/null +++ b/tests/config/bad-plan-output-config.yaml @@ -0,0 +1,7 @@ +manager: + plan_outputs: + 'bad': + class: octodns.provider.plan.PlanLogger + invalid: config +providers: {} +zones: {} diff --git a/tests/config/bad-plan-output-missing-class.yaml b/tests/config/bad-plan-output-missing-class.yaml new file mode 100644 index 0000000..71b1bd5 --- /dev/null +++ b/tests/config/bad-plan-output-missing-class.yaml @@ -0,0 +1,5 @@ +manager: + plan_outputs: + 'bad': {} +providers: {} +zones: {} diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 4db2103..ada54e5 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -83,6 +83,19 @@ class TestManager(TestCase): .sync(['unknown.target.']) self.assertTrue('unknown target' in ctx.exception.message) + def test_bad_plan_output_class(self): + with self.assertRaises(Exception) as ctx: + name = 'bad-plan-output-missing-class.yaml' + Manager(get_config_filename(name)).sync() + self.assertEquals('plan_output bad is missing class', + ctx.exception.message) + + def test_bad_plan_output_config(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('bad-plan-output-config.yaml')).sync() + self.assertEqual('Incorrect plan_output config for bad', + ctx.exception.message) + def test_source_only_as_a_target(self): with self.assertRaises(Exception) as ctx: Manager(get_config_filename('unknown-provider.yaml')) \ diff --git a/tests/test_octodns_plan.py b/tests/test_octodns_plan.py new file mode 100644 index 0000000..2b23b4e --- /dev/null +++ b/tests/test_octodns_plan.py @@ -0,0 +1,19 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from octodns.provider.plan import PlanLogger + + +class TestPlanLogger(TestCase): + + def test_invalid_level(self): + with self.assertRaises(Exception) as ctx: + PlanLogger('invalid', 'not-a-level') + self.assertEquals('Unsupported level: not-a-level', + ctx.exception.message)