...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)) | |||