mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge branch 'master' into extract-gandi
This commit is contained in:
@@ -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/)
|
||||
* [GandiProvider](https://github.com/octodns/octodns-gandi/)
|
||||
* [Ns1Provider](https://github.com/octodns/octodns-ns1/)
|
||||
* [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/)
|
||||
|
||||
@@ -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](https://github.com/octodns/octodns-gandi/) | [octodns_gandi](https://github.com/octodns/octodns-gandi/) | | | | |
|
||||
| [GCoreProvider](/octodns/provider/gcore.py) | | | A, AAAA, NS, MX, TXT, SRV, CNAME, PTR | Dynamic | |
|
||||
|
||||
@@ -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 <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')
|
||||
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,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
|
||||
|
||||
Reference in New Issue
Block a user