From c09416eb050c3a94e3d43d2df89aa4b7bd26e27d Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 9 Jan 2022 09:12:14 -0800 Subject: [PATCH] Extract EtcHostsProvider from octoDNS core --- CHANGELOG.md | 1 + README.md | 2 +- octodns/provider/etc_hosts.py | 122 ++-------------- tests/test_octodns_provider_etc_hosts.py | 172 +---------------------- 4 files changed, 22 insertions(+), 275 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4a8a4..80e1850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/) * [DynProvider](https://github.com/octodns/octodns-dynprovider/) * [EasyDnsProvider](https://github.com/octodns/octodns-easydns/) + * [EtcHostsProvider](https://github.com/octodns/octodns-etchosts/) * [Ns1Provider](https://github.com/octodns/octodns-ns1/) * [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/) * [Route53Provider](https://github.com/octodns/octodns-route53/) also diff --git a/README.md b/README.md index 388f0fd..a7aeaad 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ The table below lists the providers octoDNS supports. We're currently in the pro | [DnsimpleProvider](https://github.com/octodns/octodns-dnsimple/) | [octodns_dnsimple](https://github.com/octodns/octodns-dnsimple/) | | | | | | [DynProvider](https://github.com/octodns/octodns-dyn/) (deprecated) | [octodns_dyn](https://github.com/octodns/octodns-dyn/) | | | | | | [EasyDnsProvider](https://github.com/octodns/octodns-easydns/) | [octodns_easydns](https://github.com/octodns/octodns-easydns/) | | | | | -| [EtcHostsProvider](/octodns/provider/etc_hosts.py) | | | A, AAAA, ALIAS, CNAME | No | | +| [EtcHostsProvider](https://github.com/octodns/octodns-etchosts/) | [octodns_etchosts](https://github.com/octodns/octodns-etchosts/) | | | | | | [EnvVarSource](/octodns/source/envvar.py) | | | TXT | No | read-only environment variable injection | | [GandiProvider](/octodns/provider/gandi.py) | | | A, AAAA, ALIAS, CAA, CNAME, DNAME, MX, NS, PTR, SPF, SRV, SSHFP, TXT | No | | | [GCoreProvider](/octodns/provider/gcore.py) | | | A, AAAA, NS, MX, TXT, SRV, CNAME, PTR | Dynamic | | diff --git a/octodns/provider/etc_hosts.py b/octodns/provider/etc_hosts.py index 65e9f4f..bd8e95d 100644 --- a/octodns/provider/etc_hosts.py +++ b/octodns/provider/etc_hosts.py @@ -5,111 +5,19 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from os import makedirs, path -from os.path import isdir -import logging +from logging import getLogger -from .base import BaseProvider - - -class EtcHostsProvider(BaseProvider): - ''' - Provider that creates a "best effort" static/emergency content that can be - used in /etc/hosts to resolve things. A, AAAA records are supported and - ALIAS and CNAME records will be included when they can be mapped within the - zone. - - config: - class: octodns.provider.etc_hosts.EtcHostsProvider - # The output director for the hosts file .hosts - directory: ./hosts - ''' - SUPPORTS_GEO = False - SUPPORTS_DYNAMIC = False - SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME')) - - def __init__(self, id, directory, *args, **kwargs): - self.log = logging.getLogger(f'EtcHostsProvider[{id}]') - self.log.debug('__init__: id=%s, directory=%s', id, directory) - super(EtcHostsProvider, self).__init__(id, *args, **kwargs) - self.directory = directory - - def populate(self, zone, target=False, lenient=False): - self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, - target, lenient) - - # We never act as a source, at least for now, if/when we do we still - # need to noop `if target` - 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)) - cnames = {} - values = {} - for record in sorted([c.new for c in changes]): - # Since we don't have existing we'll only see creates - fqdn = record.fqdn[:-1] - if record._type in ('ALIAS', 'CNAME'): - # Store cnames so we can try and look them up in a minute - cnames[fqdn] = record.value[:-1] - elif record._type == 'AAAA' and fqdn in values: - # We'll prefer A over AAAA, skipping rather than replacing an - # existing A - pass - else: - # If we're here it's and A or AAAA and we want to record it's - # value (maybe replacing if it's an A and we have a AAAA - values[fqdn] = record.values[0] - - if not isdir(self.directory): - makedirs(self.directory) - - filepath = path.join(self.directory, desired.name) - filename = f'{filepath}hosts' - self.log.info('_apply: filename=%s', filename) - with open(filename, 'w') as fh: - fh.write('##################################################\n') - fh.write(f'# octoDNS {self.id} {desired.name}\n') - fh.write('##################################################\n\n') - if values: - fh.write('## A & AAAA\n\n') - for fqdn, value in sorted(values.items()): - if fqdn[0] == '*': - fh.write('# ') - fh.write(f'{value}\t{fqdn}\n\n') - - if cnames: - fh.write('\n## CNAME (mapped)\n\n') - for fqdn, value in sorted(cnames.items()): - # Print out a comment of the first level - fh.write(f'# {fqdn} -> {value}\n') - seen = set() - while True: - seen.add(value) - try: - value = values[value] - # If we're here we've found the target, print it - # and break the loop - fh.write(f'{value}\t{fqdn}\n') - break - except KeyError: - # Try and step down one level - orig = value - value = cnames.get(value, None) - # Print out this step - if value: - if value in seen: - # We'd loop here, break it - fh.write(f'# {orig} -> {value} **loop**\n') - break - else: - fh.write(f'# {orig} -> {value}\n') - else: - # Don't have anywhere else to go - fh.write(f'# {orig} -> **unknown**\n') - break - - fh.write('\n') +logger = getLogger('EtcHosts') +try: + logger.warn('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 seperate module, ' + 'octodns_etchosts is now required. Provider class should ' + 'be updated to octodns_etchosts.EtcHostsProvider. See ' + 'https://github.com/octodns/octodns/README.md#updating-' + 'to-use-extracted-providers for more information.') + raise diff --git a/tests/test_octodns_provider_etc_hosts.py b/tests/test_octodns_provider_etc_hosts.py index fc518bd..afefc0e 100644 --- a/tests/test_octodns_provider_etc_hosts.py +++ b/tests/test_octodns_provider_etc_hosts.py @@ -5,174 +5,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from os import path -from os.path import dirname, isfile from unittest import TestCase -from octodns.provider.etc_hosts import EtcHostsProvider -from octodns.provider.plan import Plan -from octodns.record import Record -from octodns.zone import Zone -from helpers import TemporaryDirectory +class TestEtcHostsShim(TestCase): - -class TestEtcHostsProvider(TestCase): - - def test_provider(self): - source = EtcHostsProvider('test', path.join(dirname(__file__), - 'config')) - - zone = Zone('unit.tests.', []) - - # We never populate anything, when acting as a source - source.populate(zone, target=source) - self.assertEquals(0, len(zone.records)) - # Same if we're acting as a target - source.populate(zone) - self.assertEquals(0, len(zone.records)) - - record = Record.new(zone, '', { - 'ttl': 60, - 'type': 'ALIAS', - 'value': 'www.unit.tests.' - }) - zone.add_record(record) - - record = Record.new(zone, 'www', { - 'ttl': 60, - 'type': 'AAAA', - 'value': '2001:4860:4860::8888', - }) - zone.add_record(record) - record = Record.new(zone, 'www', { - 'ttl': 60, - 'type': 'A', - 'values': ['1.1.1.1', '2.2.2.2'], - }) - zone.add_record(record) - - record = record.new(zone, 'v6', { - 'ttl': 60, - 'type': 'AAAA', - 'value': '2001:4860:4860::8844', - }) - zone.add_record(record) - - record = record.new(zone, 'start', { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'middle.unit.tests.', - }) - zone.add_record(record) - record = record.new(zone, 'middle', { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'unit.tests.', - }) - zone.add_record(record) - - record = record.new(zone, 'ext', { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'github.com.', - }) - zone.add_record(record) - - record = record.new(zone, '*', { - 'ttl': 60, - 'type': 'A', - 'value': '3.3.3.3', - }) - zone.add_record(record) - - with TemporaryDirectory() as td: - # Add some subdirs to make sure that it can create them - directory = path.join(td.dirname, 'sub', 'dir') - hosts_file = path.join(directory, 'unit.tests.hosts') - target = EtcHostsProvider('test', directory) - - # We add everything - plan = target.plan(zone) - self.assertEquals(len(zone.records), len(plan.changes)) - self.assertFalse(isfile(hosts_file)) - - # Now actually do it - self.assertEquals(len(zone.records), target.apply(plan)) - self.assertTrue(isfile(hosts_file)) - - with open(hosts_file) as fh: - data = fh.read() - # v6 - self.assertTrue('2001:4860:4860::8844\tv6.unit.tests' in data) - # www - self.assertTrue('1.1.1.1\twww.unit.tests' in data) - # root ALIAS - self.assertTrue('# unit.tests -> www.unit.tests' in data) - self.assertTrue('1.1.1.1\tunit.tests' in data) - - self.assertTrue('# start.unit.tests -> middle.unit.tests' in - data) - self.assertTrue('# middle.unit.tests -> unit.tests' in data) - self.assertTrue('# unit.tests -> www.unit.tests' in data) - self.assertTrue('1.1.1.1 start.unit.tests' in data) - - # second empty run that won't create dirs and overwrites file - plan = Plan(zone, zone, [], True) - self.assertEquals(0, target.apply(plan)) - - def test_cname_loop(self): - source = EtcHostsProvider('test', path.join(dirname(__file__), - 'config')) - - zone = Zone('unit.tests.', []) - - # We never populate anything, when acting as a source - source.populate(zone, target=source) - self.assertEquals(0, len(zone.records)) - # Same if we're acting as a target - source.populate(zone) - self.assertEquals(0, len(zone.records)) - - record = Record.new(zone, 'start', { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'middle.unit.tests.', - }) - zone.add_record(record) - record = Record.new(zone, 'middle', { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'loop.unit.tests.', - }) - zone.add_record(record) - record = Record.new(zone, 'loop', { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'start.unit.tests.', - }) - zone.add_record(record) - - with TemporaryDirectory() as td: - # Add some subdirs to make sure that it can create them - directory = path.join(td.dirname, 'sub', 'dir') - hosts_file = path.join(directory, 'unit.tests.hosts') - target = EtcHostsProvider('test', directory) - - # We add everything - plan = target.plan(zone) - self.assertEquals(len(zone.records), len(plan.changes)) - self.assertFalse(isfile(hosts_file)) - - # Now actually do it - self.assertEquals(len(zone.records), target.apply(plan)) - self.assertTrue(isfile(hosts_file)) - - with open(hosts_file) as fh: - data = fh.read() - self.assertTrue('# loop.unit.tests -> start.unit.tests ' - '**loop**' in data) - self.assertTrue('# middle.unit.tests -> loop.unit.tests ' - '**loop**' in data) - self.assertTrue('# start.unit.tests -> middle.unit.tests ' - '**loop**' in data) + def test_missing(self): + with self.assertRaises(ModuleNotFoundError): + from octodns.provider.etc_hosts import EtcHostsProvider + EtcHostsProvider