From ff2fec72d839baf21c198fc29fa00c688e37d673 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 4 Jun 2017 19:03:38 -0700 Subject: [PATCH 1/4] Add support for ignored records. ```yaml ignored: octodns: ignored: true type: A value: 1.2.3.4 ``` --- octodns/record.py | 3 +++ octodns/zone.py | 6 +++++ tests/config/unit.tests.yaml | 5 ++++ tests/test_octodns_provider_dnsimple.py | 4 +-- tests/test_octodns_provider_powerdns.py | 15 +++++------ tests/test_octodns_provider_yaml.py | 2 +- tests/test_octodns_zone.py | 33 +++++++++++++++++++++++++ 7 files changed, 58 insertions(+), 10 deletions(-) diff --git a/octodns/record.py b/octodns/record.py index 570988b..163efc1 100644 --- a/octodns/record.py +++ b/octodns/record.py @@ -112,6 +112,9 @@ class Record(object): raise Exception('Invalid record {}, missing ttl'.format(self.fqdn)) self.source = source + octodns = data.get('octodns', {}) + self.ignored = octodns.get('ignored', False) + def _data(self): return {'ttl': self.ttl} diff --git a/octodns/zone.py b/octodns/zone.py index e9c64b4..1822fec 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -76,8 +76,12 @@ class Zone(object): # Find diffs & removes for record in filter(_is_eligible, self.records): + if record.ignored: + continue try: desired_record = desired_records[record] + if desired_record.ignored: + continue except KeyError: if not target.supports(record): self.log.debug('changes: skipping record=%s %s - %s does ' @@ -103,6 +107,8 @@ class Zone(object): # This uses set math and our special __hash__ and __cmp__ functions as # well for record in filter(_is_eligible, desired.records - self.records): + if record.ignored: + continue if not target.supports(record): self.log.debug('changes: skipping record=%s %s - %s does not ' 'support it', record.fqdn, record._type, diff --git a/tests/config/unit.tests.yaml b/tests/config/unit.tests.yaml index c71638b..d18bf59 100644 --- a/tests/config/unit.tests.yaml +++ b/tests/config/unit.tests.yaml @@ -51,6 +51,11 @@ cname: ttl: 300 type: CNAME value: unit.tests. +ignored: + octodns: + ignored: true + type: A + value: 9.9.9.9 mx: ttl: 300 type: MX diff --git a/tests/test_octodns_provider_dnsimple.py b/tests/test_octodns_provider_dnsimple.py index ace7376..1f62bfd 100644 --- a/tests/test_octodns_provider_dnsimple.py +++ b/tests/test_octodns_provider_dnsimple.py @@ -129,8 +129,8 @@ class TestDnsimpleProvider(TestCase): ] plan = provider.plan(self.expected) - # No root NS - n = len(self.expected.records) - 1 + # No root NS, no ignored + n = len(self.expected.records) - 2 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) diff --git a/tests/test_octodns_provider_powerdns.py b/tests/test_octodns_provider_powerdns.py index fd2752c..01e7d83 100644 --- a/tests/test_octodns_provider_powerdns.py +++ b/tests/test_octodns_provider_powerdns.py @@ -78,7 +78,8 @@ class TestPowerDnsProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) - self.assertEquals(14, len(expected.records)) + expected_n = len(expected.records) - 1 + self.assertEquals(14, expected_n) # No diffs == no changes with requests_mock() as mock: @@ -93,7 +94,7 @@ class TestPowerDnsProvider(TestCase): # Used in a minute def assert_rrsets_callback(request, context): data = loads(request.body) - self.assertEquals(len(expected.records), len(data['rrsets'])) + self.assertEquals(expected_n, len(data['rrsets'])) return '' # No existing records -> creates for every record in expected @@ -103,8 +104,8 @@ class TestPowerDnsProvider(TestCase): mock.patch(ANY, status_code=201, text=assert_rrsets_callback) plan = provider.plan(expected) - self.assertEquals(len(expected.records), len(plan.changes)) - self.assertEquals(len(expected.records), provider.apply(plan)) + self.assertEquals(expected_n, len(plan.changes)) + self.assertEquals(expected_n, provider.apply(plan)) # Non-existent zone -> creates for every record in expected # OMG this is fucking ugly, probably better to ditch requests_mocks and @@ -121,8 +122,8 @@ class TestPowerDnsProvider(TestCase): mock.post(ANY, status_code=201, text=assert_rrsets_callback) plan = provider.plan(expected) - self.assertEquals(len(expected.records), len(plan.changes)) - self.assertEquals(len(expected.records), provider.apply(plan)) + self.assertEquals(expected_n, len(plan.changes)) + self.assertEquals(expected_n, provider.apply(plan)) with requests_mock() as mock: # get 422's, unknown zone @@ -166,7 +167,7 @@ class TestPowerDnsProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) - self.assertEquals(14, len(expected.records)) + self.assertEquals(15, len(expected.records)) # A small change to a single record with requests_mock() as mock: diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py index a557bb3..05c5248 100644 --- a/tests/test_octodns_provider_yaml.py +++ b/tests/test_octodns_provider_yaml.py @@ -30,7 +30,7 @@ class TestYamlProvider(TestCase): # without it we see everything source.populate(zone) - self.assertEquals(14, len(zone.records)) + self.assertEquals(15, len(zone.records)) # Assumption here is that a clean round-trip means that everything # worked as expected, data that went in came back out and could be diff --git a/tests/test_octodns_zone.py b/tests/test_octodns_zone.py index da83dfc..88bbb68 100644 --- a/tests/test_octodns_zone.py +++ b/tests/test_octodns_zone.py @@ -172,3 +172,36 @@ class TestZone(TestCase): with self.assertRaises(SubzoneRecordException) as ctx: zone.add_record(record) self.assertTrue('under a managed sub-zone', ctx.exception.message) + + def test_ignored_records(self): + zone_normal = Zone('unit.tests.', []) + zone_ignored = Zone('unit.tests.', []) + zone_missing = Zone('unit.tests.', []) + + normal = Record.new(zone_normal, 'www', { + 'ttl': 60, + 'type': 'A', + 'value': '9.9.9.9', + }) + zone_normal.add_record(normal) + + ignored = Record.new(zone_ignored, 'www', { + 'octodns': { + 'ignored': True + }, + 'ttl': 60, + 'type': 'A', + 'value': '9.9.9.9', + }) + zone_ignored.add_record(ignored) + + provider = SimpleProvider() + + self.assertFalse(zone_normal.changes(zone_ignored, provider)) + self.assertTrue(zone_normal.changes(zone_missing, provider)) + + self.assertFalse(zone_ignored.changes(zone_normal, provider)) + self.assertFalse(zone_ignored.changes(zone_missing, provider)) + + self.assertTrue(zone_missing.changes(zone_normal, provider)) + self.assertFalse(zone_missing.changes(zone_ignored, provider)) From dd0042c6ff9f4b8824f1f46a885339f5893cc0d3 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 8 Jun 2017 17:55:19 -0700 Subject: [PATCH 2/4] Escape unescaped semicolons coming out of Route53 --- octodns/provider/route53.py | 5 ++++- tests/test_octodns_provider_route53.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/octodns/provider/route53.py b/octodns/provider/route53.py index 3849561..4d1b2e9 100644 --- a/octodns/provider/route53.py +++ b/octodns/provider/route53.py @@ -253,10 +253,13 @@ class Route53Provider(BaseProvider): _data_for_PTR = _data_for_single _data_for_CNAME = _data_for_single + _fix_semicolons = re.compile(r'(? Date: Thu, 8 Jun 2017 18:34:33 -0700 Subject: [PATCH 3/4] Fix zone-level always-dry-run functionality Thanks @offmindby! --- octodns/manager.py | 8 +++++++- tests/test_octodns_manager.py | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/octodns/manager.py b/octodns/manager.py index 2545e71..11a675b 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -273,12 +273,18 @@ class Manager(object): for target, plan in plans: plan.raise_if_unsafe() - if dry_run or config.get('always-dry-run', False): + if dry_run: return 0 total_changes = 0 self.log.debug('sync: applying') + zones = self.config['zones'] 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) + continue total_changes += target.apply(plan) self.log.info('sync: %d total changes', total_changes) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 811503a..fa8bdd1 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -88,6 +88,14 @@ class TestManager(TestCase): .sync(['not.targetable.']) self.assertTrue('does not support targeting' in ctx.exception.message) + 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) + # only the stuff from subzone, unit.tests. is always-dry-run + self.assertEquals(3, tc) + def test_simple(self): with TemporaryDirectory() as tmpdir: environ['YAML_TMP_DIR'] = tmpdir.dirname From 7e0730ea1b76ec6179850adaccdb76703ef5f00f Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 8 Jun 2017 18:45:47 -0700 Subject: [PATCH 4/4] Helps if I add the new config file --- tests/config/always-dry-run.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/config/always-dry-run.yaml diff --git a/tests/config/always-dry-run.yaml b/tests/config/always-dry-run.yaml new file mode 100644 index 0000000..466c26b --- /dev/null +++ b/tests/config/always-dry-run.yaml @@ -0,0 +1,20 @@ +providers: + in: + class: octodns.provider.yaml.YamlProvider + directory: tests/config + dump: + class: octodns.provider.yaml.YamlProvider + directory: env/YAML_TMP_DIR +zones: + unit.tests.: + always-dry-run: true + sources: + - in + targets: + - dump + subzone.unit.tests.: + always-dry-run: false + sources: + - in + targets: + - dump