--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/expand-zone Thu Mar 15 16:08:42 2012 +0200
@@ -0,0 +1,230 @@
+#!/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")
+
+ parser.add_option('--update-serial', action='store_true',
+ help="Update serial in 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, update=False) :
+ """
+ Update/process new serial number from given file, based on date.
+
+ Returns the new serial as a string.
+ """
+
+ DATE_FMT = '%Y%m%d'
+ DATE_LEN = 8
+
+ SERIAL_FMT = "{date:8}{count:02}"
+ SERIAL_LEN = 10
+
+ if os.path.exists(path) :
+ # read current
+ serial = open(path).read().strip()
+
+ assert len(serial) == SERIAL_LEN
+
+ old_serial = int(serial)
+
+ old_date = datetime.strptime(serial[:DATE_LEN], DATE_FMT).date()
+ old_count = int(serial[DATE_LEN:])
+
+ else :
+ log.warn("given .serial does not exist, assuming from today: %s", path)
+ old_serial = old_date = old_count = None
+
+ if update :
+ # update
+ today = datetime.now().date()
+
+ if not old_serial :
+ # fresh start
+ date = today
+ count = 1
+
+ log.info("Starting with fresh serial: %s:%s", date, count)
+
+ elif old_date < today :
+ # update date
+ date = today
+ count = 1
+
+ log.info("Updating to today: %s -> %s", old_date, date)
+
+ elif old_date == today :
+ # keep date, update count
+ date = old_date
+ count = old_count + 1
+
+ if count > 99 :
+ raise Exception("Serial update rollover: %s, %s", date, count)
+
+ log.info("Updating today's count: %s, %s", date, count)
+
+ else :
+ raise Exception("Invalid serial: %s:%s", old_date, old_count)
+
+ else :
+ date = old_date
+ count = old_count
+
+ serial = SERIAL_FMT.format(date=date.strftime(DATE_FMT), count=count)
+
+ open(path, 'w').write(serial)
+
+ return serial
+
+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, update=options.update_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))