...for testing or emergenciespull/245/head
| @ -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') | |||||
| @ -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)) | |||||