Browse Source

Merge remote-tracking branch 'origin' into processors

pull/637/head
Ross McFarland 5 years ago
parent
commit
395a5c7054
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
14 changed files with 95 additions and 36 deletions
  1. +1
    -4
      .github/workflows/main.yml
  2. +3
    -3
      README.md
  3. +10
    -6
      octodns/manager.py
  4. +3
    -3
      octodns/provider/dnsimple.py
  5. +1
    -1
      octodns/provider/easydns.py
  6. +1
    -1
      octodns/provider/plan.py
  7. +2
    -2
      octodns/provider/transip.py
  8. +8
    -4
      octodns/record/__init__.py
  9. +1
    -1
      requirements.txt
  10. +23
    -0
      tests/config/dynamic.tests.yaml
  11. +6
    -0
      tests/config/plan-output-filehandle.yaml
  12. +21
    -1
      tests/test_octodns_manager.py
  13. +6
    -5
      tests/test_octodns_provider_transip.py
  14. +9
    -5
      tests/test_octodns_provider_yaml.py

+ 1
- 4
.github/workflows/main.yml View File

@ -1,8 +1,5 @@
name: OctoDNS name: OctoDNS
on:
pull_request:
paths-ignore:
- '**.md'
on: [pull_request]
jobs: jobs:
ci: ci:


+ 3
- 3
README.md View File

