mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
#
|
|
#
|
|
#
|
|
|
|
from __future__ import absolute_import, division, print_function, \
|
|
unicode_literals
|
|
|
|
from ..source.base import BaseSource
|
|
from ..zone import Zone
|
|
from logging import getLogger
|
|
|
|
|
|
class UnsafePlan(Exception):
|
|
pass
|
|
|
|
|
|
class Plan(object):
|
|
log = getLogger('Plan')
|
|
|
|
MAX_SAFE_UPDATE_PCENT = .3
|
|
MAX_SAFE_DELETE_PCENT = .3
|
|
MIN_EXISTING_RECORDS = 10
|
|
|
|
def __init__(self, existing, desired, changes):
|
|
self.existing = existing
|
|
self.desired = desired
|
|
self.changes = changes
|
|
|
|
change_counts = {
|
|
'Create': 0,
|
|
'Delete': 0,
|
|
'Update': 0
|
|
}
|
|
for change in changes:
|
|
change_counts[change.__class__.__name__] += 1
|
|
self.change_counts = change_counts
|
|
|
|
try:
|
|
existing_n = len(self.existing.records)
|
|
except AttributeError:
|
|
existing_n = 0
|
|
|
|
self.log.debug('__init__: Creates=%d, Updates=%d, Deletes=%d'
|
|
'Existing=%d',
|
|
self.change_counts['Create'],
|
|
self.change_counts['Update'],
|
|
self.change_counts['Delete'], existing_n)
|
|
|
|
def raise_if_unsafe(self):
|
|
# TODO: what is safe really?
|
|
if self.existing and \
|
|
len(self.existing.records) >= self.MIN_EXISTING_RECORDS:
|
|
|
|
existing_record_count = len(self.existing.records)
|
|
update_pcent = self.change_counts['Update'] / existing_record_count
|
|
delete_pcent = self.change_counts['Delete'] / existing_record_count
|
|
|
|
if update_pcent > self.MAX_SAFE_UPDATE_PCENT:
|
|
raise UnsafePlan('Too many updates, {} is over {} percent'
|
|
'({}/{})'.format(
|
|
update_pcent,
|
|
self.MAX_SAFE_UPDATE_PCENT * 100,
|
|
self.change_counts['Update'],
|
|
existing_record_count))
|
|
if delete_pcent > self.MAX_SAFE_DELETE_PCENT:
|
|
raise UnsafePlan('Too many deletes, {} is over {} percent'
|
|
'({}/{})'.format(
|
|
delete_pcent,
|
|
self.MAX_SAFE_DELETE_PCENT * 100,
|
|
self.change_counts['Delete'],
|
|
existing_record_count))
|
|
|
|
def __repr__(self):
|
|
return 'Creates={}, Updates={}, Deletes={}, Existing Records={}' \
|
|
.format(self.change_counts['Create'], self.change_counts['Update'],
|
|
self.change_counts['Delete'],
|
|
len(self.existing.records))
|
|
|
|
|
|
class BaseProvider(BaseSource):
|
|
|
|
def __init__(self, id, apply_disabled=False):
|
|
super(BaseProvider, self).__init__(id)
|
|
self.log.debug('__init__: id=%s, apply_disabled=%s', id,
|
|
apply_disabled)
|
|
self.apply_disabled = apply_disabled
|
|
|
|
def _include_change(self, change):
|
|
'''
|
|
An opportunity for providers to filter out false positives due to
|
|
pecularities in their implementation. E.g. minimum TTLs.
|
|
'''
|
|
return True
|
|
|
|
def _extra_changes(self, existing, changes):
|
|
'''
|
|
An opportunity for providers to add extra changes to the plan that are
|
|
necessary to update ancilary record data or configure the zone. E.g.
|
|
base NS records.
|
|
'''
|
|
return []
|
|
|
|
def plan(self, desired):
|
|
self.log.info('plan: desired=%s', desired.name)
|
|
|
|
existing = Zone(desired.name, desired.sub_zones)
|
|
self.populate(existing, target=True, lenient=True)
|
|
|
|
# compute the changes at the zone/record level
|
|
changes = existing.changes(desired, self)
|
|
|
|
# allow the provider to filter out false positives
|
|
before = len(changes)
|
|
changes = filter(self._include_change, changes)
|
|
after = len(changes)
|
|
if before != after:
|
|
self.log.info('plan: filtered out %s changes', before - after)
|
|
|
|
# allow the provider to add extra changes it needs
|
|
extra = self._extra_changes(existing, changes)
|
|
if extra:
|
|
self.log.info('plan: extra changes\n %s', '\n '
|
|
.join([str(c) for c in extra]))
|
|
changes += extra
|
|
|
|
if changes:
|
|
plan = Plan(existing, desired, changes)
|
|
self.log.info('plan: %s', plan)
|
|
return plan
|
|
self.log.info('plan: No changes')
|
|
return None
|
|
|
|
def apply(self, plan):
|
|
'''
|
|
Submits actual planned changes to the provider. Returns the number of
|
|
changes made
|
|
'''
|
|
if self.apply_disabled:
|
|
self.log.info('apply: disabled')
|
|
return 0
|
|
|
|
self.log.info('apply: making changes')
|
|
self._apply(plan)
|
|
return len(plan.changes)
|
|
|
|
def _apply(self, plan):
|
|
raise NotImplementedError('Abstract base class, _apply method '
|
|
'missing')
|