mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Add zone specific threshold config
This commit is contained in:
@@ -126,7 +126,7 @@ If you'd like to enable lenience for a whole zone you can do so with the followi
|
|||||||
#### Restrict Record manipulations
|
#### Restrict Record manipulations
|
||||||
|
|
||||||
octoDNS currently provides the ability to limit the number of updates/deletes on
|
octoDNS currently provides the ability to limit the number of updates/deletes on
|
||||||
DNS records by configuring a percentage of allowed operations as a threshold.
|
DNS records by configuring a percentage of allowed operations as a provider threshold.
|
||||||
If left unconfigured, suitable defaults take over instead. In the below example,
|
If left unconfigured, suitable defaults take over instead. In the below example,
|
||||||
the Dyn provider is configured with limits of 40% on both update and
|
the Dyn provider is configured with limits of 40% on both update and
|
||||||
delete operations over all the records present.
|
delete operations over all the records present.
|
||||||
@@ -138,6 +138,17 @@ dyn:
|
|||||||
delete_pcent_threshold: 0.4
|
delete_pcent_threshold: 0.4
|
||||||
````
|
````
|
||||||
|
|
||||||
|
Additionally, thresholds can be configured at the zone level. Zone thresholds
|
||||||
|
take precedence over any provider default or explicit configuration. Zone
|
||||||
|
thresholds do not have a default.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
zones:
|
||||||
|
example.com.:
|
||||||
|
update_pcent_threshold: 0.2
|
||||||
|
delete_pcent_threshold: 0.1
|
||||||
|
```
|
||||||
|
|
||||||
## Provider specific record types
|
## Provider specific record types
|
||||||
|
|
||||||
### Creating and registering
|
### Creating and registering
|
||||||
|
@@ -1052,6 +1052,13 @@ class Manager(object):
|
|||||||
zone = self.config['zones'].get(zone_name)
|
zone = self.config['zones'].get(zone_name)
|
||||||
if zone is not None:
|
if zone is not None:
|
||||||
sub_zones = self.configured_sub_zones(zone_name)
|
sub_zones = self.configured_sub_zones(zone_name)
|
||||||
return Zone(idna_encode(zone_name), sub_zones)
|
update_pcent_threshold = zone.get("update_pcent_threshold", None)
|
||||||
|
delete_pcent_threshold = zone.get("delete_pcent_threshold", None)
|
||||||
|
return Zone(
|
||||||
|
idna_encode(zone_name),
|
||||||
|
sub_zones,
|
||||||
|
update_pcent_threshold,
|
||||||
|
delete_pcent_threshold,
|
||||||
|
)
|
||||||
|
|
||||||
raise ManagerException(f'Unknown zone name {idna_decode(zone_name)}')
|
raise ManagerException(f'Unknown zone name {idna_decode(zone_name)}')
|
||||||
|
@@ -60,6 +60,12 @@ class Plan(object):
|
|||||||
self.update_pcent_threshold = update_pcent_threshold
|
self.update_pcent_threshold = update_pcent_threshold
|
||||||
self.delete_pcent_threshold = delete_pcent_threshold
|
self.delete_pcent_threshold = delete_pcent_threshold
|
||||||
|
|
||||||
|
# Zone thresholds take precedence over provider
|
||||||
|
if existing and existing.update_pcent_threshold is not None:
|
||||||
|
self.update_pcent_threshold = existing.update_pcent_threshold
|
||||||
|
if existing and existing.delete_pcent_threshold is not None:
|
||||||
|
self.delete_pcent_threshold = existing.delete_pcent_threshold
|
||||||
|
|
||||||
change_counts = {'Create': 0, 'Delete': 0, 'Update': 0}
|
change_counts = {'Create': 0, 'Delete': 0, 'Update': 0}
|
||||||
for change in changes:
|
for change in changes:
|
||||||
change_counts[change.__class__.__name__] += 1
|
change_counts[change.__class__.__name__] += 1
|
||||||
|
@@ -56,7 +56,13 @@ class InvalidNodeException(Exception):
|
|||||||
class Zone(object):
|
class Zone(object):
|
||||||
log = getLogger('Zone')
|
log = getLogger('Zone')
|
||||||
|
|
||||||
def __init__(self, name, sub_zones):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
sub_zones,
|
||||||
|
update_pcent_threshold=None,
|
||||||
|
delete_pcent_threshold=None,
|
||||||
|
):
|
||||||
if not name[-1] == '.':
|
if not name[-1] == '.':
|
||||||
raise Exception(f'Invalid zone name {name}, missing ending dot')
|
raise Exception(f'Invalid zone name {name}, missing ending dot')
|
||||||
elif ' ' in name or '\t' in name:
|
elif ' ' in name or '\t' in name:
|
||||||
@@ -78,6 +84,9 @@ class Zone(object):
|
|||||||
self._utf8_name_re = re.compile(fr'\.?{idna_decode(name)}?$')
|
self._utf8_name_re = re.compile(fr'\.?{idna_decode(name)}?$')
|
||||||
self._idna_name_re = re.compile(fr'\.?{self.name}?$')
|
self._idna_name_re = re.compile(fr'\.?{self.name}?$')
|
||||||
|
|
||||||
|
self.update_pcent_threshold = update_pcent_threshold
|
||||||
|
self.delete_pcent_threshold = delete_pcent_threshold
|
||||||
|
|
||||||
# Copy-on-write semantics support, when `not None` this property will
|
# Copy-on-write semantics support, when `not None` this property will
|
||||||
# point to a location with records for this `Zone`. Once `hydrated`
|
# point to a location with records for this `Zone`. Once `hydrated`
|
||||||
# this property will be set to None
|
# this property will be set to None
|
||||||
@@ -352,7 +361,12 @@ class Zone(object):
|
|||||||
copying the records when required. The actual record copy will not be
|
copying the records when required. The actual record copy will not be
|
||||||
"deep" meaning that records should not be modified directly.
|
"deep" meaning that records should not be modified directly.
|
||||||
'''
|
'''
|
||||||
copy = Zone(self.name, self.sub_zones)
|
copy = Zone(
|
||||||
|
self.name,
|
||||||
|
self.sub_zones,
|
||||||
|
self.update_pcent_threshold,
|
||||||
|
self.delete_pcent_threshold,
|
||||||
|
)
|
||||||
copy._origin = self
|
copy._origin = self
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
|
28
tests/config/zone-threshold.yaml
Normal file
28
tests/config/zone-threshold.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
manager:
|
||||||
|
max_workers: 2
|
||||||
|
providers:
|
||||||
|
in:
|
||||||
|
class: octodns.provider.yaml.YamlProvider
|
||||||
|
directory: tests/config
|
||||||
|
strict_supports: False
|
||||||
|
dump:
|
||||||
|
class: octodns.provider.yaml.YamlProvider
|
||||||
|
directory: env/YAML_TMP_DIR
|
||||||
|
supports_root_ns: False
|
||||||
|
strict_supports: False
|
||||||
|
zones:
|
||||||
|
unit.tests.:
|
||||||
|
update_pcent_threshold: 0.2
|
||||||
|
delete_pcent_threshold: 0.1
|
||||||
|
sources:
|
||||||
|
- in
|
||||||
|
targets:
|
||||||
|
- dump
|
||||||
|
|
||||||
|
subzone.unit.tests.:
|
||||||
|
update_pcent_threshold: 0.02
|
||||||
|
delete_pcent_threshold: 0.01
|
||||||
|
sources:
|
||||||
|
- in
|
||||||
|
targets:
|
||||||
|
- dump
|
@@ -1294,6 +1294,23 @@ class TestManager(TestCase):
|
|||||||
requires_dummy.fetch(':hello', None),
|
requires_dummy.fetch(':hello', None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_zone_threshold(self):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
environ['YAML_TMP_DIR'] = tmpdir.dirname
|
||||||
|
|
||||||
|
manager = Manager(get_config_filename('zone-threshold.yaml'))
|
||||||
|
|
||||||
|
zone = manager.get_zone('unit.tests.')
|
||||||
|
|
||||||
|
self.assertEqual(0.2, zone.update_pcent_threshold)
|
||||||
|
self.assertEqual(0.1, zone.delete_pcent_threshold)
|
||||||
|
|
||||||
|
# subzone has different threshold
|
||||||
|
subzone = manager.get_zone('subzone.unit.tests.')
|
||||||
|
|
||||||
|
self.assertEqual(0.02, subzone.update_pcent_threshold)
|
||||||
|
self.assertEqual(0.01, subzone.delete_pcent_threshold)
|
||||||
|
|
||||||
|
|
||||||
class TestMainThreadExecutor(TestCase):
|
class TestMainThreadExecutor(TestCase):
|
||||||
def test_success(self):
|
def test_success(self):
|
||||||
|
@@ -165,8 +165,27 @@ class TestPlanSafety(TestCase):
|
|||||||
record_4 = Record.new(
|
record_4 = Record.new(
|
||||||
existing, '4', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
|
existing, '4', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
|
||||||
)
|
)
|
||||||
|
record_5 = Record.new(
|
||||||
|
existing, '5', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
|
||||||
|
)
|
||||||
|
record_6 = Record.new(
|
||||||
|
existing, '6', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
|
||||||
|
)
|
||||||
|
record_7 = Record.new(
|
||||||
|
existing, '7', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
|
||||||
|
)
|
||||||
|
record_8 = Record.new(
|
||||||
|
existing, '8', data={'type': 'A', 'ttl': 42, 'value': '1.2.3.4'}
|
||||||
|
)
|
||||||
|
|
||||||
def test_too_many_updates(self):
|
# manager loads the zone's config, so existing also holds providers & other config
|
||||||
|
update_threshold = 0.2
|
||||||
|
delete_threshold = 0.1
|
||||||
|
existing_with_thresholds = Zone(
|
||||||
|
'cautious.tests.', [], update_threshold, delete_threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_too_many_provider_updates(self):
|
||||||
existing = self.existing.copy()
|
existing = self.existing.copy()
|
||||||
changes = []
|
changes = []
|
||||||
|
|
||||||
@@ -206,7 +225,46 @@ class TestPlanSafety(TestCase):
|
|||||||
plan = HelperPlan(existing, None, changes, True, min_existing=10)
|
plan = HelperPlan(existing, None, changes, True, min_existing=10)
|
||||||
plan.raise_if_unsafe()
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
def test_too_many_deletes(self):
|
def test_too_many_zone_updates(self):
|
||||||
|
existing = self.existing_with_thresholds.copy()
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
# No records, no changes, we're good
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
# Setup quite a few records, so that
|
||||||
|
# zone can be more cautious than provider's default
|
||||||
|
existing.add_record(self.record_1)
|
||||||
|
existing.add_record(self.record_2)
|
||||||
|
existing.add_record(self.record_3)
|
||||||
|
existing.add_record(self.record_4)
|
||||||
|
existing.add_record(self.record_5)
|
||||||
|
existing.add_record(self.record_6)
|
||||||
|
existing.add_record(self.record_7)
|
||||||
|
existing.add_record(self.record_8)
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
# One update is ok: 12.5% < 20%
|
||||||
|
changes.append(Update(self.record_1, self.record_1))
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
# Still ok, zone takes precedence
|
||||||
|
plan = HelperPlan(
|
||||||
|
existing, None, changes, True, update_pcent_threshold=0
|
||||||
|
)
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
# Two exceeds threshold, 25% > 20%
|
||||||
|
changes.append(Update(self.record_2, self.record_2))
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
with self.assertRaises(TooMuchChange) as ctx:
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
self.assertTrue('Too many updates', str(ctx.exception))
|
||||||
|
|
||||||
|
def test_too_many_provider_deletes(self):
|
||||||
existing = self.existing.copy()
|
existing = self.existing.copy()
|
||||||
changes = []
|
changes = []
|
||||||
|
|
||||||
@@ -246,6 +304,34 @@ class TestPlanSafety(TestCase):
|
|||||||
plan = HelperPlan(existing, None, changes, True, min_existing=10)
|
plan = HelperPlan(existing, None, changes, True, min_existing=10)
|
||||||
plan.raise_if_unsafe()
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
def test_too_many_zone_deletes(self):
|
||||||
|
existing = self.existing_with_thresholds.copy()
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
# No records, no changes, we're good
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
# Setup quite a few records, so that
|
||||||
|
# zone can be more cautious than provider's default
|
||||||
|
existing.add_record(self.record_1)
|
||||||
|
existing.add_record(self.record_2)
|
||||||
|
existing.add_record(self.record_3)
|
||||||
|
existing.add_record(self.record_4)
|
||||||
|
existing.add_record(self.record_5)
|
||||||
|
existing.add_record(self.record_6)
|
||||||
|
existing.add_record(self.record_7)
|
||||||
|
existing.add_record(self.record_8)
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
|
||||||
|
# One delete exceeds Zone threshold
|
||||||
|
changes.append(Delete(self.record_1))
|
||||||
|
plan = HelperPlan(existing, None, changes, True)
|
||||||
|
with self.assertRaises(TooMuchChange) as ctx:
|
||||||
|
plan.raise_if_unsafe()
|
||||||
|
self.assertTrue('Too many deletes', str(ctx.exception))
|
||||||
|
|
||||||
def test_root_ns_change(self):
|
def test_root_ns_change(self):
|
||||||
existing = self.existing.copy()
|
existing = self.existing.copy()
|
||||||
changes = []
|
changes = []
|
||||||
|
Reference in New Issue
Block a user