@ -93,7 +93,7 @@ The `max_workers` key in the `manager` section of the config enables threading t
In this example, `example.net` is an alias of zone `example.com`, which means they share the same sources and targets. They will therefore have identical records. In this example, `example.net` is an alias of zone `example.com`, which means they share the same sources and targets. They will therefore have identical records.
Now that we have something to tell OctoDNS about our providers & zones we need to tell it about or records. We'll keep it simple for now and just create a single `A` record at the top-level of the domain.
Now that we have something to tell OctoDNS about our providers & zones we need to tell it about our records. We'll keep it simple for now and just create a single `A` record at the top-level of the domain.
`config/example.com.yaml` `config/example.com.yaml`
@ -204,7 +204,7 @@ The above command pulled the existing data out of Route53 and placed the results
| [Rackspace](/octodns/provider/rackspace.py) | | A, AAAA, ALIAS, CNAME, MX, NS, PTR, SPF, TXT | No | | | [Rackspace](/octodns/provider/rackspace.py) | | A, AAAA, ALIAS, CNAME, MX, NS, PTR, SPF, TXT | No | |
| [Route53](/octodns/provider/route53.py) | boto3 | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Both | CNAME health checks don't support a Host header | | [Route53](/octodns/provider/route53.py) | boto3 | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Both | CNAME health checks don't support a Host header |
| [Selectel](/octodns/provider/selectel.py) | | A, AAAA, CNAME, MX, NS, SPF, SRV, TXT | No | | | [Selectel](/octodns/provider/selectel.py) | | A, AAAA, CNAME, MX, NS, SPF, SRV, TXT | No | |
| [Transip](/octodns/provider/transip.py) | transip | A, AAAA, CNAME, MX, SRV, SPF, TXT, SSHFP, CAA | No | |
| [Transip](/octodns/provider/transip.py) | transip | A, AAAA, CNAME, MX, NS, SRV, SPF, TXT, SSHFP, CAA | No | |
| [UltraDns](/octodns/provider/ultra.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | | | [UltraDns](/octodns/provider/ultra.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | |
| [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT | No | read-only | | [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
| [ZoneFileSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only | | [ZoneFileSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
@ -294,7 +294,7 @@ If you have a problem or suggestion, please [open an issue](https://github.com/o
- **GitHub Action:** [OctoDNS-Sync](https://github.com/marketplace/actions/octodns-sync) - **GitHub Action:** [OctoDNS-Sync](https://github.com/marketplace/actions/octodns-sync)
- **Sample Implementations.** See how others are using it - **Sample Implementations.** See how others are using it
- [`hackclub/dns`](https://github.com/hackclub/dns) - [`hackclub/dns`](https://github.com/hackclub/dns)
- [`kubernetes/k8s.io:/dns`](https://github.com/kubernetes/k8s.io/tree/master/dns)
- [`kubernetes/k8s.io:/dns`](https://github.com/kubernetes/k8s.io/tree/main/dns)
- [`g0v-network/domains`](https://github.com/g0v-network/domains) - [`g0v-network/domains`](https://github.com/g0v-network/domains)
- [`jekyll/dns`](https://github.com/jekyll/dns) - [`jekyll/dns`](https://github.com/jekyll/dns)
- **Custom Sources & Providers.** - **Custom Sources & Providers.**


+ 10
- 6
octodns/manager.py View File

@ -9,6 +9,7 @@ from concurrent.futures import ThreadPoolExecutor
from importlib import import_module from importlib import import_module
from os import environ from os import environ
from six import text_type from six import text_type
from sys import stdout
import logging import logging
from .provider.base import BaseProvider from .provider.base import BaseProvider
@ -293,16 +294,19 @@ class Manager(object):
return plans, zone return plans, zone
def sync(self, eligible_zones=[], eligible_sources=[], eligible_targets=[], def sync(self, eligible_zones=[], eligible_sources=[], eligible_targets=[],
dry_run=True, force=False):
self.log.info('sync: eligible_zones=%s, eligible_targets=%s, '
'dry_run=%s, force=%s', eligible_zones, eligible_targets,
dry_run, force)
dry_run=True, force=False, plan_output_fh=stdout):
self.log.info(
'sync: eligible_zones=%s, eligible_targets=%s, dry_run=%s, '
'force=%s, plan_output_fh=%s',
eligible_zones, eligible_targets, dry_run, force,
getattr(plan_output_fh, 'name', plan_output_fh.__class__.__name__))
zones = self.config['zones'].items() zones = self.config['zones'].items()
if eligible_zones: if eligible_zones:
zones = [z for z in zones if z[0] in eligible_zones] zones = [z for z in zones if z[0] in eligible_zones]
aliased_zones = {}
aliased_zones = {}
futures = [] futures = []
for zone_name, config in zones: for zone_name, config in zones:
self.log.info('sync: zone=%s', zone_name) self.log.info('sync: zone=%s', zone_name)
@ -441,7 +445,7 @@ class Manager(object):
plans.sort(key=self._plan_keyer, reverse=True) plans.sort(key=self._plan_keyer, reverse=True)
for output in self.plan_outputs.values(): for output in self.plan_outputs.values():
output.run(plans=plans, log=self.log)
output.run(plans=plans, log=self.log, fh=plan_output_fh)
if not force: if not force:
self.log.debug('sync: checking safety') self.log.debug('sync: checking safety')


+ 3
- 3
octodns/provider/dnsimple.py View File

@ -51,8 +51,8 @@ class DnsimpleClient(object):
resp.raise_for_status() resp.raise_for_status()
return resp return resp
def domain(self, name):
path = '/domains/{}'.format(name)
def zone(self, name):
path = '/zones/{}'.format(name)
return self._request('GET', path).json() return self._request('GET', path).json()
def domain_create(self, name): def domain_create(self, name):
@ -442,7 +442,7 @@ class DnsimpleProvider(BaseProvider):
domain_name = desired.name[:-1] domain_name = desired.name[:-1]
try: try:
self._client.domain(domain_name)
self._client.zone(domain_name)
except DnsimpleClientNotFound: except DnsimpleClientNotFound:
self.log.debug('_apply: no matching zone, creating domain') self.log.debug('_apply: no matching zone, creating domain')
self._client.domain_create(domain_name) self._client.domain_create(domain_name)


+ 1
- 1
octodns/provider/easydns.py View File

@ -59,7 +59,7 @@ class EasyDNSClient(object):
self.base_path = self.SANDBOX if sandbox else self.LIVE self.base_path = self.SANDBOX if sandbox else self.LIVE
sess = Session() sess = Session()
sess.headers.update({'Authorization': 'Basic {}' sess.headers.update({'Authorization': 'Basic {}'
.format(self.auth_key)})
.format(self.auth_key.decode('utf-8'))})
sess.headers.update({'accept': 'application/json'}) sess.headers.update({'accept': 'application/json'})
self._sess = sess self._sess = sess


+ 1
- 1
octodns/provider/plan.py View File

@ -50,7 +50,7 @@ class Plan(object):
except AttributeError: except AttributeError:
existing_n = 0 existing_n = 0
self.log.debug('__init__: Creates=%d, Updates=%d, Deletes=%d'
self.log.debug('__init__: Creates=%d, Updates=%d, Deletes=%d '
'Existing=%d', 'Existing=%d',
self.change_counts['Create'], self.change_counts['Create'],
self.change_counts['Update'], self.change_counts['Update'],


+ 2
- 2
octodns/provider/transip.py View File

@ -49,8 +49,8 @@ class TransipProvider(BaseProvider):
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False SUPPORTS_DYNAMIC = False
SUPPORTS = set(
('A', 'AAAA', 'CNAME', 'MX', 'SRV', 'SPF', 'TXT', 'SSHFP', 'CAA'))
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'SPF', 'TXT',
'SSHFP', 'CAA'))
# unsupported by OctoDNS: 'TLSA' # unsupported by OctoDNS: 'TLSA'
MIN_TTL = 120 MIN_TTL = 120
TIMEOUT = 15 TIMEOUT = 15


+ 8
- 4
octodns/record/__init__.py View File

@ -531,6 +531,7 @@ class _DynamicMixin(object):
pools_exist = set() pools_exist = set()
pools_seen = set() pools_seen = set()
pools_seen_as_fallback = set()
if not isinstance(pools, dict): if not isinstance(pools, dict):
reasons.append('pools must be a dict') reasons.append('pools must be a dict')
elif not pools: elif not pools:
@ -573,9 +574,12 @@ class _DynamicMixin(object):
'value {}'.format(_id, value_num)) 'value {}'.format(_id, value_num))
fallback = pool.get('fallback', None) fallback = pool.get('fallback', None)
if fallback is not None and fallback not in pools:
reasons.append('undefined fallback "{}" for pool "{}"'
.format(fallback, _id))
if fallback is not None:
if fallback in pools:
pools_seen_as_fallback.add(fallback)
else:
reasons.append('undefined fallback "{}" for pool "{}"'
.format(fallback, _id))
# Check for loops # Check for loops
fallback = pools[_id].get('fallback', None) fallback = pools[_id].get('fallback', None)
@ -644,7 +648,7 @@ class _DynamicMixin(object):
reasons.extend(GeoCodes.validate(geo, 'rule {} ' reasons.extend(GeoCodes.validate(geo, 'rule {} '
.format(rule_num))) .format(rule_num)))
unused = pools_exist - pools_seen
unused = pools_exist - pools_seen - pools_seen_as_fallback
if unused: if unused:
unused = '", "'.join(sorted(unused)) unused = '", "'.join(sorted(unused))
reasons.append('unused pools: "{}"'.format(unused)) reasons.append('unused pools: "{}"'.format(unused))


+ 1
- 1
requirements.txt View File

@ -1,4 +1,4 @@
PyYaml==5.3.1
PyYaml==5.4
azure-common==1.1.25 azure-common==1.1.25
azure-mgmt-dns==3.0.0 azure-mgmt-dns==3.0.0
boto3==1.15.9 boto3==1.15.9


+ 23
- 0
tests/config/dynamic.tests.yaml View File

@ -109,6 +109,29 @@ cname:
- pool: iad - pool: iad
type: CNAME type: CNAME
value: target.unit.tests. value: target.unit.tests.
pool-only-in-fallback:
dynamic:
pools:
one:
fallback: two
values:
- value: 1.1.1.1
three:
values:
- value: 3.3.3.3
two:
values:
- value: 2.2.2.2
rules:
- geos:
- NA-US
pool: one
- geos:
- AS-SG
pool: three
ttl: 300
type: A
values: [4.4.4.4]
real-ish-a: real-ish-a:
dynamic: dynamic:
pools: pools:


+ 6
- 0
tests/config/plan-output-filehandle.yaml View File

@ -0,0 +1,6 @@
manager:
plan_outputs:
"doesntexist":
class: octodns.provider.plan.DoesntExist
providers: {}
zones: {}

+ 21
- 1
tests/test_octodns_manager.py View File

@ -8,7 +8,6 @@ from __future__ import absolute_import, division, print_function, \
from os import environ from os import environ
from os.path import dirname, join from os.path import dirname, join
from six import text_type from six import text_type
from unittest import TestCase
from octodns.record import Record from octodns.record import Record
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \ from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \
@ -16,6 +15,9 @@ from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \
from octodns.yaml import safe_load from octodns.yaml import safe_load
from octodns.zone import Zone from octodns.zone import Zone
from mock import MagicMock, patch
from unittest import TestCase
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \ from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \
SimpleProvider, TemporaryDirectory SimpleProvider, TemporaryDirectory
@ -371,6 +373,24 @@ class TestManager(TestCase):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
manager._populate_and_plan('unit.tests.', [NoZone()], []) manager._populate_and_plan('unit.tests.', [NoZone()], [])
@patch('octodns.manager.Manager._get_named_class')
def test_sync_passes_file_handle(self, mock):
plan_output_mock = MagicMock()
plan_output_class_mock = MagicMock()
plan_output_class_mock.return_value = plan_output_mock
mock.return_value = plan_output_class_mock
fh_mock = MagicMock()
Manager(get_config_filename('plan-output-filehandle.yaml')
).sync(plan_output_fh=fh_mock)
# Since we only care about the fh kwarg, and different _PlanOutputs are
# are free to require arbitrary kwargs anyway, we concern ourselves
# with checking the value of fh only.
plan_output_mock.run.assert_called()
_, kwargs = plan_output_mock.run.call_args
self.assertEqual(fh_mock, kwargs.get('fh'))
class TestMainThreadExecutor(TestCase): class TestMainThreadExecutor(TestCase):


+ 6
- 5
tests/test_octodns_provider_transip.py View File

@ -56,10 +56,11 @@ class MockDomainService(DomainService):
_dns_entries.extend(entries_for(name, record)) _dns_entries.extend(entries_for(name, record))
# NS is not supported as a DNS Entry,
# so it should cover the if statement
# Add a non-supported type
# so it triggers the "is supported" (transip.py:115) check and
# give 100% code coverage
_dns_entries.append( _dns_entries.append(
DnsEntry('@', '3600', 'NS', 'ns01.transip.nl.'))
DnsEntry('@', '3600', 'BOGUS', 'ns01.transip.nl.'))
self.mockupEntries = _dns_entries self.mockupEntries = _dns_entries
@ -222,7 +223,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
provider._client = MockDomainService('unittest', self.bogus_key) provider._client = MockDomainService('unittest', self.bogus_key)
plan = provider.plan(_expected) plan = provider.plan(_expected)
self.assertEqual(14, plan.change_counts['Create'])
self.assertEqual(15, plan.change_counts['Create'])
self.assertEqual(0, plan.change_counts['Update']) self.assertEqual(0, plan.change_counts['Update'])
self.assertEqual(0, plan.change_counts['Delete']) self.assertEqual(0, plan.change_counts['Delete'])
@ -235,7 +236,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
provider = TransipProvider('test', 'unittest', self.bogus_key) provider = TransipProvider('test', 'unittest', self.bogus_key)
provider._client = MockDomainService('unittest', self.bogus_key) provider._client = MockDomainService('unittest', self.bogus_key)
plan = provider.plan(_expected) plan = provider.plan(_expected)
self.assertEqual(14, len(plan.changes))
self.assertEqual(15, len(plan.changes))
changes = provider.apply(plan) changes = provider.apply(plan)
self.assertEqual(changes, len(plan.changes)) self.assertEqual(changes, len(plan.changes))


+ 9
- 5
tests/test_octodns_provider_yaml.py View File

@ -38,7 +38,7 @@ class TestYamlProvider(TestCase):
self.assertEquals(22, len(zone.records)) self.assertEquals(22, len(zone.records))
source.populate(dynamic_zone) source.populate(dynamic_zone)
self.assertEquals(5, len(dynamic_zone.records))
self.assertEquals(6, len(dynamic_zone.records))
# Assumption here is that a clean round-trip means that everything # Assumption here is that a clean round-trip means that everything
# worked as expected, data that went in came back out and could be # worked as expected, data that went in came back out and could be
@ -68,11 +68,11 @@ class TestYamlProvider(TestCase):
# Dynamic plan # Dynamic plan
plan = target.plan(dynamic_zone) plan = target.plan(dynamic_zone)
self.assertEquals(5, len([c for c in plan.changes
self.assertEquals(6, len([c for c in plan.changes
if isinstance(c, Create)])) if isinstance(c, Create)]))
self.assertFalse(isfile(dynamic_yaml_file)) self.assertFalse(isfile(dynamic_yaml_file))
# Apply it # Apply it
self.assertEquals(5, target.apply(plan))
self.assertEquals(6, target.apply(plan))
self.assertTrue(isfile(dynamic_yaml_file)) self.assertTrue(isfile(dynamic_yaml_file))
# There should be no changes after the round trip # There should be no changes after the round trip
@ -148,6 +148,10 @@ class TestYamlProvider(TestCase):
self.assertTrue('value' in dyna) self.assertTrue('value' in dyna)
# self.assertTrue('dynamic' in dyna) # self.assertTrue('dynamic' in dyna)
dyna = data.pop('pool-only-in-fallback')
self.assertTrue('value' in dyna)
# self.assertTrue('dynamic' in dyna)
# make sure nothing is left # make sure nothing is left
self.assertEquals([], list(data.keys())) self.assertEquals([], list(data.keys()))
@ -397,7 +401,7 @@ class TestOverridingYamlProvider(TestCase):
# Load the base, should see the 5 records # Load the base, should see the 5 records
base.populate(zone) base.populate(zone)
got = {r.name: r for r in zone.records} got = {r.name: r for r in zone.records}
self.assertEquals(5, len(got))
self.assertEquals(6, len(got))
# We get the "dynamic" A from the base config # We get the "dynamic" A from the base config
self.assertTrue('dynamic' in got['a'].data) self.assertTrue('dynamic' in got['a'].data)
# No added # No added
@ -406,7 +410,7 @@ class TestOverridingYamlProvider(TestCase):
# Load the overrides, should replace one and add 1 # Load the overrides, should replace one and add 1
override.populate(zone) override.populate(zone)
got = {r.name: r for r in zone.records} got = {r.name: r for r in zone.records}
self.assertEquals(6, len(got))
self.assertEquals(7, len(got))
# 'a' was replaced with a generic record # 'a' was replaced with a generic record
self.assertEquals({ self.assertEquals({
'ttl': 3600, 'ttl': 3600,


Loading…
Cancel
Save