bin/process-zone
branchdns-new
changeset 604 9a23fca9167a
parent 603 b58236f9ea7b
child 605 26a307558602
equal deleted inserted replaced
603:b58236f9ea7b 604:9a23fca9167a
     1 #!/usr/bin/env python
       
     2 
       
     3 """
       
     4     Process zonefiles.
       
     5 """
       
     6 
       
     7 __version__ = '0.0.1-dev'
       
     8 
       
     9 import optparse
       
    10 import codecs
       
    11 from datetime import datetime
       
    12 import logging
       
    13 
       
    14 import ipaddr
       
    15 
       
    16 log = logging.getLogger('main')
       
    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     prog = argv[0]
       
    27 
       
    28     parser = optparse.OptionParser(
       
    29             prog        = prog,
       
    30             usage       = '%prog: [options]',
       
    31             version     = __version__,
       
    32 
       
    33             # module docstring
       
    34             description = __doc__,
       
    35     )
       
    36 
       
    37     # logging
       
    38     general = optparse.OptionGroup(parser, "General Options")
       
    39 
       
    40     general.add_option('-q', '--quiet',     dest='loglevel', action='store_const', const=logging.ERROR, help="Less output")
       
    41     general.add_option('-v', '--verbose',   dest='loglevel', action='store_const', const=logging.INFO,  help="More output")
       
    42     general.add_option('-D', '--debug',     dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output")
       
    43 
       
    44     parser.add_option_group(general)
       
    45 
       
    46     # input/output
       
    47     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
       
    48             help="Encoding used for input files")
       
    49 
       
    50     parser.add_option('-o', '--output',         metavar='FILE',     default='-',
       
    51             help="Write to output file; default stdout")
       
    52 
       
    53     parser.add_option('--output-charset',       metavar='CHARSET',  default='utf-8', 
       
    54             help="Encoding used for output files")
       
    55 
       
    56     # check stage
       
    57     parser.add_option('--check-hosts',          action='store_true',
       
    58             help="Check that host/IPs are unique. Use --quiet to silence warnings, and test exit status")
       
    59 
       
    60     parser.add_option('--check-exempt',         metavar='HOST', action='append',
       
    61             help="Allow given names to have multiple records")
       
    62 
       
    63     # meta stage
       
    64     parser.add_option('--meta-zone',            action='store_true',
       
    65             help="Generate host metadata zone; requires --input-line-date")
       
    66 
       
    67     parser.add_option('--meta-ignore',          metavar='HOST', action='append',
       
    68             help="Ignore given hostnames in metadata output")
       
    69 
       
    70     parser.add_option('--input-line-date',      action='store_true',
       
    71             help="Parse timestamp prefix from each input line (e.g. `hg blame | ...`)")
       
    72 
       
    73     # forward stage
       
    74     parser.add_option('--forward-zone',         action='store_true', 
       
    75             help="Generate forward zone")
       
    76 
       
    77     parser.add_option('--forward-txt',          action='store_true',
       
    78             help="Generate TXT records for forward zone")
       
    79 
       
    80     parser.add_option('--forward-mx',           metavar='MX',
       
    81             help="Generate MX records for forward zone")
       
    82 
       
    83     # reverse stage
       
    84     parser.add_option('--reverse-domain',       metavar='DOMAIN',
       
    85             help="Domain to use for hosts in reverse zone")
       
    86 
       
    87     parser.add_option('--reverse-zone',         metavar='NET',
       
    88             help="Generate forward zone for given subnet (x.z.y | a:b:c:d)")
       
    89 
       
    90     # 
       
    91     parser.add_option('--doctest',              action='store_true',
       
    92             help="Run module doctests")
       
    93 
       
    94     # defaults
       
    95     parser.set_defaults(
       
    96         loglevel            = logging.WARN,
       
    97 
       
    98         # XXX: combine
       
    99         check_exempt        = [],
       
   100         meta_ignore         = [],
       
   101     )
       
   102     
       
   103     # parse
       
   104     options, args = parser.parse_args(argv[1:])
       
   105 
       
   106     # configure
       
   107     logging.basicConfig(
       
   108         format  = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s',
       
   109         level   = options.loglevel,
       
   110     )
       
   111 
       
   112     return options, args
       
   113 
       
   114 class ZoneError (Exception) :
       
   115     pass
       
   116 
       
   117 class ZoneLineError (ZoneError) :
       
   118     """
       
   119         ZoneLine-related error
       
   120     """
       
   121 
       
   122     def __init__ (self, line, msg, *args, **kwargs) :
       
   123         super(ZoneLineError, self).__init__("%s: %s" % (line, msg.format(*args, **kwargs)))
       
   124 
       
   125 class ZoneLine (object) :
       
   126     """
       
   127         A line in a zonefile.
       
   128     """
       
   129 
       
   130     file = None
       
   131     lineno = None
       
   132 
       
   133     # data
       
   134     indent = None # was the line indented?
       
   135     data = None
       
   136     parts = None # split line fields
       
   137 
       
   138     # optional
       
   139     timestamp = None
       
   140     comment = None
       
   141 
       
   142     PARSE_DATETIME_FORMAT = '%Y-%m-%d'
       
   143 
       
   144     @classmethod
       
   145     def parse (cls, file, lineno, line, line_timestamp_prefix=False) :
       
   146         """
       
   147             Parse out given line and build.
       
   148         """
       
   149 
       
   150         log.debug("parse: %s:%d: %s", file, lineno, line)
       
   151 
       
   152         ts = None
       
   153 
       
   154         if line_timestamp_prefix :
       
   155             if ': ' not in line :
       
   156                 raise ZoneError("%s:%d: Missing timestamp prefix: %s" % (file, lineno, line))
       
   157 
       
   158             # split prefix
       
   159             prefix, line = line.split(': ', 1)
       
   160 
       
   161             # parse it out
       
   162             ts = datetime.strptime(prefix, cls.PARSE_DATETIME_FORMAT)
       
   163 
       
   164             log.debug("  ts=%r", ts)
       
   165 
       
   166         # was line indented?
       
   167         indent = line.startswith(' ') or line.startswith('\t')
       
   168         
       
   169         # strip
       
   170         line = line.strip()
       
   171         
       
   172         log.debug("  indent=%r, line=%r", indent, line)
       
   173 
       
   174         # parse comment out?
       
   175         if ';' in line :
       
   176             line, comment = line.split(';', 1)
       
   177 
       
   178             line = line.strip()
       
   179             comment = comment.strip()
       
   180 
       
   181         else :
       
   182             line = line.strip()
       
   183             comment = None
       
   184         
       
   185         log.debug("  line=%r, comment=%r", line, comment)
       
   186 
       
   187         # parse fields
       
   188         if '"' in line :
       
   189             pre, data, post = line.split('"', 2)
       
   190             parts = pre.split() + [data] + post.split()
       
   191            
       
   192         else :
       
   193             parts = line.split()
       
   194 
       
   195         log.debug("  parts=%r", parts)
       
   196 
       
   197         # build
       
   198         return cls(file, lineno, indent, line, parts, timestamp=ts, comment=comment)
       
   199 
       
   200     def __init__ (self, file, lineno, indent, data, parts, timestamp=None, comment=None) :
       
   201         self.file = file
       
   202         self.lineno = lineno
       
   203 
       
   204         self.indent = indent
       
   205         self.data = data
       
   206         self.parts = parts
       
   207 
       
   208         self.timestamp = timestamp
       
   209         self.comment = comment
       
   210 
       
   211     def __str__ (self) :
       
   212         return "{file}:{lineno}".format(file=self.file, lineno=self.lineno)
       
   213 
       
   214 class ZoneRecord (object) :
       
   215     """
       
   216         A record from a zonefile.
       
   217     """
       
   218 
       
   219     # the underlying line
       
   220     line = None
       
   221 
       
   222     # possible $ORIGIN context
       
   223     origin = None
       
   224 
       
   225     # record fields
       
   226     name = None
       
   227     type = None
       
   228 
       
   229     # list of data fields
       
   230     data = None
       
   231 
       
   232     # optional
       
   233     ttl = None
       
   234     cls = None
       
   235 
       
   236     @classmethod
       
   237     def parse (cls, line, parts=None, origin=None) :
       
   238         """
       
   239             Parse from ZoneLine. Returns None if there is no record on the line..
       
   240         """
       
   241 
       
   242         if parts is None :
       
   243             parts = list(line.parts)
       
   244 
       
   245         if not parts :
       
   246             # skip
       
   247             return
       
   248         
       
   249         # XXX: indented lines keep name from previous record
       
   250         if line.indent :
       
   251             name = None
       
   252 
       
   253         else :
       
   254             name = parts.pop(0)
       
   255         
       
   256         log.debug("  name=%r, origin=%r", name, origin)
       
   257 
       
   258         if len(parts) < 2 :
       
   259             raise ZoneLineError(line, "Too few parts to parse: {0!r}", line.data)
       
   260 
       
   261         # parse ttl/cls/type
       
   262         ttl = _cls = None
       
   263 
       
   264         if parts and parts[0][0].isdigit() :
       
   265             ttl = parts.pop(0)
       
   266 
       
   267         if parts and parts[0].upper() in ('IN', 'CH') :
       
   268             _cls = parts.pop(0)
       
   269 
       
   270         # always have type
       
   271         type = parts.pop(0)
       
   272 
       
   273         # remaining parts are data
       
   274         data = parts
       
   275 
       
   276         log.debug("  ttl=%r, cls=%r, type=%r, data=%r", ttl, _cls, type, data)
       
   277 
       
   278         return cls(name, type, data,
       
   279             origin  = origin,
       
   280             ttl     = ttl,
       
   281             cls     = _cls,
       
   282             line    = line,
       
   283         )
       
   284 
       
   285     def __init__ (self, name, type, data, origin=None, ttl=None, cls=None, line=None, comment=None) :
       
   286         self.name = name
       
   287         self.type = type
       
   288         self.data = data
       
   289         
       
   290         self.ttl = ttl
       
   291         self.cls = cls
       
   292         
       
   293         self.origin = origin
       
   294         self.line = line
       
   295 
       
   296         # XXX: within line
       
   297         self._comment = comment
       
   298 
       
   299     def build_line (self) :
       
   300         """
       
   301             Construct a zonefile-format line..."
       
   302         """
       
   303 
       
   304         # XXX: comment?
       
   305         if self._comment :
       
   306             comment = '\t; ' + self._comment
       
   307         else :
       
   308             comment = ''
       
   309             
       
   310         return u"{name:25} {ttl:4} {cls:2} {type:5} {data}{comment}".format(
       
   311                 name    = self.name or '',
       
   312                 ttl     = self.ttl or '',
       
   313                 cls     = self.cls or '',
       
   314                 type    = self.type,
       
   315                 data    = ' '.join(unicode(data) for data in self.data),
       
   316                 comment = comment,
       
   317         )
       
   318 
       
   319     def __str__ (self) :
       
   320         return ' '.join((self.name or '', self.type, ' '.join(self.data)))
       
   321 
       
   322 class TXTRecord (ZoneRecord) :
       
   323     """
       
   324         TXT record.
       
   325     """
       
   326 
       
   327     def __init__ (self, name, text, **opts) :
       
   328         return super(TXTRecord, self).__init__(name, 'TXT', 
       
   329             [u'"{0}"'.format(text.replace('"', '\\"'))], 
       
   330             **opts
       
   331         )
       
   332 
       
   333 class OffsetValue (object) :
       
   334     def __init__ (self, value) :
       
   335         self.value = value
       
   336 
       
   337     def __getitem__ (self, offset) :
       
   338         value = self.value + offset
       
   339 
       
   340         #log.debug("OffsetValue: %d[%d] -> %d", self.value, offset, value)
       
   341 
       
   342         return value
       
   343 
       
   344 def parse_generate_field (line, field) :
       
   345     """
       
   346         Parse a $GENERATE lhs/rhs field:
       
   347             $
       
   348             ${<offset>[,<width>[,<base>]]}
       
   349             \$
       
   350             $$
       
   351 
       
   352         Returns a wrapper that builds the field-value when called with the index.
       
   353         
       
   354         >>> parse_generate_field(None, "foo")(1)
       
   355         'foo'
       
   356         >>> parse_generate_field(None, "foo-$")(1)
       
   357         'foo-1'
       
   358         >>> parse_generate_field(None, "foo-$$")(1)
       
   359         'foo-$'
       
   360         >>> parse_generate_field(None, "\$")(1)
       
   361         '$'
       
   362         >>> parse_generate_field(None, "10.0.0.${100}")(1)
       
   363         '10.0.0.101'
       
   364         >>> parse_generate_field(None, "foo-${0,2,d}")(1)
       
   365         'foo-01'
       
   366 
       
   367     """
       
   368 
       
   369     input = field
       
   370     expr = []
       
   371 
       
   372     while '$' in field :
       
   373         # defaults
       
   374         offset = 0
       
   375         width = 0
       
   376         base = 'd'
       
   377         escape = False
       
   378 
       
   379         # different forms
       
   380         if '${' in field :
       
   381             pre, body = field.split('${', 1)
       
   382             body, post = body.split('}', 1)
       
   383 
       
   384             # parse body
       
   385             parts = body.split(',')
       
   386 
       
   387             # offset
       
   388             offset = int(parts.pop(0))
       
   389 
       
   390             # width
       
   391             if parts :
       
   392                 width = int(parts.pop(0))
       
   393 
       
   394             # base
       
   395             if parts :
       
   396                 base = parts.pop(0)
       
   397             
       
   398             if parts:
       
   399                 # fail
       
   400                 raise ZoneLineError(line, "extra data in ${...} body: {0!r}", parts)
       
   401 
       
   402         elif '$$' in field :
       
   403             pre, post = field.split('$$', 1)
       
   404             escape = True
       
   405 
       
   406         elif '\\$' in field :
       
   407             pre, post = field.split('\\$', 1)
       
   408             escape = True
       
   409 
       
   410         else :
       
   411             pre, post = field.split('$', 1)
       
   412         
       
   413         expr.append(pre)
       
   414 
       
   415         if escape :
       
   416             expr.append('$')
       
   417 
       
   418         else :
       
   419             # meta-format
       
   420             fmt = '{value[%d]:0%d%s}' % (offset, width, base)
       
   421 
       
   422             log.debug("field=%r -> pre=%r, fmt=%r, post=%r", field, expr, fmt, post)
       
   423 
       
   424             expr.append(fmt)
       
   425 
       
   426         field = post
       
   427 
       
   428     # final
       
   429     if field :
       
   430         expr.append(field)
       
   431     
       
   432     # combine
       
   433     expr = ''.join(expr)
       
   434 
       
   435     log.debug("%s: %s", input, expr)
       
   436 
       
   437     # processed
       
   438     def value_func (value) :
       
   439         # magic wrapper to implement offsets
       
   440         return expr.format(value=OffsetValue(value))
       
   441     
       
   442     return value_func
       
   443 
       
   444 def process_generate (line, origin, parts) :
       
   445     """
       
   446         Process a 
       
   447             $GENERATE <start>-<stop>[/<step>] lhs [ttl] [class] type rhs [comment]
       
   448         directive into a series of ZoneResource's.
       
   449     """
       
   450 
       
   451     range = parts.pop(0)
       
   452 
       
   453     # parse range
       
   454     if '/' in range :
       
   455         range, step = range.split('/')
       
   456         step = int(step)
       
   457     else :
       
   458         step = 1
       
   459 
       
   460     start, stop = range.split('-')
       
   461     start = int(start)
       
   462     stop = int(stop)
       
   463 
       
   464     log.debug("  range: start=%r, stop=%r, step=%r", start, stop, step)
       
   465 
       
   466     # inclusive
       
   467     range = xrange(start, stop + 1, step)
       
   468 
       
   469     lhs_func = parse_generate_field(line, parts.pop(0))
       
   470     rhs_func = parse_generate_field(line, parts.pop(-1))
       
   471     body = parts
       
   472 
       
   473     for i in range :
       
   474         # build
       
   475         parts = [lhs_func(i)] + body + [rhs_func(i)]
       
   476 
       
   477         log.debug(" %03d: %r", i, parts)
       
   478 
       
   479         # parse
       
   480         yield ZoneRecord.parse(line, parts=parts, origin=origin)
       
   481 
       
   482 def parse_zone_records (file, origin=None, **opts) :
       
   483     """
       
   484         Parse ZoneRecord items from the given zonefile, ignoring non-record lines.
       
   485     """
       
   486 
       
   487     ttl = None
       
   488 
       
   489     skip_multiline = False
       
   490     
       
   491     for lineno, raw_line in enumerate(file) :
       
   492         # parse comment
       
   493         if ';' in raw_line :
       
   494             line, comment = raw_line.split(';', 1)
       
   495         else :
       
   496             line = raw_line
       
   497             comment = None
       
   498 
       
   499         # XXX: handle multi-line statements...
       
   500         # start
       
   501         if '(' in line :
       
   502             skip_multiline = True
       
   503             
       
   504             log.warn("%s:%d: Start of multi-line statement: %s", file.name, lineno, raw_line)
       
   505 
       
   506         # end?
       
   507         if ')' in line :
       
   508             skip_multiline = False
       
   509             
       
   510             log.warn("%s:%d: End of multi-line statement: %s", file.name, lineno, raw_line)
       
   511             
       
   512             continue
       
   513 
       
   514         elif skip_multiline :
       
   515             log.warn("%s:%d: Multi-line statement: %s", file.name, lineno, raw_line)
       
   516 
       
   517             continue
       
   518         
       
   519         # parse
       
   520         line = ZoneLine.parse(file.name, lineno, raw_line, **opts)
       
   521 
       
   522         if not line.data :
       
   523             log.debug("%s: skip empty line: %s", line, raw_line)
       
   524 
       
   525             continue
       
   526 
       
   527         elif line.data.startswith('$') :
       
   528             # control record
       
   529             type = line.parts[0]
       
   530 
       
   531             if type == '$ORIGIN':
       
   532                 # update
       
   533                 origin = line.parts[1]
       
   534                 
       
   535                 log.info("%s: origin: %s", line, origin)
       
   536             
       
   537             elif type == '$GENERATE':
       
   538                 # process...
       
   539                 log.info("%s: generate: %s", line, line.parts)
       
   540 
       
   541                 for record in process_generate(line, origin, line.parts[1:]) :
       
   542                     yield record
       
   543 
       
   544             else :
       
   545                 log.warning("%s: skip control record: %s", line, line.data)
       
   546             
       
   547             # XXX: passthrough!
       
   548             continue
       
   549 
       
   550         # normal record?
       
   551         record = ZoneRecord.parse(line, origin=origin)
       
   552 
       
   553         if record :
       
   554             yield record
       
   555 
       
   556         else :
       
   557             # unknown
       
   558             log.warning("%s: skip unknown line: %s", line, line.data)
       
   559 
       
   560 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) :
       
   561     """
       
   562         Parse host/IP pairs from the zone, and verify that they are unique.
       
   563 
       
   564         As an exception, names listed in the given whitelist may have multiple IPs.
       
   565     """
       
   566 
       
   567     by_name = {}
       
   568     by_ip = {}
       
   569 
       
   570     fail = None
       
   571 
       
   572     last_name = None
       
   573 
       
   574     for r in zone :
       
   575         name = r.name or last_name
       
   576 
       
   577         name = (r.origin, name)
       
   578 
       
   579         # name
       
   580         if r.type not in whitelist_types :
       
   581             if name not in by_name :
       
   582                 by_name[name] = r
       
   583 
       
   584             elif r.name in whitelist :
       
   585                 log.debug("Duplicate whitelist entry: %s", r)
       
   586 
       
   587             else :
       
   588                 # fail!
       
   589                 log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name])
       
   590                 fail = True
       
   591 
       
   592         # ip
       
   593         if r.type == 'A' :
       
   594             ip, = r.data
       
   595 
       
   596             if ip not in by_ip :
       
   597                 by_ip[ip] = r
       
   598 
       
   599             else :
       
   600                 # fail!
       
   601                 log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip])
       
   602                 fail = True
       
   603 
       
   604     return fail
       
   605 
       
   606 def process_zone_forwards (zone, txt=False, mx=False) :
       
   607     """
       
   608         Process zone data -> forward zone data.
       
   609     """
       
   610 
       
   611     for r in zone :
       
   612         yield r
       
   613 
       
   614         if r.type == 'A' :
       
   615             if txt :
       
   616                 # comment?
       
   617                 comment = r.line.comment
       
   618 
       
   619                 if comment :
       
   620                     yield TXTRecord(None, comment, ttl=r.ttl)
       
   621 
       
   622            
       
   623             # XXX: RP, do we need it?
       
   624 
       
   625             if mx :
       
   626                 # XXX: is this a good idea?
       
   627                 yield ZoneRecord(None, 'MX', [10, mx], ttl=r.ttl)
       
   628 
       
   629 def process_zone_meta (zone, ignore=None) :
       
   630     """
       
   631         Process zone metadata -> output.
       
   632     """
       
   633     
       
   634     TIMESTAMP_FORMAT='%Y/%m/%d'
       
   635     
       
   636     for r in zone :
       
   637         if ignore and r.name in ignore :
       
   638             # skip
       
   639             log.debug("Ignore record: %s", r)
       
   640             continue
       
   641 
       
   642         # for hosts..
       
   643         if r.type == 'A' :
       
   644             # timestamp?
       
   645             timestamp = r.line.timestamp
       
   646 
       
   647             if timestamp :
       
   648                 yield TXTRecord(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl)
       
   649      
       
   650 def reverse_ipv4 (ip) :
       
   651     """
       
   652         Return in-addr.arpa reverse for given IPv4 prefix.
       
   653     """
       
   654 
       
   655     # parse
       
   656     octets = tuple(int(part) for part in ip.split('.'))
       
   657 
       
   658     for octet in octets :
       
   659         assert 0 <= octet <= 255
       
   660 
       
   661     return '.'.join([str(octet) for octet in reversed(octets)] + ['in-addr', 'arpa'])
       
   662 
       
   663 def reverse_ipv6 (ip6) :
       
   664     """
       
   665         Return ip6.arpa reverse for given IPv6 prefix.
       
   666     """
       
   667 
       
   668     parts = [int(part, 16) for part in ip6.split(':')]
       
   669     parts = ['{0:04x}'.format(part) for part in parts]
       
   670     parts = ''.join(parts)
       
   671 
       
   672     return '.'.join(tuple(reversed(parts)) + ( 'ip6', 'arpa'))
       
   673 
       
   674 def fqdn (*parts) :
       
   675     fqdn = '.'.join(parts)
       
   676     
       
   677     # we may be given an fqdn in parts
       
   678     if not fqdn.endswith('.') :
       
   679         fqdn += '.'
       
   680     
       
   681     return fqdn
       
   682 
       
   683 def process_zone_reverse (zone, origin, domain) :
       
   684     """
       
   685         Process zone data -> reverse zone data.
       
   686     """
       
   687 
       
   688     name = None
       
   689 
       
   690     for r in zone :
       
   691         # keep name from previous..
       
   692         if r.name :
       
   693             name = r.name
       
   694 
       
   695         if r.type == 'A' :
       
   696             ip, = r.data
       
   697             ptr = reverse_ipv4(ip)
       
   698 
       
   699         elif r.type == 'AAAA' :
       
   700             ip, = r.data
       
   701             ptr = reverse_ipv6(ip)
       
   702             
       
   703         else :
       
   704             continue
       
   705 
       
   706         # verify
       
   707         if zone and ptr.endswith(origin) :
       
   708             ptr = ptr[:-(len(origin) + 1)]
       
   709 
       
   710         else :
       
   711             log.warning("Reverse does not match zone origin, skipping: (%s) -> %s <-> %s", ip, ptr, origin)
       
   712             continue
       
   713 
       
   714         # domain to use
       
   715         host_domain = r.origin or domain
       
   716         host_fqdn = fqdn(name, host_domain)
       
   717 
       
   718         yield ZoneRecord(ptr, 'PTR', [host_fqdn])
       
   719 
       
   720 def write_zone_records (file, zone) :
       
   721     for r in zone :
       
   722         file.write(r.build_line() + u'\n')
       
   723 
       
   724 def open_file (path, mode, charset) :
       
   725     """
       
   726         Open unicode-enabled file from path, with - using stdio.
       
   727     """
       
   728 
       
   729     if path == '-' :
       
   730         # use stdin/out based on mode
       
   731         stream, func = {
       
   732             'r':    (sys.stdin, codecs.getreader),
       
   733             'w':    (sys.stdout, codecs.getwriter),
       
   734         }[mode[0]]
       
   735 
       
   736         # wrap
       
   737         return func(charset)(stream)
       
   738 
       
   739     else :
       
   740         # open
       
   741         return codecs.open(path, mode, charset)
       
   742 
       
   743 def main (argv) :
       
   744     global options
       
   745     
       
   746     options, args = parse_options(argv)
       
   747 
       
   748     if options.doctest :
       
   749         import doctest
       
   750         fail, total = doctest.testmod()
       
   751         return fail
       
   752 
       
   753     if args :
       
   754         # open files
       
   755         input_files = [open_file(path, 'r', options.input_charset) for path in args]
       
   756 
       
   757     else :
       
   758         # default to stdout
       
   759         input_files = [open_file('-', 'r', options.input_charset)]
       
   760    
       
   761     # process zone data
       
   762     zone = []
       
   763 
       
   764     for file in input_files :
       
   765         log.info("Reading zone: %s", file)
       
   766 
       
   767         zone += list(parse_zone_records(file, 
       
   768             line_timestamp_prefix   = options.input_line_date,
       
   769         ))
       
   770 
       
   771     # check?
       
   772     if options.check_hosts :
       
   773         whitelist = set(options.check_exempt)
       
   774 
       
   775         log.debug("checking hosts; whitelist=%r", whitelist)
       
   776 
       
   777         if check_zone_hosts(zone, whitelist=whitelist) :
       
   778             log.warn("Hosts check failed")
       
   779             return 2
       
   780 
       
   781         else :
       
   782             log.info("Hosts check OK")
       
   783 
       
   784     # output file
       
   785     output = open_file(options.output, 'w', options.output_charset)
       
   786 
       
   787     if options.forward_zone :
       
   788         log.info("Write forward zone: %s", output)
       
   789 
       
   790         zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx))
       
   791 
       
   792     elif options.meta_zone :
       
   793         log.info("Write metadata zone: %s", output)
       
   794 
       
   795         if not options.input_line_date :
       
   796             log.error("--meta-zone requires --input-line-date")
       
   797             return 1
       
   798 
       
   799         zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore)))
       
   800 
       
   801     elif options.reverse_zone :
       
   802         if ':' in options.reverse_zone :
       
   803             # IPv6
       
   804             origin = reverse_ipv6(options.reverse_zone)
       
   805 
       
   806         else :
       
   807             # IPv4
       
   808             origin = reverse_ipv4(options.reverse_zone)
       
   809 
       
   810         domain = options.reverse_domain
       
   811 
       
   812         if not domain :
       
   813             log.error("--reverse-zone requires --reverse-domain")
       
   814             return 1
       
   815 
       
   816         zone = list(process_zone_reverse(zone, origin=origin, domain=domain))
       
   817 
       
   818     elif options.check_hosts :
       
   819         # we only did that, done
       
   820         return 0
       
   821 
       
   822     else :
       
   823         log.warn("Nothing to do")
       
   824         return 1
       
   825 
       
   826     write_zone_records(output, zone)
       
   827 
       
   828     return 0
       
   829 
       
   830 if __name__ == '__main__':
       
   831     import sys
       
   832 
       
   833     sys.exit(main(sys.argv))