mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
The SplitYamlProvider itself now requires a directory matching the zone name under its directory to contain all YAML files. This doesn't actually change the intended usage at all, just how the configuration file is laid out. Signed-off-by: Christian Funkhouser <cfunkhouser@heroku.com>
204 lines
7.5 KiB
Python
204 lines
7.5 KiB
Python
#
|
|
#
|
|
#
|
|
|
|
from __future__ import absolute_import, division, print_function, \
|
|
unicode_literals
|
|
|
|
from collections import defaultdict
|
|
from os import listdir, makedirs
|
|
from os.path import isdir, isfile, join
|
|
import logging
|
|
|
|
from ..record import Record
|
|
from ..yaml import safe_load, safe_dump
|
|
from .base import BaseProvider
|
|
|
|
|
|
class YamlProvider(BaseProvider):
|
|
'''
|
|
Core provider for records configured in yaml files on disk.
|
|
|
|
config:
|
|
class: octodns.provider.yaml.YamlProvider
|
|
# The location of yaml config files (required)
|
|
directory: ./config
|
|
# The ttl to use for records when not specified in the data
|
|
# (optional, default 3600)
|
|
default_ttl: 3600
|
|
# Whether or not to enforce sorting order on the yaml config
|
|
# (optional, default True)
|
|
enforce_order: True
|
|
'''
|
|
SUPPORTS_GEO = True
|
|
SUPPORTS_DYNAMIC = True
|
|
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
|
'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT'))
|
|
|
|
def __init__(self, id, directory, default_ttl=3600, enforce_order=True,
|
|
*args, **kwargs):
|
|
self.log = logging.getLogger('YamlProvider[{}]'.format(id))
|
|
self.log.debug('__init__: id=%s, directory=%s, default_ttl=%d, '
|
|
'enforce_order=%d', id, directory, default_ttl,
|
|
enforce_order)
|
|
super(YamlProvider, self).__init__(id, *args, **kwargs)
|
|
self.directory = directory
|
|
self.default_ttl = default_ttl
|
|
self.enforce_order = enforce_order
|
|
|
|
def _populate_from_file(self, filename, zone, lenient):
|
|
with open(filename, 'r') as fh:
|
|
yaml_data = safe_load(fh, enforce_order=self.enforce_order)
|
|
if yaml_data:
|
|
for name, data in yaml_data.items():
|
|
if not isinstance(data, list):
|
|
data = [data]
|
|
for d in data:
|
|
if 'ttl' not in d:
|
|
d['ttl'] = self.default_ttl
|
|
record = Record.new(zone, name, d, source=self,
|
|
lenient=lenient)
|
|
zone.add_record(record, lenient=lenient)
|
|
self.log.debug(
|
|
'_populate_from_file: successfully loaded "%s"', filename)
|
|
|
|
def populate(self, zone, target=False, lenient=False):
|
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
|
target, lenient)
|
|
|
|
if target:
|
|
# When acting as a target we ignore any existing records so that we
|
|
# create a completely new copy
|
|
return False
|
|
|
|
before = len(zone.records)
|
|
filename = join(self.directory, '{}yaml'.format(zone.name))
|
|
self._populate_from_file(filename, zone, lenient)
|
|
|
|
self.log.info('populate: found %s records, exists=False',
|
|
len(zone.records) - before)
|
|
return False
|
|
|
|
def _apply(self, plan):
|
|
desired = plan.desired
|
|
changes = plan.changes
|
|
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
|
|
len(changes))
|
|
# Since we don't have existing we'll only see creates
|
|
records = [c.new for c in changes]
|
|
# Order things alphabetically (records sort that way
|
|
records.sort()
|
|
data = defaultdict(list)
|
|
for record in records:
|
|
d = record.data
|
|
d['type'] = record._type
|
|
if record.ttl == self.default_ttl:
|
|
# ttl is the default, we don't need to store it
|
|
del d['ttl']
|
|
if record._octodns:
|
|
d['octodns'] = record._octodns
|
|
data[record.name].append(d)
|
|
|
|
# Flatten single element lists
|
|
for k in data.keys():
|
|
if len(data[k]) == 1:
|
|
data[k] = data[k][0]
|
|
|
|
if not isdir(self.directory):
|
|
makedirs(self.directory)
|
|
|
|
self._do_apply(desired, data)
|
|
|
|
def _do_apply(self, desired, data):
|
|
filename = join(self.directory, '{}yaml'.format(desired.name))
|
|
self.log.debug('_apply: writing filename=%s', filename)
|
|
with open(filename, 'w') as fh:
|
|
safe_dump(dict(data), fh)
|
|
|
|
|
|
# Any record name added to this set will be included in the catch-all file,
|
|
# instead of a file matching the record name.
|
|
_CATCHALL_RECORD_NAMES = ('*', '')
|
|
|
|
|
|
def list_all_yaml_files(directory):
|
|
yaml_files = set()
|
|
for f in listdir(directory):
|
|
filename = join(directory, '{}'.format(f))
|
|
if f.endswith('.yaml') and isfile(filename):
|
|
yaml_files.add(filename)
|
|
return list(yaml_files)
|
|
|
|
|
|
class SplitYamlProvider(YamlProvider):
|
|
'''
|
|
Core provider for records configured in multiple YAML files on disk.
|
|
|
|
Behaves mostly similarly to YamlConfig, but interacts with multiple YAML
|
|
files, instead of a single monolitic one. The files are named RECORD.yaml,
|
|
except for any record which cannot be represented easily as a file; these
|
|
are stored in the catchall file, which is a YAML file the zone name,
|
|
prepended with a '$'. For example, a zone, 'github.com.' would have a
|
|
catch-all file named '$github.com.yaml'.
|
|
|
|
config:
|
|
class: octodns.provider.yaml.SplitYamlProvider
|
|
# The location of yaml config files (required)
|
|
directory: ./config
|
|
# The ttl to use for records when not specified in the data
|
|
# (optional, default 3600)
|
|
default_ttl: 3600
|
|
# Whether or not to enforce sorting order on the yaml config
|
|
# (optional, default True)
|
|
enforce_order: True
|
|
'''
|
|
|
|
def __init__(self, id, directory, *args, **kwargs):
|
|
super(SplitYamlProvider, self).__init__(id, directory, *args, **kwargs)
|
|
self.log = logging.getLogger('SplitYamlProvider[{}]'.format(id))
|
|
|
|
def _zone_directory(self, zone):
|
|
return join(self.directory, zone.name)
|
|
|
|
def populate(self, zone, target=False, lenient=False):
|
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
|
target, lenient)
|
|
|
|
if target:
|
|
# When acting as a target we ignore any existing records so that we
|
|
# create a completely new copy
|
|
return False
|
|
|
|
before = len(zone.records)
|
|
yaml_filenames = list_all_yaml_files(self._zone_directory(zone))
|
|
self.log.info('populate: found %s YAML files', len(yaml_filenames))
|
|
for yaml_filename in yaml_filenames:
|
|
self._populate_from_file(yaml_filename, zone, lenient)
|
|
|
|
self.log.info('populate: found %s records, exists=False',
|
|
len(zone.records) - before)
|
|
return False
|
|
|
|
def _do_apply(self, desired, data):
|
|
zone_dir = self._zone_directory(desired)
|
|
if not isdir(zone_dir):
|
|
makedirs(zone_dir)
|
|
|
|
catchall = dict()
|
|
for record, config in data.items():
|
|
if record in _CATCHALL_RECORD_NAMES:
|
|
catchall[record] = config
|
|
continue
|
|
filename = join(zone_dir, '{}.yaml'.format(record))
|
|
self.log.debug('_apply: writing filename=%s', filename)
|
|
with open(filename, 'w') as fh:
|
|
record_data = {record: config}
|
|
safe_dump(record_data, fh)
|
|
if catchall:
|
|
# Scrub the trailing . to make filenames more sane.
|
|
dname = desired.name[:-1]
|
|
filename = join(zone_dir, '${}.yaml'.format(dname))
|
|
self.log.debug('_apply: writing catchall filename=%s', filename)
|
|
with open(filename, 'w') as fh:
|
|
safe_dump(catchall, fh)
|