mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Many providers make their modifications in the order that changes comes. In python3 this causes things to be inconsistently ordered. That mostly works, but could result in hidenbugs (e.g. Route53Provider's batching could be completely different based on the order it sees changes.) Sorting changes consistently is a good thing and it shouldn't hurt situations where providers are already doing their own ordering. All-in-all more consistent is better and we have to be explicit with python 3.
292 lines
10 KiB
Python
292 lines
10 KiB
Python
#
|
|
#
|
|
#
|
|
|
|
from __future__ import absolute_import, division, print_function, \
|
|
unicode_literals
|
|
|
|
from logging import DEBUG, ERROR, INFO, WARN, getLogger
|
|
from sys import stdout
|
|
|
|
from six import text_type
|
|
from ..compat import StringIO
|
|
|
|
|
|
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, exists,
|
|
update_pcent_threshold=MAX_SAFE_UPDATE_PCENT,
|
|
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT):
|
|
self.existing = existing
|
|
self.desired = desired
|
|
# Sort changes to ensure we always have a consistent ordering for
|
|
# things that make assumptions about that. Many providers will do their
|
|
# own ordering to ensure things happen in a way that makes sense to
|
|
# them and/or is as safe as possible.
|
|
self.changes = sorted(changes)
|
|
self.exists = exists
|
|
self.update_pcent_threshold = update_pcent_threshold
|
|
self.delete_pcent_threshold = delete_pcent_threshold
|
|
|
|
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.update_pcent_threshold:
|
|
raise UnsafePlan('Too many updates, {:.2f} is over {:.2f} %'
|
|
'({}/{})'.format(
|
|
update_pcent * 100,
|
|
self.update_pcent_threshold * 100,
|
|
self.change_counts['Update'],
|
|
existing_record_count))
|
|
if delete_pcent > self.delete_pcent_threshold:
|
|
raise UnsafePlan('Too many deletes, {:.2f} is over {:.2f} %'
|
|
'({}/{})'.format(
|
|
delete_pcent * 100,
|
|
self.delete_pcent_threshold * 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 _PlanOutput(object):
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
class PlanLogger(_PlanOutput):
|
|
|
|
def __init__(self, name, level='info'):
|
|
super(PlanLogger, self).__init__(name)
|
|
try:
|
|
self.level = {
|
|
'debug': DEBUG,
|
|
'info': INFO,
|
|
'warn': WARN,
|
|
'warning': WARN,
|
|
'error': ERROR
|
|
}[level.lower()]
|
|
except (AttributeError, KeyError):
|
|
raise Exception('Unsupported level: {}'.format(level))
|
|
|
|
def run(self, log, plans, *args, **kwargs):
|
|
hr = '*************************************************************' \
|
|
'*******************\n'
|
|
buf = StringIO()
|
|
buf.write('\n')
|
|
if plans:
|
|
current_zone = None
|
|
for target, plan in plans:
|
|
if plan.desired.name != current_zone:
|
|
current_zone = plan.desired.name
|
|
buf.write(hr)
|
|
buf.write('* ')
|
|
buf.write(current_zone)
|
|
buf.write('\n')
|
|
buf.write(hr)
|
|
|
|
buf.write('* ')
|
|
buf.write(target.id)
|
|
buf.write(' (')
|
|
buf.write(text_type(target))
|
|
buf.write(')\n* ')
|
|
|
|
if plan.exists is False:
|
|
buf.write('Create ')
|
|
buf.write(str(plan.desired))
|
|
buf.write('\n* ')
|
|
|
|
for change in plan.changes:
|
|
buf.write(change.__repr__(leader='* '))
|
|
buf.write('\n* ')
|
|
|
|
buf.write('Summary: ')
|
|
buf.write(text_type(plan))
|
|
buf.write('\n')
|
|
else:
|
|
buf.write(hr)
|
|
buf.write('No changes were planned\n')
|
|
buf.write(hr)
|
|
buf.write('\n')
|
|
log.log(self.level, buf.getvalue())
|
|
|
|
|
|
def _value_stringifier(record, sep):
|
|
try:
|
|
values = [text_type(v) for v in record.values]
|
|
except AttributeError:
|
|
values = [record.value]
|
|
for code, gv in sorted(getattr(record, 'geo', {}).items()):
|
|
vs = ', '.join([text_type(v) for v in gv.values])
|
|
values.append('{}: {}'.format(code, vs))
|
|
return sep.join(values)
|
|
|
|
|
|
class PlanMarkdown(_PlanOutput):
|
|
|
|
def run(self, plans, fh=stdout, *args, **kwargs):
|
|
if plans:
|
|
current_zone = None
|
|
for target, plan in plans:
|
|
if plan.desired.name != current_zone:
|
|
current_zone = plan.desired.name
|
|
fh.write('## ')
|
|
fh.write(current_zone)
|
|
fh.write('\n\n')
|
|
|
|
fh.write('### ')
|
|
fh.write(target.id)
|
|
fh.write('\n\n')
|
|
|
|
fh.write('| Operation | Name | Type | TTL | Value | Source |\n'
|
|
'|--|--|--|--|--|--|\n')
|
|
|
|
if plan.exists is False:
|
|
fh.write('| Create | ')
|
|
fh.write(str(plan.desired))
|
|
fh.write(' | | | | |\n')
|
|
|
|
for change in plan.changes:
|
|
existing = change.existing
|
|
new = change.new
|
|
record = change.record
|
|
fh.write('| ')
|
|
fh.write(change.__class__.__name__)
|
|
fh.write(' | ')
|
|
fh.write(record.name)
|
|
fh.write(' | ')
|
|
fh.write(record._type)
|
|
fh.write(' | ')
|
|
# TTL
|
|
if existing:
|
|
fh.write(text_type(existing.ttl))
|
|
fh.write(' | ')
|
|
fh.write(_value_stringifier(existing, '; '))
|
|
fh.write(' | |\n')
|
|
if new:
|
|
fh.write('| | | | ')
|
|
|
|
if new:
|
|
fh.write(text_type(new.ttl))
|
|
fh.write(' | ')
|
|
fh.write(_value_stringifier(new, '; '))
|
|
fh.write(' | ')
|
|
if new.source:
|
|
fh.write(new.source.id)
|
|
fh.write(' |\n')
|
|
|
|
fh.write('\nSummary: ')
|
|
fh.write(text_type(plan))
|
|
fh.write('\n\n')
|
|
else:
|
|
fh.write('## No changes were planned\n')
|
|
|
|
|
|
class PlanHtml(_PlanOutput):
|
|
|
|
def run(self, plans, fh=stdout, *args, **kwargs):
|
|
if plans:
|
|
current_zone = None
|
|
for target, plan in plans:
|
|
if plan.desired.name != current_zone:
|
|
current_zone = plan.desired.name
|
|
fh.write('<h2>')
|
|
fh.write(current_zone)
|
|
fh.write('</h2>\n')
|
|
|
|
fh.write('<h3>')
|
|
fh.write(target.id)
|
|
fh.write('''</h3>
|
|
<table>
|
|
<tr>
|
|
<th>Operation</th>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>TTL</th>
|
|
<th>Value</th>
|
|
<th>Source</th>
|
|
</tr>
|
|
''')
|
|
|
|
if plan.exists is False:
|
|
fh.write(' <tr>\n <td>Create</td>\n <td colspan=5>')
|
|
fh.write(str(plan.desired))
|
|
fh.write('</td>\n </tr>\n')
|
|
|
|
for change in plan.changes:
|
|
existing = change.existing
|
|
new = change.new
|
|
record = change.record
|
|
fh.write(' <tr>\n <td>')
|
|
fh.write(change.__class__.__name__)
|
|
fh.write('</td>\n <td>')
|
|
fh.write(record.name)
|
|
fh.write('</td>\n <td>')
|
|
fh.write(record._type)
|
|
fh.write('</td>\n')
|
|
# TTL
|
|
if existing:
|
|
fh.write(' <td>')
|
|
fh.write(text_type(existing.ttl))
|
|
fh.write('</td>\n <td>')
|
|
fh.write(_value_stringifier(existing, '<br/>'))
|
|
fh.write('</td>\n <td></td>\n </tr>\n')
|
|
if new:
|
|
fh.write(' <tr>\n <td colspan=3></td>\n')
|
|
|
|
if new:
|
|
fh.write(' <td>')
|
|
fh.write(text_type(new.ttl))
|
|
fh.write('</td>\n <td>')
|
|
fh.write(_value_stringifier(new, '<br/>'))
|
|
fh.write('</td>\n <td>')
|
|
if new.source:
|
|
fh.write(new.source.id)
|
|
fh.write('</td>\n </tr>\n')
|
|
|
|
fh.write(' <tr>\n <td colspan=6>Summary: ')
|
|
fh.write(text_type(plan))
|
|
fh.write('</td>\n </tr>\n</table>\n')
|
|
else:
|
|
fh.write('<b>No changes were planned</b>')
|