1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00
Files
github-octodns/octodns/provider/plan.py
2018-01-21 13:47:58 -08:00

268 lines
9.0 KiB
Python

#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from StringIO import StringIO
from logging import DEBUG, ERROR, INFO, WARN, getLogger
from sys import stdout
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, create,
update_pcent_threshold=MAX_SAFE_UPDATE_PCENT,
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT):
self.existing = existing
self.desired = desired
self.changes = changes
self.create = create
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, {} is over {} percent'
'({}/{})'.format(
update_pcent,
self.MAX_SAFE_UPDATE_PCENT * 100,
self.change_counts['Update'],
existing_record_count))
if delete_pcent > self.delete_pcent_threshold:
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 _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(target)
buf.write(')\n* ')
for change in plan.changes:
buf.write(change.__repr__(leader='* '))
buf.write('\n* ')
buf.write('Summary: ')
buf.write(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 = [str(v) for v in record.values]
except AttributeError:
values = [record.value]
for code, gv in sorted(getattr(record, 'geo', {}).items()):
vs = ', '.join([str(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')
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(str(existing.ttl))
fh.write(' | ')
fh.write(_value_stringifier(existing, '; '))
fh.write(' | |\n')
if new:
fh.write('| | | | ')
if new:
fh.write(str(new.ttl))
fh.write(' | ')
fh.write(_value_stringifier(new, '; '))
fh.write(' | ')
fh.write(new.source.id)
fh.write(' |\n')
fh.write('\nSummary: ')
fh.write(str(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>
''')
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(str(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(str(new.ttl))
fh.write('</td>\n <td>')
fh.write(_value_stringifier(new, '<br/>'))
fh.write('</td>\n <td>')
fh.write(new.source.id)
fh.write('</td>\n </tr>\n')
fh.write(' <tr>\n <td colspan=6>Summary: ')
fh.write(str(plan))
fh.write('</td>\n </tr>\n</table>\n')
else:
fh.write('<b>No changes were planned</b>')