|
|
@ -9,9 +9,10 @@ from os import environ |
|
|
from os.path import dirname, join |
|
|
from os.path import dirname, join |
|
|
from six import text_type |
|
|
from six import text_type |
|
|
|
|
|
|
|
|
from octodns.record import Record |
|
|
|
|
|
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \ |
|
|
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \ |
|
|
ManagerException |
|
|
ManagerException |
|
|
|
|
|
from octodns.processor.base import BaseProcessor |
|
|
|
|
|
from octodns.record import Create, Delete, Record |
|
|
from octodns.yaml import safe_load |
|
|
from octodns.yaml import safe_load |
|
|
from octodns.zone import Zone |
|
|
from octodns.zone import Zone |
|
|
|
|
|
|
|
|
@ -19,7 +20,7 @@ from mock import MagicMock, patch |
|
|
from unittest import TestCase |
|
|
from unittest import TestCase |
|
|
|
|
|
|
|
|
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \ |
|
|
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \ |
|
|
SimpleProvider, TemporaryDirectory |
|
|
|
|
|
|
|
|
PlannableProvider, SimpleProvider, TemporaryDirectory |
|
|
|
|
|
|
|
|
config_dir = join(dirname(__file__), 'config') |
|
|
config_dir = join(dirname(__file__), 'config') |
|
|
|
|
|
|
|
|
@ -338,6 +339,11 @@ class TestManager(TestCase): |
|
|
Manager(get_config_filename('simple-alias-zone.yaml')) \ |
|
|
Manager(get_config_filename('simple-alias-zone.yaml')) \ |
|
|
.validate_configs() |
|
|
.validate_configs() |
|
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(ManagerException) as ctx: |
|
|
|
|
|
Manager(get_config_filename('unknown-processor.yaml')) \ |
|
|
|
|
|
.validate_configs() |
|
|
|
|
|
self.assertTrue('unknown processor' in text_type(ctx.exception)) |
|
|
|
|
|
|
|
|
def test_get_zone(self): |
|
|
def test_get_zone(self): |
|
|
Manager(get_config_filename('simple.yaml')).get_zone('unit.tests.') |
|
|
Manager(get_config_filename('simple.yaml')).get_zone('unit.tests.') |
|
|
|
|
|
|
|
|
@ -358,20 +364,48 @@ class TestManager(TestCase): |
|
|
|
|
|
|
|
|
class NoLenient(SimpleProvider): |
|
|
class NoLenient(SimpleProvider): |
|
|
|
|
|
|
|
|
def populate(self, zone, source=False): |
|
|
|
|
|
|
|
|
def populate(self, zone): |
|
|
pass |
|
|
pass |
|
|
|
|
|
|
|
|
# This should be ok, we'll fall back to not passing it |
|
|
# This should be ok, we'll fall back to not passing it |
|
|
manager._populate_and_plan('unit.tests.', [NoLenient()], []) |
|
|
|
|
|
|
|
|
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()], |
|
|
|
|
|
[]) |
|
|
|
|
|
self.assertEquals('something else', text_type(ctx.exception)) |
|
|
|
|
|
|
|
|
|
|
|
def test_plan_processors_fallback(self): |
|
|
|
|
|
with TemporaryDirectory() as tmpdir: |
|
|
|
|
|
environ['YAML_TMP_DIR'] = tmpdir.dirname |
|
|
|
|
|
# Only allow a target that doesn't exist |
|
|
|
|
|
manager = Manager(get_config_filename('simple.yaml')) |
|
|
|
|
|
|
|
|
class NoZone(SimpleProvider): |
|
|
|
|
|
|
|
|
class NoProcessors(SimpleProvider): |
|
|
|
|
|
|
|
|
def populate(self, lenient=False): |
|
|
|
|
|
|
|
|
def plan(self, zone): |
|
|
pass |
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
# This should be ok, we'll fall back to not passing it |
|
|
|
|
|
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 |
|
|
# This will blow up, we don't fallback for source |
|
|
with self.assertRaises(TypeError): |
|
|
|
|
|
manager._populate_and_plan('unit.tests.', [NoZone()], []) |
|
|
|
|
|
|
|
|
with self.assertRaises(TypeError) as ctx: |
|
|
|
|
|
manager._populate_and_plan('unit.tests.', [], [], |
|
|
|
|
|
[OtherType()]) |
|
|
|
|
|
self.assertEquals('something else', text_type(ctx.exception)) |
|
|
|
|
|
|
|
|
@patch('octodns.manager.Manager._get_named_class') |
|
|
@patch('octodns.manager.Manager._get_named_class') |
|
|
def test_sync_passes_file_handle(self, mock): |
|
|
def test_sync_passes_file_handle(self, mock): |
|
|
@ -391,6 +425,92 @@ class TestManager(TestCase): |
|
|
_, kwargs = plan_output_mock.run.call_args |
|
|
_, kwargs = plan_output_mock.run.call_args |
|
|
self.assertEqual(fh_mock, kwargs.get('fh')) |
|
|
self.assertEqual(fh_mock, kwargs.get('fh')) |
|
|
|
|
|
|
|
|
|
|
|
def test_processor_config(self): |
|
|
|
|
|
# Smoke test loading a valid config |
|
|
|
|
|
manager = Manager(get_config_filename('processors.yaml')) |
|
|
|
|
|
self.assertEquals(['noop'], list(manager.processors.keys())) |
|
|
|
|
|
# This zone specifies a valid processor |
|
|
|
|
|
manager.sync(['unit.tests.']) |
|
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(ManagerException) as ctx: |
|
|
|
|
|
# This zone specifies a non-existant processor |
|
|
|
|
|
manager.sync(['bad.unit.tests.']) |
|
|
|
|
|
self.assertTrue('Zone bad.unit.tests., unknown processor: ' |
|
|
|
|
|
'doesnt-exist' in text_type(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 |
|
|
|
|
|
text_type(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 |
|
|
|
|
|
text_type(ctx.exception)) |
|
|
|
|
|
|
|
|
|
|
|
def test_processors(self): |
|
|
|
|
|
manager = Manager(get_config_filename('simple.yaml')) |
|
|
|
|
|
|
|
|
|
|
|
targets = [PlannableProvider('prov')] |
|
|
|
|
|
|
|
|
|
|
|
zone = Zone('unit.tests.', []) |
|
|
|
|
|
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 = self._clone_zone(zone) |
|
|
|
|
|
zone.add_record(record) |
|
|
|
|
|
return zone |
|
|
|
|
|
|
|
|
|
|
|
mock = MockProcessor('mock') |
|
|
|
|
|
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [], |
|
|
|
|
|
targets) |
|
|
|
|
|
# Our mock was called and added the record |
|
|
|
|
|
self.assertEquals(record, list(zone.records)[0]) |
|
|
|
|
|
# We got a create for the thing added to the expected state (source) |
|
|
|
|
|
self.assertIsInstance(plans[0][1].changes[0], Create) |
|
|
|
|
|
|
|
|
|
|
|
# muck with targets |
|
|
|
|
|
class MockProcessor(BaseProcessor): |
|
|
|
|
|
|
|
|
|
|
|
def process_target_zone(self, zone, target): |
|
|
|
|
|
zone = self._clone_zone(zone) |
|
|
|
|
|
zone.add_record(record) |
|
|
|
|
|
return zone |
|
|
|
|
|
|
|
|
|
|
|
mock = MockProcessor('mock') |
|
|
|
|
|
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) |
|
|
|
|
|
self.assertIsInstance(plans[0][1].changes[0], Delete) |
|
|
|
|
|
|
|
|
|
|
|
# muck with plans |
|
|
|
|
|
class MockProcessor(BaseProcessor): |
|
|
|
|
|
|
|
|
|
|
|
def process_target_zone(self, zone, target): |
|
|
|
|
|
zone = self._clone_zone(zone) |
|
|
|
|
|
zone.add_record(record) |
|
|
|
|
|
return zone |
|
|
|
|
|
|
|
|
|
|
|
def process_plan(self, plans, sources, target): |
|
|
|
|
|
# get rid of the change |
|
|
|
|
|
plans.changes.pop(0) |
|
|
|
|
|
|
|
|
|
|
|
mock = MockProcessor('mock') |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMainThreadExecutor(TestCase): |
|
|
class TestMainThreadExecutor(TestCase): |
|
|
|
|
|
|
|
|
|