1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

Implement EtcHostsProvider, content to be used in /etc/hosts

...for testing or emergencies
This commit is contained in:
Ross McFarland
2018-06-01 20:59:03 -07:00
parent f8a29f49d5
commit 36b67b8b7a
4 changed files with 246 additions and 0 deletions

View File

@@ -1,5 +1,7 @@
## v0.9.2 - Unreleased
* EtcHostsProvider implementation to create static/emergency best effort
content that can be used in /etc/hosts to resolve things.
* Add lenient support to Zone.add_record, allows populate from providers that
have allowed/created invalid data and situations where a sub-zone is being
extracted from a parent, but the records still exist in the remote provider.

View File

@@ -155,6 +155,7 @@ The above command pulled the existing data out of Route53 and placed the results
| [DnsMadeEasyProvider](/octodns/provider/dnsmadeeasy.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
| [DnsimpleProvider](/octodns/provider/dnsimple.py) | | All | No | CAA tags restricted |
| [DynProvider](/octodns/provider/dyn.py) | dyn | All | Yes | |
| [EtcHostsProvider](/octodns/provider/etc_hosts.py) | | A, AAAA, ALIAS, CNAME | No | |
| [GoogleCloudProvider](/octodns/provider/googlecloud.py) | google-cloud | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | No | |
| [Ns1Provider](/octodns/provider/ns1.py) | nsone | All | Yes | No health checking for GeoDNS |
| [OVH](/octodns/provider/ovh.py) | ovh | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT, DKIM | No | |

View File

@@ -0,0 +1,123 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from os import makedirs
from os.path import isdir, join
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 = set(('A', 'AAAA', 'ALIAS', 'CNAME'))
def __init__(self, id, directory, *args, **kwargs):
self.log = logging.getLogger('EtcHostsProvider[{}]'.format(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)
filename = '{}hosts'.format(join(self.directory, desired.name))
self.log.info('_apply: filename=%s', filename)
with open(filename, 'w') as fh:
fh.write('########################################\n')
fh.write('# octoDNS ')
fh.write(self.id)
fh.write('\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(value)
fh.write('\t')
fh.write(fqdn)
fh.write('\n\n')
if cnames:
fh.write('\n')
fh.write('## CNAME (mapped)\n\n')
for fqdn, value in sorted(cnames.items()):
# Print out a comment of the first level
fh.write('# ')
fh.write(fqdn)
fh.write(' -> ')
fh.write(value)
fh.write('\n')
# No loop protection :-/
while True:
try:
value = values[value]
# If we're here we've found the target, print it
# and break the loop
fh.write(value)
fh.write('\t')
fh.write(fqdn)
fh.write('\n')
break
except KeyError:
# Try and step down one level
orig = value
value = cnames.get(value, None)
# Print out this step
fh.write('# ')
fh.write(orig)
fh.write(' -> ')
if value:
fh.write(value)
else:
# Don't have anywhere else to go
fh.write('**unknown**')
break
fh.write('\n')
fh.write('\n')

View File

@@ -0,0 +1,120 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from os.path import dirname, isfile, join
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 TestEtcHostsProvider(TestCase):
def test_provider(self):
source = EtcHostsProvider('test', 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 = join(td.dirname, 'sub', 'dir')
hosts_file = 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')
# 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))