Browse Source

Extract EtcHostsProvider from octoDNS core

pull/841/head
Ross McFarland 4 years ago
parent
commit
c09416eb05
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
4 changed files with 23 additions and 276 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +1
    -1
      README.md
  3. +16
    -108
      octodns/provider/etc_hosts.py
  4. +5
    -167
      tests/test_octodns_provider_etc_hosts.py

+ 1
- 0
CHANGELOG.md View File

@ -14,6 +14,7 @@
* [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/) * [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/)
* [DynProvider](https://github.com/octodns/octodns-dynprovider/) * [DynProvider](https://github.com/octodns/octodns-dynprovider/)
* [EasyDnsProvider](https://github.com/octodns/octodns-easydns/) * [EasyDnsProvider](https://github.com/octodns/octodns-easydns/)
* [EtcHostsProvider](https://github.com/octodns/octodns-etchosts/)
* [Ns1Provider](https://github.com/octodns/octodns-ns1/) * [Ns1Provider](https://github.com/octodns/octodns-ns1/)
* [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/) * [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/)
* [Route53Provider](https://github.com/octodns/octodns-route53/) also * [Route53Provider](https://github.com/octodns/octodns-route53/) also


+ 1
- 1
README.md View File

@ -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/) | | | | | | [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/) | | | | | | [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/) | | | | | | [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 | | [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 | | | [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 | | | [GCoreProvider](/octodns/provider/gcore.py) | | | A, AAAA, NS, MX, TXT, SRV, CNAME, PTR | Dynamic | |


+ 16
- 108
octodns/provider/etc_hosts.py View File

@ -5,111 +5,19 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from os import makedirs, path
from os.path import isdir
import logging
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 <zone>.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')
from logging import getLogger
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

+ 5
- 167
tests/test_octodns_provider_etc_hosts.py View File

@ -5,174 +5,12 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from os import path
from os.path import dirname, isfile
from unittest import TestCase 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

Loading…
Cancel
Save