bin/expand-zone
changeset 1 ea30c9b619b8
child 5 1eb454630f47
equal deleted inserted replaced
0:f0ac5a96fb19 1:ea30c9b619b8
       
     1 #!/usr/bin/env python
       
     2 # vim: set ft=python :
       
     3 
       
     4 """
       
     5     Process zonefiles with template expansions.
       
     6 """
       
     7 
       
     8 __version__ = '0.0.1-dev'
       
     9 
       
    10 import optparse
       
    11 import codecs
       
    12 import os.path
       
    13 from datetime import datetime
       
    14 import logging
       
    15 
       
    16 log = logging.getLogger()
       
    17 
       
    18 # command-line options, global state
       
    19 options = None
       
    20 
       
    21 def parse_options (argv) :
       
    22     """
       
    23         Parse command-line arguments.
       
    24     """
       
    25 
       
    26     parser = optparse.OptionParser(
       
    27             prog        = argv[0],
       
    28             usage       = '%prog: [options]',
       
    29             version     = __version__,
       
    30 
       
    31             # module docstring
       
    32             description = __doc__,
       
    33     )
       
    34 
       
    35     # logging
       
    36     general = optparse.OptionGroup(parser, "General Options")
       
    37 
       
    38     general.add_option('-q', '--quiet',     dest='loglevel', action='store_const', const=logging.ERROR, help="Less output")
       
    39     general.add_option('-v', '--verbose',   dest='loglevel', action='store_const', const=logging.INFO,  help="More output")
       
    40     general.add_option('-D', '--debug',     dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output")
       
    41 
       
    42     parser.add_option_group(general)
       
    43 
       
    44     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
       
    45             help="Encoding used for input files")
       
    46 
       
    47     parser.add_option('-o', '--output',         metavar='FILE',     default='-',
       
    48             help="Write to output file; default stdout")
       
    49 
       
    50     parser.add_option('--output-charset',       metavar='CHARSET',  default='utf-8', 
       
    51             help="Encoding used for output files")
       
    52 
       
    53     parser.add_option('--expand',               metavar='NAME=VALUE', action='append',
       
    54             help="Expand given template variable in zone")
       
    55 
       
    56     parser.add_option('--serial',               metavar='FILE',
       
    57             help="Read/expand serial from given .serial file")
       
    58 
       
    59     parser.add_option('--update-serial',        action='store_true',
       
    60             help="Update serial in given .serial file")
       
    61 
       
    62     # defaults
       
    63     parser.set_defaults(
       
    64         loglevel            = logging.WARN,
       
    65         expand              = [],
       
    66     )
       
    67     
       
    68     # parse
       
    69     options, args = parser.parse_args(argv[1:])
       
    70 
       
    71     # configure
       
    72     logging.basicConfig(
       
    73         format  = '%(processName)s: %(name)s: %(levelname)s %(funcName)s : %(message)s',
       
    74         level   = options.loglevel,
       
    75     )
       
    76 
       
    77     return options, args
       
    78 
       
    79 def process_file (file, expansions) :
       
    80     """
       
    81         Process file, expanding lines.
       
    82     """
       
    83 
       
    84     for line in file :
       
    85         line = line.format(**expansions)
       
    86 
       
    87         yield line
       
    88 
       
    89 def write_lines (file, lines, suffix='\n') :
       
    90     for line in lines :
       
    91         file.write(line + suffix)
       
    92 
       
    93 def open_file (path, mode, charset) :
       
    94     """
       
    95         Open unicode-enabled file from path, with - using stdio.
       
    96     """
       
    97 
       
    98     if path == '-' :
       
    99         # use stdin/out based on mode
       
   100         stream, func = {
       
   101             'r':    (sys.stdin, codecs.getreader),
       
   102             'w':    (sys.stdout, codecs.getwriter),
       
   103         }[mode[0]]
       
   104 
       
   105         # wrap
       
   106         return func(charset)(stream)
       
   107 
       
   108     else :
       
   109         # open
       
   110         return codecs.open(path, mode, charset)
       
   111 
       
   112 def process_serial (path, update=False) :
       
   113     """
       
   114         Update/process new serial number from given file, based on date.
       
   115 
       
   116         Returns the new serial as a string.
       
   117     """
       
   118 
       
   119     DATE_FMT = '%Y%m%d'
       
   120     DATE_LEN = 8
       
   121 
       
   122     SERIAL_FMT = "{date:8}{count:02}"
       
   123     SERIAL_LEN = 10
       
   124 
       
   125     if os.path.exists(path) :
       
   126         # read current
       
   127         serial = open(path).read().strip()
       
   128 
       
   129         assert len(serial) == SERIAL_LEN
       
   130 
       
   131         old_serial = int(serial)
       
   132 
       
   133         old_date = datetime.strptime(serial[:DATE_LEN], DATE_FMT).date()
       
   134         old_count = int(serial[DATE_LEN:])
       
   135         
       
   136     else :
       
   137         log.warn("given .serial does not exist, assuming from today: %s", path)
       
   138         old_serial = old_date = old_count = None
       
   139 
       
   140     if update :
       
   141         # update
       
   142         today = datetime.now().date()
       
   143 
       
   144         if not old_serial :
       
   145             # fresh start
       
   146             date = today
       
   147             count = 1
       
   148             
       
   149             log.info("Starting with fresh serial: %s:%s", date, count)
       
   150         
       
   151         elif old_date < today :
       
   152             # update date
       
   153             date = today
       
   154             count = 1
       
   155             
       
   156             log.info("Updating to today: %s -> %s", old_date, date)
       
   157 
       
   158         elif old_date == today :
       
   159             # keep date, update count
       
   160             date = old_date
       
   161             count = old_count + 1
       
   162 
       
   163             if count > 99 :
       
   164                 raise Exception("Serial update rollover: %s, %s", date, count)
       
   165 
       
   166             log.info("Updating today's count: %s, %s", date, count)
       
   167 
       
   168         else :
       
   169             raise Exception("Invalid serial: %s:%s", old_date, old_count)
       
   170 
       
   171     else :
       
   172         date = old_date
       
   173         count = old_count
       
   174 
       
   175     serial = SERIAL_FMT.format(date=date.strftime(DATE_FMT), count=count)
       
   176 
       
   177     open(path, 'w').write(serial)
       
   178 
       
   179     return serial
       
   180 
       
   181 def parse_expand (expand) :
       
   182     """
       
   183         Parse an --expand foo=bar to (key, value)
       
   184     """
       
   185 
       
   186     key, value = expand.split('=', 1)
       
   187 
       
   188     return key, value
       
   189 
       
   190 def main (argv) :
       
   191     global options
       
   192     
       
   193     options, args = parse_options(argv)
       
   194 
       
   195     # expands
       
   196     expand = dict(parse_expand(expand) for expand in options.expand)
       
   197 
       
   198     # serial?
       
   199     if options.serial :
       
   200         serial = process_serial(options.serial, update=options.update_serial)
       
   201 
       
   202         expand['serial'] = serial
       
   203 
       
   204     # input
       
   205     if args :
       
   206         # open files
       
   207         input_files = [open_file(path, 'r', options.input_charset) for path in args]
       
   208 
       
   209     else :
       
   210         # default to stdout
       
   211         input_files = [open_file('-', 'r', options.input_charset)]
       
   212    
       
   213     # process
       
   214     lines = []
       
   215 
       
   216     for file in input_files :
       
   217         log.info("Reading zone: %s", file)
       
   218 
       
   219         lines += list(process_file(file, expand))
       
   220 
       
   221     # output
       
   222     output = open_file(options.output, 'w', options.output_charset)
       
   223     write_lines(output, lines, suffix='')
       
   224 
       
   225     return 0
       
   226 
       
   227 if __name__ == '__main__':
       
   228     import sys
       
   229 
       
   230     sys.exit(main(sys.argv))