# HG changeset patch # User Tero Marttila # Date 1387658618 -7200 # Node ID bfdf1633b2a1cc4dcdf8011170024f7943201e00 # Parent b58236f9ea7b767eb950a9ee494f2c7d032fc2dc# Parent bed4765fc56f5a807b62aecb60cf3524e94f3428 merge dns-new branch into default diff -r b58236f9ea7b -r bfdf1633b2a1 .hgignore --- a/.hgignore Fri May 10 00:05:25 2013 +0300 +++ b/.hgignore Sat Dec 21 22:43:38 2013 +0200 @@ -1,13 +1,9 @@ -# temp -\.sw[op]$ -\.pyc$ +syntax:glob -# output -^zones/ -^var/ +.*.swo +.*.swp -# data -^settings/ +etc/ +opt/ +var/ -# stuff -^old/ diff -r b58236f9ea7b -r bfdf1633b2a1 bin/check-dhcp-hosts --- a/bin/check-dhcp-hosts Fri May 10 00:05:25 2013 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,201 +0,0 @@ -#!/usr/bin/env python - -""" - Go through a dhcp conf file looking for fixed-address stanzas, and make sure that they are valid. -""" - -__version__ = '0.0.1-dev' - -import optparse -import codecs -import logging - -import socket - -log = logging.getLogger('main') - -# command-line options, global state -options = None - -def parse_options (argv) : - """ - Parse command-line arguments. - """ - - prog = argv[0] - - parser = optparse.OptionParser( - prog = prog, - usage = '%prog: [options]', - version = __version__, - - # module docstring - description = __doc__, - ) - - # logging - general = optparse.OptionGroup(parser, "General Options") - - general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") - general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") - general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") - - parser.add_option_group(general) - - # input/output - parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', - help="Encoding used for input files") - - # - parser.add_option('--doctest', action='store_true', - help="Run module doctests") - - # defaults - parser.set_defaults( - loglevel = logging.WARN, - ) - - # parse - options, args = parser.parse_args(argv[1:]) - - # configure - logging.basicConfig( - format = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s', - level = options.loglevel, - ) - - return options, args - -def parse_fixedaddrs (file) : - """ - Go through lines in given .conf file, looking for fixed-address stanzas. - """ - - filename = file.name - - for lineno, line in enumerate(file) : - # comments? - if '#' in line : - line, comment = line.split('#', 1) - - else : - comment = None - - # whitespace - line = line.strip() - - if not line : - # empty - continue - - # grep - if 'fixed-address' in line : - # great parsing :) - fixedaddr = line.replace('fixed-address', '').replace(';', '').strip() - - log.debug("%s:%d: %s: %s", filename, lineno, fixedaddr, line) - - yield lineno, fixedaddr - -def resolve_addr (addr, af=socket.AF_INET, socktype=socket.SOCK_STREAM) : - """ - Resolve given address for given AF_INET, returning a list of resolved addresses. - - Raises an Exception if failed. - - >>> resolve_addr('127.0.0.1') - ['127.0.0.1'] - """ - - if not addr : - raise Exception("Empty addr: %r", addr) - - # resolve - result = socket.getaddrinfo(addr, None, af, socktype) - - #log.debug("%s: %s", addr, result) - - # addresses - addrs = list(sorted(set(sockaddr[0] for family, socktype, proto, canonname, sockaddr in result))) - - return addrs - -def check_file_hosts (file) : - """ - Check all fixed-address parameters in given file. - """ - - filename = file.name - fail = 0 - - for lineno, addr in parse_fixedaddrs(file) : - # lookup - try : - resolved = resolve_addr(addr) - - except Exception as e: - log.warning("%s:%d: failed to resolve: %s: %s", filename, lineno, addr, e) - fail += 1 - - else : - log.debug("%s:%d: %s: %r", filename, lineno, addr, resolved) - - return fail - -def open_file (path, mode, charset) : - """ - Open unicode-enabled file from path, with - using stdio. - """ - - if path == '-' : - # use stdin/out based on mode - stream, func = { - 'r': (sys.stdin, codecs.getreader), - 'w': (sys.stdout, codecs.getwriter), - }[mode[0]] - - # wrap - return func(charset)(stream) - - else : - # open - return codecs.open(path, mode, charset) - -def main (argv) : - global options - - options, args = parse_options(argv) - - if options.doctest : - import doctest - fail, total = doctest.testmod() - return fail - - if args : - # open files - input_files = [open_file(path, 'r', options.input_charset) for path in args] - - else : - # default to stdout - input_files = [open_file('-', 'r', options.input_charset)] - - # process zone data - for file in input_files : - log.info("Reading zone: %s", file) - - fail = check_file_hosts(file) - - if fail : - log.warn("DHCP hosts check failed: %d", fail) - return 2 - - else : - log.info("DHCP hosts check OK") - - return 0 - -if __name__ == '__main__': - import sys - - sys.exit(main(sys.argv)) - diff -r b58236f9ea7b -r bfdf1633b2a1 bin/expand-zone --- a/bin/expand-zone Fri May 10 00:05:25 2013 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,170 +0,0 @@ -#!/usr/bin/env python -# vim: set ft=python : - -""" - Process zonefiles with template expansions. -""" - -__version__ = '0.0.1-dev' - -import optparse -import codecs -import os.path -from datetime import datetime -import logging - -log = logging.getLogger() - -# command-line options, global state -options = None - -def parse_options (argv) : - """ - Parse command-line arguments. - """ - - parser = optparse.OptionParser( - prog = argv[0], - usage = '%prog: [options]', - version = __version__, - - # module docstring - description = __doc__, - ) - - # logging - general = optparse.OptionGroup(parser, "General Options") - - general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") - general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") - general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") - - parser.add_option_group(general) - - parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', - help="Encoding used for input files") - - parser.add_option('-o', '--output', metavar='FILE', default='-', - help="Write to output file; default stdout") - - parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', - help="Encoding used for output files") - - parser.add_option('--expand', metavar='NAME=VALUE', action='append', - help="Expand given template variable in zone") - - parser.add_option('--serial', metavar='FILE', - help="Read/expand serial from given .serial file") - - # defaults - parser.set_defaults( - loglevel = logging.WARN, - expand = [], - ) - - # parse - options, args = parser.parse_args(argv[1:]) - - # configure - logging.basicConfig( - format = '%(processName)s: %(name)s: %(levelname)s %(funcName)s : %(message)s', - level = options.loglevel, - ) - - return options, args - -def process_file (file, expansions) : - """ - Process file, expanding lines. - """ - - for line in file : - line = line.format(**expansions) - - yield line - -def write_lines (file, lines, suffix='\n') : - for line in lines : - file.write(line + suffix) - -def open_file (path, mode, charset) : - """ - Open unicode-enabled file from path, with - using stdio. - """ - - if path == '-' : - # use stdin/out based on mode - stream, func = { - 'r': (sys.stdin, codecs.getreader), - 'w': (sys.stdout, codecs.getwriter), - }[mode[0]] - - # wrap - return func(charset)(stream) - - else : - # open - return codecs.open(path, mode, charset) - -def process_serial (path) : - """ - Use serial number from given file. - - Returns the new serial as a string. - """ - - if not os.path.exists(path) : - raise Exception("Given --serial does not exist: %s" % path) - - return open(path).read().strip() - -def parse_expand (expand) : - """ - Parse an --expand foo=bar to (key, value) - """ - - key, value = expand.split('=', 1) - - return key, value - -def main (argv) : - global options - - options, args = parse_options(argv) - - # expands - expand = dict(parse_expand(expand) for expand in options.expand) - - # serial? - if options.serial : - serial = process_serial(options.serial) - - expand['serial'] = serial - - # input - if args : - # open files - input_files = [open_file(path, 'r', options.input_charset) for path in args] - - else : - # default to stdout - input_files = [open_file('-', 'r', options.input_charset)] - - # process - lines = [] - - for file in input_files : - log.info("Reading zone: %s", file) - - lines += list(process_file(file, expand)) - - # output - output = open_file(options.output, 'w', options.output_charset) - write_lines(output, lines, suffix='') - - return 0 - -if __name__ == '__main__': - import sys - - sys.exit(main(sys.argv)) diff -r b58236f9ea7b -r bfdf1633b2a1 bin/process-zone --- a/bin/process-zone Fri May 10 00:05:25 2013 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,833 +0,0 @@ -#!/usr/bin/env python - -""" - Process zonefiles. -""" - -__version__ = '0.0.1-dev' - -import optparse -import codecs -from datetime import datetime -import logging - -import ipaddr - -log = logging.getLogger('main') - -# command-line options, global state -options = None - -def parse_options (argv) : - """ - Parse command-line arguments. - """ - - prog = argv[0] - - parser = optparse.OptionParser( - prog = prog, - usage = '%prog: [options]', - version = __version__, - - # module docstring - description = __doc__, - ) - - # logging - general = optparse.OptionGroup(parser, "General Options") - - general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") - general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") - general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") - - parser.add_option_group(general) - - # input/output - parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', - help="Encoding used for input files") - - parser.add_option('-o', '--output', metavar='FILE', default='-', - help="Write to output file; default stdout") - - parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', - help="Encoding used for output files") - - # check stage - parser.add_option('--check-hosts', action='store_true', - help="Check that host/IPs are unique. Use --quiet to silence warnings, and test exit status") - - parser.add_option('--check-exempt', metavar='HOST', action='append', - help="Allow given names to have multiple records") - - # meta stage - parser.add_option('--meta-zone', action='store_true', - help="Generate host metadata zone; requires --input-line-date") - - parser.add_option('--meta-ignore', metavar='HOST', action='append', - help="Ignore given hostnames in metadata output") - - parser.add_option('--input-line-date', action='store_true', - help="Parse timestamp prefix from each input line (e.g. `hg blame | ...`)") - - # forward stage - parser.add_option('--forward-zone', action='store_true', - help="Generate forward zone") - - parser.add_option('--forward-txt', action='store_true', - help="Generate TXT records for forward zone") - - parser.add_option('--forward-mx', metavar='MX', - help="Generate MX records for forward zone") - - # reverse stage - parser.add_option('--reverse-domain', metavar='DOMAIN', - help="Domain to use for hosts in reverse zone") - - parser.add_option('--reverse-zone', metavar='NET', - help="Generate forward zone for given subnet (x.z.y | a:b:c:d)") - - # - parser.add_option('--doctest', action='store_true', - help="Run module doctests") - - # defaults - parser.set_defaults( - loglevel = logging.WARN, - - # XXX: combine - check_exempt = [], - meta_ignore = [], - ) - - # parse - options, args = parser.parse_args(argv[1:]) - - # configure - logging.basicConfig( - format = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s', - level = options.loglevel, - ) - - return options, args - -class ZoneError (Exception) : - pass - -class ZoneLineError (ZoneError) : - """ - ZoneLine-related error - """ - - def __init__ (self, line, msg, *args, **kwargs) : - super(ZoneLineError, self).__init__("%s: %s" % (line, msg.format(*args, **kwargs))) - -class ZoneLine (object) : - """ - A line in a zonefile. - """ - - file = None - lineno = None - - # data - indent = None # was the line indented? - data = None - parts = None # split line fields - - # optional - timestamp = None - comment = None - - PARSE_DATETIME_FORMAT = '%Y-%m-%d' - - @classmethod - def parse (cls, file, lineno, line, line_timestamp_prefix=False) : - """ - Parse out given line and build. - """ - - log.debug("parse: %s:%d: %s", file, lineno, line) - - ts = None - - if line_timestamp_prefix : - if ': ' not in line : - raise ZoneError("%s:%d: Missing timestamp prefix: %s" % (file, lineno, line)) - - # split prefix - prefix, line = line.split(': ', 1) - - # parse it out - ts = datetime.strptime(prefix, cls.PARSE_DATETIME_FORMAT) - - log.debug(" ts=%r", ts) - - # was line indented? - indent = line.startswith(' ') or line.startswith('\t') - - # strip - line = line.strip() - - log.debug(" indent=%r, line=%r", indent, line) - - # parse comment out? - if ';' in line : - line, comment = line.split(';', 1) - - line = line.strip() - comment = comment.strip() - - else : - line = line.strip() - comment = None - - log.debug(" line=%r, comment=%r", line, comment) - - # parse fields - if '"' in line : - pre, data, post = line.split('"', 2) - parts = pre.split() + [data] + post.split() - - else : - parts = line.split() - - log.debug(" parts=%r", parts) - - # build - return cls(file, lineno, indent, line, parts, timestamp=ts, comment=comment) - - def __init__ (self, file, lineno, indent, data, parts, timestamp=None, comment=None) : - self.file = file - self.lineno = lineno - - self.indent = indent - self.data = data - self.parts = parts - - self.timestamp = timestamp - self.comment = comment - - def __str__ (self) : - return "{file}:{lineno}".format(file=self.file, lineno=self.lineno) - -class ZoneRecord (object) : - """ - A record from a zonefile. - """ - - # the underlying line - line = None - - # possible $ORIGIN context - origin = None - - # record fields - name = None - type = None - - # list of data fields - data = None - - # optional - ttl = None - cls = None - - @classmethod - def parse (cls, line, parts=None, origin=None) : - """ - Parse from ZoneLine. Returns None if there is no record on the line.. - """ - - if parts is None : - parts = list(line.parts) - - if not parts : - # skip - return - - # XXX: indented lines keep name from previous record - if line.indent : - name = None - - else : - name = parts.pop(0) - - log.debug(" name=%r, origin=%r", name, origin) - - if len(parts) < 2 : - raise ZoneLineError(line, "Too few parts to parse: {0!r}", line.data) - - # parse ttl/cls/type - ttl = _cls = None - - if parts and parts[0][0].isdigit() : - ttl = parts.pop(0) - - if parts and parts[0].upper() in ('IN', 'CH') : - _cls = parts.pop(0) - - # always have type - type = parts.pop(0) - - # remaining parts are data - data = parts - - log.debug(" ttl=%r, cls=%r, type=%r, data=%r", ttl, _cls, type, data) - - return cls(name, type, data, - origin = origin, - ttl = ttl, - cls = _cls, - line = line, - ) - - def __init__ (self, name, type, data, origin=None, ttl=None, cls=None, line=None, comment=None) : - self.name = name - self.type = type - self.data = data - - self.ttl = ttl - self.cls = cls - - self.origin = origin - self.line = line - - # XXX: within line - self._comment = comment - - def build_line (self) : - """ - Construct a zonefile-format line..." - """ - - # XXX: comment? - if self._comment : - comment = '\t; ' + self._comment - else : - comment = '' - - return u"{name:25} {ttl:4} {cls:2} {type:5} {data}{comment}".format( - name = self.name or '', - ttl = self.ttl or '', - cls = self.cls or '', - type = self.type, - data = ' '.join(unicode(data) for data in self.data), - comment = comment, - ) - - def __str__ (self) : - return ' '.join((self.name or '', self.type, ' '.join(self.data))) - -class TXTRecord (ZoneRecord) : - """ - TXT record. - """ - - def __init__ (self, name, text, **opts) : - return super(TXTRecord, self).__init__(name, 'TXT', - [u'"{0}"'.format(text.replace('"', '\\"'))], - **opts - ) - -class OffsetValue (object) : - def __init__ (self, value) : - self.value = value - - def __getitem__ (self, offset) : - value = self.value + offset - - #log.debug("OffsetValue: %d[%d] -> %d", self.value, offset, value) - - return value - -def parse_generate_field (line, field) : - """ - Parse a $GENERATE lhs/rhs field: - $ - ${[,[,]]} - \$ - $$ - - Returns a wrapper that builds the field-value when called with the index. - - >>> parse_generate_field(None, "foo")(1) - 'foo' - >>> parse_generate_field(None, "foo-$")(1) - 'foo-1' - >>> parse_generate_field(None, "foo-$$")(1) - 'foo-$' - >>> parse_generate_field(None, "\$")(1) - '$' - >>> parse_generate_field(None, "10.0.0.${100}")(1) - '10.0.0.101' - >>> parse_generate_field(None, "foo-${0,2,d}")(1) - 'foo-01' - - """ - - input = field - expr = [] - - while '$' in field : - # defaults - offset = 0 - width = 0 - base = 'd' - escape = False - - # different forms - if '${' in field : - pre, body = field.split('${', 1) - body, post = body.split('}', 1) - - # parse body - parts = body.split(',') - - # offset - offset = int(parts.pop(0)) - - # width - if parts : - width = int(parts.pop(0)) - - # base - if parts : - base = parts.pop(0) - - if parts: - # fail - raise ZoneLineError(line, "extra data in ${...} body: {0!r}", parts) - - elif '$$' in field : - pre, post = field.split('$$', 1) - escape = True - - elif '\\$' in field : - pre, post = field.split('\\$', 1) - escape = True - - else : - pre, post = field.split('$', 1) - - expr.append(pre) - - if escape : - expr.append('$') - - else : - # meta-format - fmt = '{value[%d]:0%d%s}' % (offset, width, base) - - log.debug("field=%r -> pre=%r, fmt=%r, post=%r", field, expr, fmt, post) - - expr.append(fmt) - - field = post - - # final - if field : - expr.append(field) - - # combine - expr = ''.join(expr) - - log.debug("%s: %s", input, expr) - - # processed - def value_func (value) : - # magic wrapper to implement offsets - return expr.format(value=OffsetValue(value)) - - return value_func - -def process_generate (line, origin, parts) : - """ - Process a - $GENERATE -[/] lhs [ttl] [class] type rhs [comment] - directive into a series of ZoneResource's. - """ - - range = parts.pop(0) - - # parse range - if '/' in range : - range, step = range.split('/') - step = int(step) - else : - step = 1 - - start, stop = range.split('-') - start = int(start) - stop = int(stop) - - log.debug(" range: start=%r, stop=%r, step=%r", start, stop, step) - - # inclusive - range = xrange(start, stop + 1, step) - - lhs_func = parse_generate_field(line, parts.pop(0)) - rhs_func = parse_generate_field(line, parts.pop(-1)) - body = parts - - for i in range : - # build - parts = [lhs_func(i)] + body + [rhs_func(i)] - - log.debug(" %03d: %r", i, parts) - - # parse - yield ZoneRecord.parse(line, parts=parts, origin=origin) - -def parse_zone_records (file, origin=None, **opts) : - """ - Parse ZoneRecord items from the given zonefile, ignoring non-record lines. - """ - - ttl = None - - skip_multiline = False - - for lineno, raw_line in enumerate(file) : - # parse comment - if ';' in raw_line : - line, comment = raw_line.split(';', 1) - else : - line = raw_line - comment = None - - # XXX: handle multi-line statements... - # start - if '(' in line : - skip_multiline = True - - log.warn("%s:%d: Start of multi-line statement: %s", file.name, lineno, raw_line) - - # end? - if ')' in line : - skip_multiline = False - - log.warn("%s:%d: End of multi-line statement: %s", file.name, lineno, raw_line) - - continue - - elif skip_multiline : - log.warn("%s:%d: Multi-line statement: %s", file.name, lineno, raw_line) - - continue - - # parse - line = ZoneLine.parse(file.name, lineno, raw_line, **opts) - - if not line.data : - log.debug("%s: skip empty line: %s", line, raw_line) - - continue - - elif line.data.startswith('$') : - # control record - type = line.parts[0] - - if type == '$ORIGIN': - # update - origin = line.parts[1] - - log.info("%s: origin: %s", line, origin) - - elif type == '$GENERATE': - # process... - log.info("%s: generate: %s", line, line.parts) - - for record in process_generate(line, origin, line.parts[1:]) : - yield record - - else : - log.warning("%s: skip control record: %s", line, line.data) - - # XXX: passthrough! - continue - - # normal record? - record = ZoneRecord.parse(line, origin=origin) - - if record : - yield record - - else : - # unknown - log.warning("%s: skip unknown line: %s", line, line.data) - -def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) : - """ - Parse host/IP pairs from the zone, and verify that they are unique. - - As an exception, names listed in the given whitelist may have multiple IPs. - """ - - by_name = {} - by_ip = {} - - fail = None - - last_name = None - - for r in zone : - name = r.name or last_name - - name = (r.origin, name) - - # name - if r.type not in whitelist_types : - if name not in by_name : - by_name[name] = r - - elif r.name in whitelist : - log.debug("Duplicate whitelist entry: %s", r) - - else : - # fail! - log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name]) - fail = True - - # ip - if r.type == 'A' : - ip, = r.data - - if ip not in by_ip : - by_ip[ip] = r - - else : - # fail! - log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip]) - fail = True - - return fail - -def process_zone_forwards (zone, txt=False, mx=False) : - """ - Process zone data -> forward zone data. - """ - - for r in zone : - yield r - - if r.type == 'A' : - if txt : - # comment? - comment = r.line.comment - - if comment : - yield TXTRecord(None, comment, ttl=r.ttl) - - - # XXX: RP, do we need it? - - if mx : - # XXX: is this a good idea? - yield ZoneRecord(None, 'MX', [10, mx], ttl=r.ttl) - -def process_zone_meta (zone, ignore=None) : - """ - Process zone metadata -> output. - """ - - TIMESTAMP_FORMAT='%Y/%m/%d' - - for r in zone : - if ignore and r.name in ignore : - # skip - log.debug("Ignore record: %s", r) - continue - - # for hosts.. - if r.type == 'A' : - # timestamp? - timestamp = r.line.timestamp - - if timestamp : - yield TXTRecord(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl) - -def reverse_ipv4 (ip) : - """ - Return in-addr.arpa reverse for given IPv4 prefix. - """ - - # parse - octets = tuple(int(part) for part in ip.split('.')) - - for octet in octets : - assert 0 <= octet <= 255 - - return '.'.join([str(octet) for octet in reversed(octets)] + ['in-addr', 'arpa']) - -def reverse_ipv6 (ip6) : - """ - Return ip6.arpa reverse for given IPv6 prefix. - """ - - parts = [int(part, 16) for part in ip6.split(':')] - parts = ['{0:04x}'.format(part) for part in parts] - parts = ''.join(parts) - - return '.'.join(tuple(reversed(parts)) + ( 'ip6', 'arpa')) - -def fqdn (*parts) : - fqdn = '.'.join(parts) - - # we may be given an fqdn in parts - if not fqdn.endswith('.') : - fqdn += '.' - - return fqdn - -def process_zone_reverse (zone, origin, domain) : - """ - Process zone data -> reverse zone data. - """ - - name = None - - for r in zone : - # keep name from previous.. - if r.name : - name = r.name - - if r.type == 'A' : - ip, = r.data - ptr = reverse_ipv4(ip) - - elif r.type == 'AAAA' : - ip, = r.data - ptr = reverse_ipv6(ip) - - else : - continue - - # verify - if zone and ptr.endswith(origin) : - ptr = ptr[:-(len(origin) + 1)] - - else : - log.warning("Reverse does not match zone origin, skipping: (%s) -> %s <-> %s", ip, ptr, origin) - continue - - # domain to use - host_domain = r.origin or domain - host_fqdn = fqdn(name, host_domain) - - yield ZoneRecord(ptr, 'PTR', [host_fqdn]) - -def write_zone_records (file, zone) : - for r in zone : - file.write(r.build_line() + u'\n') - -def open_file (path, mode, charset) : - """ - Open unicode-enabled file from path, with - using stdio. - """ - - if path == '-' : - # use stdin/out based on mode - stream, func = { - 'r': (sys.stdin, codecs.getreader), - 'w': (sys.stdout, codecs.getwriter), - }[mode[0]] - - # wrap - return func(charset)(stream) - - else : - # open - return codecs.open(path, mode, charset) - -def main (argv) : - global options - - options, args = parse_options(argv) - - if options.doctest : - import doctest - fail, total = doctest.testmod() - return fail - - if args : - # open files - input_files = [open_file(path, 'r', options.input_charset) for path in args] - - else : - # default to stdout - input_files = [open_file('-', 'r', options.input_charset)] - - # process zone data - zone = [] - - for file in input_files : - log.info("Reading zone: %s", file) - - zone += list(parse_zone_records(file, - line_timestamp_prefix = options.input_line_date, - )) - - # check? - if options.check_hosts : - whitelist = set(options.check_exempt) - - log.debug("checking hosts; whitelist=%r", whitelist) - - if check_zone_hosts(zone, whitelist=whitelist) : - log.warn("Hosts check failed") - return 2 - - else : - log.info("Hosts check OK") - - # output file - output = open_file(options.output, 'w', options.output_charset) - - if options.forward_zone : - log.info("Write forward zone: %s", output) - - zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx)) - - elif options.meta_zone : - log.info("Write metadata zone: %s", output) - - if not options.input_line_date : - log.error("--meta-zone requires --input-line-date") - return 1 - - zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore))) - - elif options.reverse_zone : - if ':' in options.reverse_zone : - # IPv6 - origin = reverse_ipv6(options.reverse_zone) - - else : - # IPv4 - origin = reverse_ipv4(options.reverse_zone) - - domain = options.reverse_domain - - if not domain : - log.error("--reverse-zone requires --reverse-domain") - return 1 - - zone = list(process_zone_reverse(zone, origin=origin, domain=domain)) - - elif options.check_hosts : - # we only did that, done - return 0 - - else : - log.warn("Nothing to do") - return 1 - - write_zone_records(output, zone) - - return 0 - -if __name__ == '__main__': - import sys - - sys.exit(main(sys.argv)) diff -r b58236f9ea7b -r bfdf1633b2a1 bin/update --- a/bin/update Fri May 10 00:05:25 2013 +0300 +++ b/bin/update Sat Dec 21 22:43:38 2013 +0200 @@ -1,283 +1,131 @@ #!/bin/bash # vim: set ft=sh : -set -ue - -# resolve $0 -> bin/update -self=$0 -while [ -L $self ]; do - tgt=$(readlink $self) - - if [ "${tgt:0:1}" == "/" ]; then - self=$tgt - else - self=$(dirname $self)/$tgt - fi -done - -# root dir -ROOT=$(dirname $(dirname $self)) - -BIN=$ROOT/bin -LIB=$ROOT/lib -VAR=$ROOT/var - -## Data paths -# absolute path to data files; can be changed using -d -ROOT=$(pwd) - -DATA=settings -ZONES=$VAR/zones -SERIALS=$VAR/serials - -DHCP=$VAR/dhcp -DHCP_DATA=$DATA/dhcp - -# global DHCP conf to test -DHCPD=/usr/sbin/dhcpd -DHCPD_CONF=/etc/dhcp/dhcpd.conf -DHCPD_INIT=/etc/init.d/isc-dhcp-server - -# hg repo to commit -REPO=$DATA - -## Settings used in lib -# Hide files under repo in commit diff output.. -REPO_HIDE='*.serial' - -# data input charsets; arguments to ./bin/... python scripts -HOSTS_FILE_ARGS='--input-charset utf-8' -DHCP_FILE_ARGS='--input-charset utf-8' - -# External bins -NAMED_CHECKZONE=/usr/sbin/named-checkzone - -HG=/usr/bin/hg -HG_ARGS=(--config trusted.users=root) - -RNDC=/usr/sbin/rndc - -# Path to rndc key, must be readable to run.. -RNDC_KEY=/etc/bind/rndc.key - -## Library includes -# Command-line argument handling -source $LIB/update.args +# Bootstrap +if [ $0 == './update' ]; then + SRV=$(pwd) + OPT=./opt +else + SRV=${SRV:-/srv/dns} + OPT=${SRV:-/srv/dns/opt} + cd $SRV +fi -# Logging -source $LIB/update.logging - -# Utility functions -source $LIB/update.utils - -# Dependency-based updates -source $LIB/update.updates - -# Operations; the functions called from run() -source $LIB/update.operations - -## Flags -# set by do_reload_zone if zone data has actually been reloaded -RELOAD_ZONES= - -## Site settings, used as arguments to scripts -# MX record to generate in hosts --forward-zone -FORWARD_MX=mx0 - -# IP network to generate reverse records for in --reverse-zone -REVERSE_ZONE=194.197.235 - -# Origin domain to generate reverse records for in --reverse-zone -REVERSE_DOMAIN=paivola.fi - -# Views used -VIEWS=(internal external) - -# Base domain zone for domains -DOMAIN_BASE=paivola +source lib/update -# List of actual domains used; will be linked to $DOMAIN_BASE -DOMAINS=(paivola.fi paivola.net paivola.org paivola.info paivola.mobi xn--pivl-load8j.fi) - -# Names of dhcp conf file names -DHCP_CONFS=( $(list_files $DHCP_DATA *.conf) ) - -## Operate! -# these functions are all defined in lib/update.operations - -# Update $ZONES/$DHCP host-files from $DATA -function run_hosts { - ## Hosts - # test +function commit { + ## Commit + # pre-commit check log "Testing hosts..." - # data args... - check_hosts $DATA/paivola.txt --check-exempt ufc + for hosts in $(list_files etc/hosts); do + log_warn "TODO: check_hosts $hosts" + done - # update - log "Generating host zones..." - # hosts data args... - update_hosts $ZONES/hosts/paivola:internal $DATA/paivola.txt --forward-zone --forward-txt - update_hosts $ZONES/hosts/paivola:external $DATA/paivola.txt --forward-zone - update_hosts $ZONES/hosts/194.197.235 $DATA/paivola.txt --reverse-zone $REVERSE_ZONE --reverse-domain $REVERSE_DOMAIN - - - update_hosts $ZONES/hosts/10 $DATA/pvl.txt --reverse-zone 10 --reverse-domain pvl -q - update_hosts $ZONES/hosts/10.0 $DATA/test.pvl.txt --reverse-zone 10.0 --reverse-domain test.pvl -q - update_hosts $ZONES/hosts/fdc4:4cef:395a $DATA/test.pvl.txt --reverse-zone fdc4:4cef:395a --reverse-domain test.pvl -q - update_hosts $ZONES/hosts/192.168 $DATA/pvl.txt --reverse-zone 192.168 --reverse-domain pvl -q - - # XXX: unsupported --forward-zone with pvl.txt - # update_hosts $ZONES/hosts/pvl $DATA/pvl.txt --forward-zone - copy_hosts $ZONES/hosts/pvl $DATA/pvl.txt - copy_hosts $ZONES/hosts/test.pvl $DATA/test.pvl.txt + # commit, unless noop'd + log "Commit..." + update_commit etc } -# Update $ZONES files -function run_zones { - ## Includes - log "Copying zone includes..." - # view zone base - copy_zone includes paivola:internal paivola.zone.internal - copy_zone includes paivola:external paivola.zone.external - copy_zone includes paivola.auto paivola.zone.auto - copy_zone includes paivola.services paivola.zone.services - copy_zone includes paivola.aux paivola.zone.aux - - ## Serials - log "Updating serials..." +function update { + if hg_modified etc; then + serial=$(unix_time) + log_warn "Using local unix time for uncommited changes: $serial" + else + serial=$(hg_time etc) + log_update "Using HG commit timestamp: $serial" + fi - # zone deps... - # includes... - update_serial pvl $ZONES/hosts/pvl $DATA/pvl.zone - update_serial test.pvl $ZONES/hosts/test.pvl $DATA/test.pvl.zone - update_serial 10 $ZONES/hosts/10 $DATA/10.zone - update_serial 10.0 $ZONES/hosts/10.0 $DATA/10.0.zone - update_serial fdc4:4cef:395a $ZONES/hosts/fdc4:4cef:395a $DATA/fdc4:4cef:395a.zone - update_serial 192.168 $ZONES/hosts/192.168 $DATA/192.168.zone + ## Hosts + log "Updating forward host zones..." + for zone in $(list_dirs etc/hosts/forward); do + update_hosts_forward "var/zones/hosts/forward/$zone" "$zone" \ + etc/hosts/forward/$zone/* + done - update_serial paivola $ZONES/hosts/paivola:* $DATA/paivola.zone \ - $ZONES/includes/paivola:* \ - $ZONES/includes/paivola.* + log "Updating DHCP hosts..." + for hosts in $(list_files etc/hosts); do + update_hosts_dhcp "var/dhcp/hosts/$hosts.conf" \ + "etc/hosts/$hosts" + done - update_serial 194.197.235 $ZONES/hosts/194.197.235 $DATA/194.197.235.zone + log "Updating reverse host zones..." + for zone in $(list_dirs etc/hosts/reverse); do + update_hosts_reverse "var/zones/hosts/reverse/$zone" "$zone" \ + etc/hosts/reverse/$zone/* + done ## Zones - log "Updating zones..." - # view zone base - update_zone internal pvl - update_zone internal test.pvl - - update_zone internal 10 - update_zone internal 10.0 - update_zone internal fdc4:4cef:395a - update_zone internal 192.168 - - update_zone common 194.197.235 - link_zone internal 194.197.235 - link_zone external 194.197.235 + log "Copying zone includes..." + for zone in $(list_files etc/zones/includes); do + copy "var/zones/includes/$zone" "etc/zones/includes/$zone" + done - ## Test - log "Testing zones..." - # view zone origin - check_zone internal 10 10.in-addr.arpa - check_zone internal 10.0 0.10.in-addr.arpa - check_zone internal fdc4:4cef:395a a.5.9.3.f.e.c.4.4.c.d.f.ip6.arpa - - check_zone internal 192.168 192.168.in-addr.arpa - check_zone common 194.197.235 235.197.194.in-addr.arpa + log "Updating zone serials..." + for zone in $(list_files etc/zones); do + update_serial "var/serials/$zone" $serial \ + "etc/zones/$zone" $(zone_includes var/include-cache/$zone etc/zones/$zone var/zones/) + done - ## Domains... - log "Linking domains..." - for view in "${VIEWS[@]}"; do - for zone in "${DOMAINS[@]}"; do - # choose input .zone to use - base=$(choose_zone $zone $DOMAIN_BASE) - - if [ $base != $DOMAIN_BASE ]; then - # serial - # XXX: not all zones use all these includes? - update_serial $base $DATA/$base.zone \ - $ZONES/hosts/paivola:* \ - $ZONES/includes/paivola:* \ - $ZONES/includes/paivola.* - fi + log "Updating zones..." + for zone in $(list_files etc/zones); do + update_zone "var/zones/$zone" "etc/zones/$zone" "var/serials/$zone" \ + $(zone_includes var/include-cache/$zone etc/zones/$zone var/zones/) + done - # link - update_zone $view $zone $base - - # test - check_zone $view $zone $zone - done - done + log "Updating DHCP confs..." + for conf in $(list_files etc/dhcp); do + update_dhcp_conf "var/dhcp/$conf" "etc/dhcp/$conf" + done } -# Update $DHCP files from $DATA/dhcp -function run_dhcp { - log_debug "DHCP_CONFS: ${DHCP_CONFS[*]}" - - log "Copying DHCP configs..." - for conf in "${DHCP_CONFS[@]}"; do - # XXX: ei toimi, koska conf:it riippuu toisistaan include:ien takia - # check_dhcp_conf $conf - - # conf base - copy_dhcp_conf $conf - done +function deploy { + ## Check + log "Testing zones..." + for zone in $(list_files etc/zones); do + check_zone "var/zones/$zone" $zone + done - log "Testing dhcp..." - # checks the whole dhcpd.conf, with all includes.. - check_dhcp -} + log "Testing DHCP confs..." + for conf in var/dhcp/*.conf; do + check_dhcp $conf + done -# Runs DHCP checks, once DNS hosts have been updated -function run_dhcp_check { - log "Testing dhcp hosts..." - for conf in "${DHCP_CONFS[@]}"; do - check_dhcp_hosts $DHCP/$conf.conf - done -} - -function run_deploy { - ## Reload zones log "Reload zones..." reload_zones - ## DHCP - run_dhcp_check - log "Reload dhcp..." reload_dhcp - ## Commit - log "Commit data..." - commit_data } ## Main entry point function main { - # test tty - [ -t 1 ] && IS_TTY=y - parse_args "$@" ## Input dirs - [ -d $ROOT/$DATA ] || die "Missing data: $ROOT/$DATA" + for dir in etc etc/zones etc/hosts opt; do + [ -d $dir ] || die "Missing directory: $dir" + done ## Output dirs - for dir in $VAR $DHCP $ZONES $SERIALS; do + ensure_dir var + for dir in var/dhcp var/zones var/include-cache var/serials; do ensure_dir $dir done - - # sub-$ZONES - for dir in "common" "hosts" "includes" "${VIEWS[@]}"; do - ensure_dir $ZONES/$dir + for dir in var/dhcp/hosts; do + ensure_dir $dir + done + for dir in var/zones/includes var/zones/hosts; do + ensure_dir $dir + done + for dir in var/zones/hosts/forward var/zones/hosts/reverse; do + ensure_dir $dir done ## Go - run_hosts - run_zones - run_dhcp - run_deploy + commit + update + deploy } main "$@" diff -r b58236f9ea7b -r bfdf1633b2a1 bin/update-serial --- a/bin/update-serial Fri May 10 00:05:25 2013 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,195 +0,0 @@ -#!/usr/bin/env python -# vim: set ft=python : - -""" - Update zone serials. -""" - -__version__ = '0.0.1-dev' - -import optparse -import codecs -import os.path -from datetime import datetime -import logging - -log = logging.getLogger('main') - -# command-line options, global state -options = None - -def parse_options (argv) : - """ - Parse command-line arguments. - """ - - prog = argv[0] - - parser = optparse.OptionParser( - prog = prog, - usage = '%prog: [options]', - version = __version__, - - # module docstring - description = __doc__, - ) - - # logging - general = optparse.OptionGroup(parser, "General Options") - - general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") - general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") - general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") - - parser.add_option_group(general) - - # defaults - parser.set_defaults( - loglevel = logging.WARN, - expand = [], - ) - - # parse - options, args = parser.parse_args(argv[1:]) - - # configure - logging.basicConfig( - format = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s', - level = options.loglevel, - ) - - return options, args - -# date fmt to use in serial -DATE_FMT = '%Y%m%d' -DATE_LEN = 8 - -SERIAL_FMT = "{date:8}{count:02}" -SERIAL_LEN = 10 - -def format_serial (date, count) : - return SERIAL_FMT.format(date=date.strftime(DATE_FMT), count=count) - -def next_count (value, date, count) : - """ - Return serial with next count. - """ - - count += 1 - - # check - if count > 99 : - serial = str(value + 1) - - log.warn("Serial count rollover: %s, %s; fallback -> %s", date, count, serial) - - return serial - - return format_serial(date, count) - -def update_serial (serial) : - """ - Update new serial number into given file, based on date. - """ - - today = datetime.now().date() - - # handle - if not serial : - # fresh - log.info("Setting initial serial: %s01", today) - - return format_serial(today, 1) - - elif len(serial) != SERIAL_LEN : - log.warn("Invalid serial format: %s", serial) - - value = int(serial) - serial = str(value + 1) - - log.info("Incrementing serial: %d -> %s", value, serial) - - return serial - - else : - # parse - value = int(serial) - - try : - date = datetime.strptime(serial[:DATE_LEN], DATE_FMT).date() - count = int(serial[DATE_LEN:]) - - except ValueError, e : - # invalid date/count format? - log.warn("Unable to parse serial: %s: %s", serial, e) - - serial = str(value + 1) - - log.info("Incrementing serial: %d -> %s", value, serial) - - return serial - - log.debug("old serial=%s, value=%d, date=%s, count=%s", serial, value, date, count) - - # update - if date < today : - log.info("Updating to today: %s -> %s", date, today) - - # update date - return format_serial(today, 1) - - elif date == today : - # keep date, update count - log.info("Updating today's count: %s, %s", date, count) - - # handle count rollover - return next_count(value, date, count) - - elif date > today : - # keep, update count - serial = next_count(value, date, count) - - log.warn("Serial in future; incrementing count: %s, %s -> %s", date, count, serial) - - return serial - - else : - raise Exception("Invalid serial: %s:%s", old_date, old_count) - -def process_serial (path) : - """ - Read old serial, generate new one, and update file. - """ - - # read - if os.path.exists(path) : - serial = open(path).read().strip() - log.debug("current serial: %s", serial) - - else : - log.warn("Given .serial does not yet exist: %s", path) - serial = None - - # update - serial = update_serial(serial) - - # write - open(path, 'w').write(serial + '\n') - - return serial - -def main (argv) : - global options - - options, args = parse_options(argv) - - # serial files to update - for path in args : - process_serial(path) - - return 0 - -if __name__ == '__main__': - import sys - - sys.exit(main(sys.argv)) diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/update Sat Dec 21 22:43:38 2013 +0200 @@ -0,0 +1,11 @@ +#!/bin/bash +# + +## Strict errors +set -ue + +## Library includes +for lib in lib/update.*; do + source $lib +done + diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.args --- a/lib/update.args Fri May 10 00:05:25 2013 +0300 +++ b/lib/update.args Sat Dec 21 22:43:38 2013 +0200 @@ -1,7 +1,7 @@ #!/bin/bash # vim: set ft=sh : # -# Command-line option handling +# Command-line options # use color output? IS_TTY= @@ -70,6 +70,9 @@ ## Parse any command-line arguments, setting the global options vars. function parse_args { + # test tty + [ -t 1 ] && IS_TTY=y + OPTIND=1 while getopts 'hd:qvDVpFSsnCcm:Rr' opt "$@"; do @@ -79,7 +82,7 @@ exit 0 ;; - d) ROOT="$OPTARG" ;; + d) SRV="$OPTARG" ;; q) LOG= @@ -106,7 +109,7 @@ UPDATE_NOOP=y # implies -Sp UPDATE_DIFF=y - SERIAL_NOUPDATE=y + SERIAL_NOOP=y COMMIT_SKIP=y RELOAD_NOOP=y ;; diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.config --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/update.config Sat Dec 21 22:43:38 2013 +0200 @@ -0,0 +1,16 @@ +# charset for files under etc/ +CHARSET='utf-8' + +# External bins +NAMED_CHECKZONE=/usr/sbin/named-checkzone + +DHCPD=/usr/sbin/dhcpd +DHCPD_CONF=/etc/dhcp/dhcpd.conf +DHCPD_INIT=/etc/init.d/isc-dhcp-server + +HG=/usr/bin/hg +HG_ARGS=(--config trusted.users=root) + +RNDC=/usr/sbin/rndc +RNDC_KEY=/etc/bind/rndc.key + diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.hg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/update.hg Sat Dec 21 22:43:38 2013 +0200 @@ -0,0 +1,75 @@ +#!/bin/bash +# +# HG wrappers + +## Run `hg ...` within $REPO. +function hg { + local repo=$1; shift + cmd $HG -R "$repo" "${HG_ARGS[@]}" "$@" +} + +## Does the repo have local modifications? +function hg_modified { + hg $1 id -i | grep -q '+' +} + +## Get the date for the current commit as an unix timestamp +function hg_time { + local repo=$1 + local hg_unix= + local hg_tz= + + local hg_date=$(hg $repo log -r . --template '{date|hgdate}') + local hg_unix=${hg_date% *} + local hg_tz=${hg_date#* } + + [ -n "$hg_unix" ] || fail "failed to read hg time" + + echo "$hg_unix" +} + +## Show changes in repo +# hg_diff [path ...] +function hg_diff { + local repo=$1; shift + hg $repo diff "$@" +} + +## Commit changes in repo, with given message: +# +# hg_commit .../etc $msg +# +# Automatically determines possible -u to use when running with sudo. +function hg_commit { + local repo="$1" + local msg="$2" + local user_opt= + local msg_opt= + + if [ ${SUDO_USER:-} ]; then + user_opt=('-u' "$SUDO_USER") + + elif [ $HOME ] && [ -e $HOME/.hgrc ]; then + debug "using .hgrc user" + user_opt=( ) + + else + user_opt=('-u' "$USER") + fi + + if [ "$msg" ]; then + msg_opt=('-m' "$msg") + fi + + # XXX: there's something about bash arrays that I don't like... empty arrays behave badly + # mercurial does not like it if you pass it '' as an argument + if [ -n "${user_opt:-}" -a -n "${msg_opt:-}" ]; then + hg $repo commit "${user_opt[@]}" "${msg_opt[@]}" + elif [ -n "${user_opt:-}" ]; then + hg $repo commit "${user_opt[@]}" + elif [ -n "${msg_opt:-}" ]; then + hg $repo commit "${msg_opt[@]}" + else + hg $repo commit + fi +} diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.log --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/update.log Sat Dec 21 22:43:38 2013 +0200 @@ -0,0 +1,107 @@ +#!/bin/bash +# vim: set ft=sh : +# +# Logging output + +# Output message to stderr. +function log_msg { + echo "$*" >&2 +} + +# Output message to stderr, optionally with given color, if TTY. +function log_color { + local code=$1; shift + + if [ $IS_TTY ]; then + echo $'\e['${code}'m'"$*"$'\e[00m' >&2 + else + echo "$*" >&2 + fi +} + +## Log at various log-levels +# plain +function log { + [ $LOG ] && log_msg "$*" || true +} + +function log_error { + [ $LOG_ERROR ] && log_color '31' "$*" || true +} + +function log_warn { + [ $LOG_WARN ] && log_color '33' "$*" || true +} + +function log_force { + [ $LOG_FORCE ] && log_color '2;33' " $*" || true +} + +function log_update { + [ $LOG_UPDATE ] && log_color '36' " $*" || true +} + +function log_check { + [ $LOG_UPDATE ] && log_color '37' " $*" || true +} + +function log_noop { + [ $LOG_NOOP ] && log_color '2;34' " $*" || true +} + +function log_skip { + [ $LOG_SKIP ] && log_color '1;34' " $*" || true +} + +function log_debug { + [ $LOG_DEBUG ] && log_color '32' " $*" || true +} + +function log_cmd { + [ $LOG_CMD ] && log_color '35' " \$ $*" || true +} + +# Output stacktrace, broken. +function log_stack { + local level=1 + + while info=$(caller $level); do + echo $info | read line sub file + + log_msg "$file:$lineno $sub()" + + level=$(($level + 1)) + done +} + +# Output calling function's name. +function func_caller { + caller 1 | cut -d ' ' -f 2 +} + +### High-level logging output +# Log with func_caller at log_debug +function debug { + printf -v prefix "%s" $(func_caller) + + log_debug "$prefix: $*" +} + +function warn { + log_warn "$(func_caller): $*" +} + +# Log with func_caller at log_error and exit, intended for internal errors... +function fail { + log_error "$(func_caller): $*" + + exit 2 +} + +# Log at log_error and exit +function die { + log_error "$*" + exit 1 +} + + diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.logging --- a/lib/update.logging Fri May 10 00:05:25 2013 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -#!/bin/bash -# vim: set ft=sh : -# -# Logging output - -# Output message to stderr. -function log_msg { - echo "$*" >&2 -} - -# Output message to stderr, optionally with given color, if TTY. -function log_color { - local code=$1; shift - - if [ $IS_TTY ]; then - echo $'\e['${code}'m'"$*"$'\e[00m' >&2 - else - echo "$*" >&2 - fi -} - -## Log at various log-levels - -function log_error { - [ $LOG_ERROR ] && log_color '31' "$*" -} - -function log_warn { - [ $LOG_WARN ] && log_color '33' "$*" || true -} - -# plain -function log { - [ $LOG ] && log_msg "$*" || true -} - -function log_force { - [ $LOG_FORCE ] && log_color '2;33' " $*" || true -} - -function log_update { - [ $LOG_UPDATE ] && log_color '36' " $*" || true -} - -function log_noop { - [ $LOG_NOOP ] && log_color '2;34' " $*" || true -} - -function log_skip { - [ $LOG_SKIP ] && log_color '1;34' " $*" || true -} - -function log_debug { - [ $LOG_DEBUG ] && log_color 32 " $*" || true -} - -function log_cmd { - [ $LOG_CMD ] && log_color 35 " \$ $*" || true -} - -# Output stacktrace, broken. -function log_stack { - local level=1 - - while info=$(caller $level); do - echo $info | read line sub file - - log_msg "$file:$lineno $sub()" - - level=$(($level + 1)) - done -} - -# Output calling function's name. -function func_caller { - caller 1 | cut -d ' ' -f 2 -} - -### High-level logging output -# Log with func_caller at log_debug -function debug { - printf -v prefix "%s" $(func_caller) - - log_debug "$prefix: $*" -} - -# Log with func_caller at log_error and exit, intended for internal errors... -function fail { - log_error "$(func_caller): $*" - - exit 2 -} - -# Log at log_error and exit -function die { - log_error "$*" - exit 1 -} - - diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.operations --- a/lib/update.operations Fri May 10 00:05:25 2013 +0300 +++ b/lib/update.operations Sat Dec 21 22:43:38 2013 +0200 @@ -3,29 +3,29 @@ # # Operations on zonefiles/hosts/whatever -function link_generic { - local out=$1 - local tgt=$2 +function link { + local out="$1" + local tgt="$2" - if check_link $out $tgt; then + if check_link "$out" "$tgt"; then log_update "Linking $out -> $tgt..." - do_link $out $tgt + do_link "$out" "$tgt" else log_skip "Linking $out -> $tgt: not changed" fi } -function copy_generic { - local out=$1 - local src=$2 +function copy { + local out="$1" + local src="$2" - if check_update $out $src; then + if check_update "$out" "$src"; then log_update "Copying $out <- $src..." - do_update $out \ - cat $ROOT/$src + do_update "$out" \ + cat "$src" else log_skip "Copying $out <- $src: not changed" fi @@ -33,271 +33,185 @@ ## Run check-command on given file, outputting results: # -# check_generic $src $cmd $args... +# check $src $cmd $args... # -function check_generic { - local src=$1; shift - local cmd=$1; shift +function check { + local src="$1"; shift + local cmd="$1"; shift - if cmd_test $cmd -q "$@"; then + if cmd_test "$cmd" -q "$@"; then log_skip "Check $src: OK" else log_error " Check $src: Failed" - indent " " $cmd "$@" + indent " " "$cmd" "$@" exit 1 fi } -## Hosts -## Update hosts from verbatim from input zone data: -# -# copy_hosts $ZONES/$zone $DATA/$base +## Generate forward zone from hosts hosts using pvl.hosts-dns: # -# Writes updated zone to $zone, deps on $base. -function copy_hosts { - local zone=$1 - local base=$2 +# update_hosts_forward out/hosts/$hosts $hosts in/hosts/$hosts +function update_hosts_forward { + local out="$1"; shift + local domain="$1"; shift - # XXX: filenames given directly - local out=$zone - local src=$base - - copy_generic $out $src + if check_update "$out" "$@"; then + log_update "Generating forward hosts zone $out @ $domain <- $@..." + + do_update "$out" $OPT/bin/pvl.hosts-dns \ + --hosts-charset=$CHARSET \ + --forward-zone="$domain" \ + "$@" + else + log_skip "Generating forward hosts $out <- $@: not changed" + fi } -## Generate hosts from input zone data using $BIN/process-zone: -# -# update_hosts $ZONES/$zone $DATA/$base +function update_hosts_dhcp { + local out=$1; shift + local src=$1; shift + + if check_update $out $src "$@"; then + log_update "Generating DHCP hosts $out <- $src..." + + do_update $out $OPT/bin/pvl.hosts-dhcp \ + --hosts-charset=$CHARSET \ + $src "$@" + else + log_skip "Generating DHCP hosts $out <- $src: not changed" + fi +} + +## Generate reverse zone from hosts hosts using pvl.hosts-dns: # -# Writes process-zone'd data to $zone, deps on $base. -function update_hosts { - local zone=$1; shift - local base=$1; shift +# update_hosts_reverse out/hosts/$reverse $reverse in/hosts/$hosts +function update_hosts_reverse { + local out="$1"; shift + local reverse="$1"; shift - if check_update $zone $base; then - log_update "Generating hosts $zone <- $base..." - - do_update $zone \ - $BIN/process-zone $HOSTS_FILE_ARGS $ROOT/$base "$@" + if check_update "$out" "$@"; then + log_update "Generating reverse hosts zone $out <- $@..." + + do_update "$out" $OPT/bin/pvl.hosts-dns \ + --hosts-charset=$CHARSET \ + --reverse-zone="$reverse" \ + "$@" else - log_skip "Generating hosts $zone <- $base: not changed" + log_skip "Generating reverse hosts $out <- $@: not changed" fi } ## Update .serial number: # -# do_update_serial $serial +# do_update_serial .../serials/$zone $serial # -# Shows old/new serial on debug. function do_update_serial { - local serial=$1 + local dst="$1" + local serial="$2" - # read - local old=$(test -e $ROOT/$serial && cat $ROOT/$serial || echo '') - - - cmd $BIN/update-serial $ROOT/$serial - - # read - local new=$(cat $ROOT/$serial) - - debug " $old -> $new" + echo $serial > $dst } -## Generate new serial for zone using $BIN/update-serial, if the zone data has changed: +## Generate new serial for zone using pvl.dns-serial, if the zone data has changed: # -# update_serial $zone $deps... +# update_serial .../serials/$zone $serial $deps... # # Supports SERIAL_FORCE/NOOP. # Updates $SERIALS/$zone.serial. function update_serial { - local zone=$1; shift + local dst="$1"; shift + local serial="$1"; shift + + local old=$(test -e "$dst" && cat "$dst" || echo '') - local serial=$SERIALS/$zone.serial - # test if [ $SERIAL_FORCE ]; then - log_force "Updating $serial: forced" + log_force "Updating $dst: $old <- $serial: forced" - do_update_serial $serial + do_update_serial "$dst" "$serial" - elif ! check_update $serial "$@"; then - log_skip "Updating $serial: not changed" + elif ! check_update "$dst" "$@"; then + log_skip "Updating $dst: $old <- $serial: not changed" elif [ $SERIAL_NOOP ]; then - log_noop "Updating $serial: skipped" + log_noop "Updating $dst: $old <- $serial: skipped" else - log_update "Updating $serial..." + log_update "Updating $dst: $old <- $serial" - do_update_serial $serial + do_update_serial "$dst" "$serial" fi } -## Link serial for zone from given base-zone: +## Generate zone file from source using pvl.dns-zone: # -# link_serial $zone $base -function link_serial { - local zone=$1 - local base=$2 - - local out=$SERIALS/$zone.serial - local tgt=$SERIALS/$base.serial - - link_generic $out $tgt -} - -## Update zone file verbatim from source: -# -# copy_zone $view $zone [$base] -# -# Copies changed $DATA/$base zone data to $ZONES/$view/$zone. -function copy_zone { - local view=$1 - local zone=$2 - local base=${3:-$zone} - - local out=$ZONES/$view/$zone - local src=$DATA/$base - - copy_generic $out $src -} +# update_zone out/zones/$zone in/zones/$zone var/serials/$zone +function update_zone { + local out="$1"; shift + local src="$1"; shift + local serial="$1"; shift + local serial_opt= -## Return the first zone that exists under $DATA/$name.zone -# -# base=$(choose_zone $name...) -function choose_zone { - # look - for name in "$@"; do - if [ $name ] && [ -e $DATA/$name.zone ]; then - echo $name - return 0 - fi - done - - # failed to find - die "Unable to find zone in $DATA/*.zone: $@" -} + if [ -n "$serial" -a -f "$serial" ]; then + serial_opt="--serial=$(cat "$serial")" + elif [ $SERIAL_NOOP ]; then + warn "$out: noop'd serial, omitting" + else + fail "$out: missing serial: $serial" + fi -## Expand zone file from source using $BIN/expand-zone: -# -# update_zone $view $zone [$base] -# -# Updates $ZONES/$view/$zone from $DATA/$base.{zone,serial} using $BIN/expand-zone. -function update_zone { - local view=$1 - local zone=$2 - local base=${3:-$zone} - - # zones <- data+serial file - local out=$ZONES/$view/$zone - local src=$DATA/$base.zone - local serial=$SERIALS/$base.serial - - if check_update $out $src $serial; then + if check_update "$out" "$src" "$serial" "$@"; then log_update "Generating $out <- $src..." - do_update $out \ - $BIN/expand-zone $ROOT/$src \ - --serial $ROOT/$serial \ - --expand zones=$(abspath $ZONES) \ - --expand view=$view + do_update "$out" $OPT/bin/pvl.dns-zone "$src" \ + --include-path=$SRV/var/zones \ + $serial_opt else log_skip "Generating $out <- $src: not changed" fi } -## Link zone file to ues given shared zone. -# -# link_zone $view $zone [$base] -# -# Looks for shared zone at: -# $ZONES/$view/$base -# $ZONES/common/$base -function link_zone { - local view=$1 - local zone=$2 - local base=${3:-$zone} +## Generate dhcp confs from source using pvl.dhcp-conf: +function update_dhcp_conf { + local out="$1" + local src="$2" - local out=$ZONES/$view/$zone - local tgt=$(choose_link $out $ZONES/$view/$base $ZONES/common/$base) - - link_generic $out $tgt -} - -## Link dhcp file directly from data to $DHCP -function link_dhcp_conf { - local conf=$1 - local base=${2:-$conf} - - local out=$DHCP/$conf.conf - local tgt=$(choose_link $out $DHCP/$base.conf $DHCP_DATA/$base.conf) - - link_generic $out $tgt + if check_update "$out" "$src"; then + log_update "Generating $out <- $src..." + + do_update "$out" $OPT/bin/pvl.dhcp-conf "$src" \ + --include-path=$SRV/var/dhcp + else + log_skip "Generating $out <- $src: not changed" + fi } -## Copy dhcp conf from data to $DHCP -function copy_dhcp_conf { - local conf=$1 - local base=${2:-$conf} - - local out=$DHCP/$conf.conf - local src=$DHCP_DATA/$base.conf - - copy_generic $out $src -} - -## Test hosts zone for validity: +## Test hosts zone for validity using pvl.hosts-check: # -# check_hosts $DATA/$hosts --check-exempt ... -# -# Fails if the check fails. +# check_hosts .../hosts function check_hosts { local hosts=$1; shift 1 - - local cmd=($BIN/process-zone $HOSTS_FILE_ARGS $ROOT/$hosts --check-hosts "$@") - - if "${cmd[@]}" -q; then - log_skip "Check $hosts: OK" - else - log_error " Check $hosts: Failed" - - indent " " "${cmd[@]}" - - exit 1 - fi + + # TODO + check $hosts \ + $OPT/bin/pvl.hosts-check $hosts } ## Test zone file for validity using named-checkzone: # -# check_zone $view $zone $origin -# -# Uses the zonefile at $ZONES/$view/$zone, loading it with given initial $ORIGIN. -# Fails if the check fails. +# check_zone ..../$zone $origin function check_zone { - local view=$1 - local zone=$2 - local origin=$3 - - local src=$ZONES/$view/$zone + local zone=$1 + local origin=$2 - local cmd=($NAMED_CHECKZONE $origin $ROOT/$src) + log_check "Checking $zone @ $origin..." - # test - # XXX: checkzone is very specific about the order of arguments, -q must be first - if cmd_test $NAMED_CHECKZONE -q $origin $ROOT/$src; then - log_skip "Check $src ($origin): OK" - else - log_error " Check $src ($origin): Failed:" - - indent " " "${cmd[@]}" - - exit 1 - fi + # checkzone is very specific about the order of arguments, -q must be first + check $zone $NAMED_CHECKZONE $origin $zone } ## Test DHCP configuration for validity using dhcpd -t: @@ -308,13 +222,15 @@ # Fails if the check fails. function check_dhcp { local conf=${1:-$DHCPD_CONF} + + log_check "Checking DHCP $conf..." if [ ! -e $DHCPD ]; then log_warn "check_dhcp: dhcpd not installed, skipping: $conf" return 0 fi - check_generic $conf \ + check $conf \ $DHCPD -cf $conf -t } @@ -328,26 +244,11 @@ check_dhcp $DHCP_DATA/$conf.conf } -## Test DHCP hosts source configuration for invalid fixed-address stanzas: -# -# check_dhcp_hosts $hosts_conf -# -function check_dhcp_hosts { - local hosts=$1 - - # XXX: still too unclear - local src=$hosts #$DHCP_DATA/$hosts.conf +### Deploy +# set by do_reload_zone if zone data has actually been reloaded +RELOAD_ZONES= - # set in do_reload_zones below - if [ $RELOAD_ZONES ]; then - check_generic $src \ - $BIN/check-dhcp-hosts $DHCP_FILE_ARGS $ROOT/$src - else - log_noop "Check $src: skipped; did not reload DNS zones" - fi -} - -# Run rndc reload +## Run rndc reload function do_reload_zones { # run indent " rndc: " \ @@ -432,45 +333,39 @@ fi } -## Perform `hg commit` for $DATA -function do_commit { - local msg=$1 - - [ $LOG_DIFF ] && indent " " hg_diff - - hg_commit "$msg" -} - - -## Commit changes in $DATA to version control: +### Commit +## Commit changes to version control: # -# commit_data +# update_commit .../etc "commit message" # -# Invokes `hg commit` in the $REPO, first showing the diff. -function commit_data { - local repo=$REPO +# Invokes `hg commit`, first showing the diff. +function update_commit { + local repo="$1" local commit_msg="$COMMIT_MSG" - local msg="Commit changes in $repo" + local msg="Commit changes" # operate? if [ $COMMIT_FORCE ]; then - log_force "$msg..." + log_force "$msg: $commit_msg" - do_commit "$commit_msg" + [ $LOG_DIFF ] && indent " " hg_diff $repo - elif ! hg_modified; then - log_skip "$msg: no changes" + hg_commit "$repo" "$commit_msg" + + elif ! hg_modified "$repo"; then + log_warn "$msg: no changes" elif [ $COMMIT_SKIP ]; then log_noop "$msg: skipped" # still show diff, though - [ $LOG_DIFF ] && indent " " hg_diff + [ $LOG_DIFF ] && indent " " hg_diff "$repo" else - log_update "$msg..." + log_update "$msg: $commit_msg" - do_commit "$commit_msg" + [ $LOG_DIFF ] && indent " " hg_diff $repo + + hg_commit "$repo" "$commit_msg" fi } - diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.updates --- a/lib/update.updates Fri May 10 00:05:25 2013 +0300 +++ b/lib/update.updates Sat Dec 21 22:43:38 2013 +0200 @@ -10,7 +10,7 @@ # Returns true if the output file needs to be updated. function check_update { # target - local out=$1; shift + local out="$1"; shift debug "$out" @@ -21,7 +21,7 @@ debug " update: unknown deps" update=y - elif [ ! -e $out ]; then + elif [ ! -e "$out" ]; then debug " update: dest missing" update=y @@ -36,10 +36,10 @@ [ $update ] && continue # check - if [ ! -e $ROOT/$dep ]; then - fail "$out: Missing source: $dep" + if [ ! -e "$dep" ]; then + warn "$out: Missing source: $dep" - elif [ $ROOT/$out -ot $ROOT/$dep ]; then + elif [ "$out" -ot "$dep" ]; then debug " update: $dep" update=y else @@ -60,18 +60,18 @@ # Writes output to a temporary .new file, optionally shows a diff of changes, and commits # the new version to $out (unless noop'd). function do_update { - local out=$1; shift - local tmp=$out.new + local out="$1"; shift + local tmp="$out.new" debug "$out" - cmd "$@" > $ROOT/$tmp + cmd "$@" > "$tmp" # compare - if [ -e $ROOT/$out ] && [ $UPDATE_DIFF ]; then + if [ -e "$out" ] && [ $UPDATE_DIFF ]; then debug " changes:" # terse - indent " " diff --unified=1 $ROOT/$out $ROOT/$tmp || true + indent " " diff --unified=1 "$out" "$tmp" || true fi # deploy @@ -79,33 +79,15 @@ # cleanup debug " no-op" - cmd rm $ROOT/$tmp + cmd rm "$tmp" else # commit debug " deploy" - cmd mv $ROOT/$tmp $ROOT/$out + cmd mv "$tmp" "$out" fi } -## Look for a link target: -# -# find_link $lnk $tgt... -# -# Outputs the first given target to exist, skipping any that are the same as the given $lnk. -# If no $tgt matches, outputs the last one, or '-'. -function choose_link { - local lnk=$1; shift - local tgt=- - - for tgt in "$@"; do - [ $tgt != $out ] && [ -e $ROOT/$tgt ] && break - done - - echo $tgt -} - - ## Compare symlink to target: # # check_link $lnk $tgt && do_link $lnk $tgt || ... @@ -113,12 +95,12 @@ # Tests if the symlink exists, and the target matches. # Fails if the target does not exist. function check_link { - local lnk=$1 - local tgt=$2 + local lnk="$1" + local tgt="$2" - [ ! -e $ROOT/$tgt ] && fail "$tgt: target does not exist" + [ ! -e "$tgt" ] && fail "$tgt: target does not exist" - [ ! -e $ROOT/$lnk ] || [ $(readlink $ROOT/$lnk) != $ROOT/$tgt ] + [ ! -e "$lnk" ] || [ $(readlink "$lnk") != "$tgt" ] } ## Update symlink to point to target: @@ -126,9 +108,41 @@ # do_link $lnk $tgt # function do_link { - local lnk=$1 - local tgt=$2 + local lnk="$1" + local tgt="$2" - cmd ln -sf $ROOT/$tgt $ROOT/$lnk + cmd ln -sf "$tgt" "$lnk" } +## Read include paths from file +function read_zone_includes { + cmd sed -n -E 's/^\$INCLUDE\s+"(.+)"/\1/p' "$@" +} + +## (cached) include paths for zone file +function zone_includes { + local cache="$1" + local src="$2" + local prefix="${3:-}" + + if [ ! -e "$cache" -o "$cache" -ot "$src" ]; then + read_zone_includes "$src" > "$cache" + fi + + while read include; do + echo -n "$prefix$include " + done < "$cache" +} + +## Search for prefix-matching includes in zone file +function zone_includes_grep { + local cache="$1" + local src="$2" + local prefix="$3" + + for include in $(zone_includes $cache $src); do + if [ "${include#$prefix}" != "$include" ]; then + echo -n " ${include#$prefix}" + fi + done +} diff -r b58236f9ea7b -r bfdf1633b2a1 lib/update.utils --- a/lib/update.utils Fri May 10 00:05:25 2013 +0300 +++ b/lib/update.utils Sat Dec 21 22:43:38 2013 +0200 @@ -1,9 +1,7 @@ #!/bin/bash -# vim: set ft=sh : # # Utility functions - ### Command execution ## Execute command, possibly logging its execution. # @@ -13,10 +11,9 @@ function cmd { log_cmd "$@" - "$@" || die "Failed" + "$@" || die "Failed: $@" } -### Command execution ## Execute command as a test, logging its execution at log_cmd # # cmd_test $cmd... && ... || ... @@ -32,11 +29,8 @@ # indent " " $cmd... # # Output is kept on stdout, exit status is that of the given command. -# Also logs the executed command at log_cmd level.. function indent () { - local indent=$1; shift - - log_cmd "$@" + local indent="$1"; shift "$@" | sed "s/^/$indent/" @@ -45,24 +39,25 @@ ### FS utils -# Create dir in $ROOT if not exists. +# Create dir if not exists. function ensure_dir { - local dir=$1 + local dir="$1" - if [ ! -d $ROOT/$dir ]; then + if [ ! -d "$dir" ]; then log_warn "Creating output dir: $dir" - cmd mkdir $ROOT/$dir + cmd mkdir "$dir" fi } -## Output absolute path from $ROOT: +## Output absolute path # # abspath $path # +# XXX: improve...? function abspath () { - local path=$1 + local path="$1" - echo "$ROOT/$path" + echo "$SRV/$path" } ## List names of files in dir: @@ -70,11 +65,14 @@ # list_files $dir $glob # function list_files { - local dir=$1 - local glob=$2 + local dir="$1" + local glob="${2:-*}" local name= for file in $dir/$glob; do + # only files + [ -f "$file" ] || continue + # strip prefix name=${file#$dir/} name=${name%$glob} @@ -83,50 +81,18 @@ done } -### HG wrappers -# Run `hg ...` within $REPO. -function hg { - local repo=$REPO +## List names of dirs in dir: +function list_dirs { + local dir="$1" - cmd $HG -R $ROOT/$repo "${HG_ARGS[@]}" "$@" -} + for file in $dir/*; do + [ -d "$file" ] || continue -# Does the repo have local modifications? -function hg_modified { - hg id | grep -q '+' + echo -n "${file#$dir/} " + done } -# Output possible -u flag for commit. -function hg_user { - if [ ${SUDO_USER:-} ]; then - echo '-u' "$SUDO_USER" - - elif [ $HOME ] && [ -e $HOME/.hgrc ]; then - debug "using .hgrc user" - echo '' - - else - echo '-u' "$USER" - fi +## Get current unix (utc) timestamp +function unix_time { + date +'%s' } - -# Show changes in repo -function hg_diff { - # just stat hidden files, but show the rest - hg diff --stat -I "$REPO/$REPO_HIDE" - hg diff -X "$REPO/$REPO_HIDE" -} - -## Commit changes in repo, with given message: -# -# hg_commit $msg -# -function hg_commit { - local msg=$1 - local user_opt=$(hg_user) - - debug "$user_opt: $msg" - hg commit $user_opt -m "$msg" -} - -