bin/pvl.dns-zone
changeset 247 08a63738f2d1
parent 233 c4941645464c
child 249 8dfe61659b18
equal deleted inserted replaced
246:819320b0cf81 247:08a63738f2d1
     1 #!/usr/bin/env python
     1 #!/usr/bin/env python
     2 
     2 
     3 """
     3 """
     4     Process zonefiles.
     4     Process bind zonefiles.
     5 """
     5 """
     6 
     6 
     7 __version__ = '0.0.1-dev'
     7 import codecs
     8 
       
     9 import optparse
     8 import optparse
    10 import codecs
     9 
    11 from datetime import datetime
    10 import pvl.args
    12 import logging
    11 import pvl.dns.zone
    13 
    12 from pvl.dns import __version__
    14 import ipaddr
    13 from pvl.dns.zone import ZoneRecord, reverse_ipv4, reverse_ipv6, fqdn
    15 
    14 
    16 log = logging.getLogger('main')
    15 import logging; log = logging.getLogger('main')
    17 
    16 
    18 # command-line options, global state
       
    19 options = None
       
    20 
    17 
    21 def parse_options (argv) :
    18 def parse_options (argv) :
    22     """
    19     """
    23         Parse command-line arguments.
    20         Parse command-line arguments.
    24     """
    21     """
    33             # module docstring
    30             # module docstring
    34             description = __doc__,
    31             description = __doc__,
    35     )
    32     )
    36 
    33 
    37     # logging
    34     # logging
    38     general = optparse.OptionGroup(parser, "General Options")
    35     parser.add_option_group(pvl.args.parser(parser))
    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 
    36 
    46     # input/output
    37     # input/output
    47     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
    38     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
    48             help="Encoding used for input files")
    39             help="Encoding used for input files")
    49 
    40 
    85             help="Domain to use for hosts in reverse zone")
    76             help="Domain to use for hosts in reverse zone")
    86 
    77 
    87     parser.add_option('--reverse-zone',         metavar='NET',
    78     parser.add_option('--reverse-zone',         metavar='NET',
    88             help="Generate forward zone for given subnet (x.z.y | a:b:c:d)")
    79             help="Generate forward zone for given subnet (x.z.y | a:b:c:d)")
    89 
    80 
    90     # 
       
    91     parser.add_option('--doctest',              action='store_true',
       
    92             help="Run module doctests")
       
    93 
       
    94     # defaults
    81     # defaults
    95     parser.set_defaults(
    82     parser.set_defaults(
    96         loglevel            = logging.WARN,
       
    97 
       
    98         # XXX: combine
    83         # XXX: combine
    99         check_exempt        = [],
    84         check_exempt        = [],
   100         meta_ignore         = [],
    85         meta_ignore         = [],
   101     )
    86     )
   102     
    87     
   103     # parse
    88     # parse
   104     options, args = parser.parse_args(argv[1:])
    89     options, args = parser.parse_args(argv[1:])
   105 
    90 
   106     # configure
    91     # apply
   107     logging.basicConfig(
    92     pvl.args.apply(options, prog)
   108         format  = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s',
       
   109         level   = options.loglevel,
       
   110     )
       
   111 
    93 
   112     return options, args
    94     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 
    95 
   560 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) :
    96 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) :
   561     """
    97     """
   562         Parse host/IP pairs from the zone, and verify that they are unique.
    98         Parse host/IP pairs from the zone, and verify that they are unique.
   563 
    99 
   615             if txt :
   151             if txt :
   616                 # comment?
   152                 # comment?
   617                 comment = r.line.comment
   153                 comment = r.line.comment
   618 
   154 
   619                 if comment :
   155                 if comment :
   620                     yield TXTRecord(None, comment, ttl=r.ttl)
   156                     yield ZoneRecord.TXT(None, comment, ttl=r.ttl)
   621 
   157 
   622            
   158            
   623             # XXX: RP, do we need it?
   159             # XXX: RP, do we need it?
   624 
   160 
   625             if mx :
   161             if mx :
   626                 # XXX: is this a good idea?
   162                 # XXX: is this even a good idea?
   627                 yield ZoneRecord(None, 'MX', [10, mx], ttl=r.ttl)
   163                 yield ZoneRecord.MX(None, 10, mx, ttl=r.ttl)
   628 
   164 
   629 def process_zone_meta (zone, ignore=None) :
   165 def process_zone_meta (zone, ignore=None) :
   630     """
   166     """
   631         Process zone metadata -> output.
   167         Process zone metadata -> output.
   632     """
   168     """
   633     
   169     
   634     TIMESTAMP_FORMAT='%Y/%m/%d'
   170     TIMESTAMP_FORMAT = '%Y/%m/%d'
   635     
   171     
   636     for r in zone :
   172     for r in zone :
   637         if ignore and r.name in ignore :
   173         if ignore and r.name in ignore :
   638             # skip
   174             # skip
   639             log.debug("Ignore record: %s", r)
   175             log.debug("Ignore record: %s", r)
   643         if r.type == 'A' :
   179         if r.type == 'A' :
   644             # timestamp?
   180             # timestamp?
   645             timestamp = r.line.timestamp
   181             timestamp = r.line.timestamp
   646 
   182 
   647             if timestamp :
   183             if timestamp :
   648                 yield TXTRecord(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl)
   184                 yield ZoneRecord.TXT(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl)
   649      
   185      
   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) :
   186 def process_zone_reverse (zone, origin, domain) :
   684     """
   187     """
   685         Process zone data -> reverse zone data.
   188         Process zone data -> reverse zone data.
   686     """
   189     """
   687 
   190 
   713 
   216 
   714         # domain to use
   217         # domain to use
   715         host_domain = r.origin or domain
   218         host_domain = r.origin or domain
   716         host_fqdn = fqdn(name, host_domain)
   219         host_fqdn = fqdn(name, host_domain)
   717 
   220 
   718         yield ZoneRecord(ptr, 'PTR', [host_fqdn])
   221         yield ZoneRecord.PTR(ptr, host_fqdn)
   719 
   222 
   720 def write_zone_records (file, zone) :
   223 def write_zone_records (file, zone) :
   721     for r in zone :
   224     for r in zone :
   722         file.write(r.build_line() + u'\n')
   225         file.write(unicode(r))
       
   226         file.write('\n')
   723 
   227 
   724 def open_file (path, mode, charset) :
   228 def open_file (path, mode, charset) :
   725     """
   229     """
   726         Open unicode-enabled file from path, with - using stdio.
   230         Open unicode-enabled file from path, with - using stdio.
   727     """
   231     """
   739     else :
   243     else :
   740         # open
   244         # open
   741         return codecs.open(path, mode, charset)
   245         return codecs.open(path, mode, charset)
   742 
   246 
   743 def main (argv) :
   247 def main (argv) :
   744     global options
       
   745     
       
   746     options, args = parse_options(argv)
   248     options, args = parse_options(argv)
   747 
       
   748     if options.doctest :
       
   749         import doctest
       
   750         fail, total = doctest.testmod()
       
   751         return fail
       
   752 
   249 
   753     if args :
   250     if args :
   754         # open files
   251         # open files
   755         input_files = [open_file(path, 'r', options.input_charset) for path in args]
   252         input_files = [open_file(path, 'r', options.input_charset) for path in args]
   756 
   253 
   762     zone = []
   259     zone = []
   763 
   260 
   764     for file in input_files :
   261     for file in input_files :
   765         log.info("Reading zone: %s", file)
   262         log.info("Reading zone: %s", file)
   766 
   263 
   767         zone += list(parse_zone_records(file, 
   264         zone += list(pvl.dns.zone.parse_zone_records(file, 
   768             line_timestamp_prefix   = options.input_line_date,
   265             line_timestamp_prefix   = options.input_line_date,
   769         ))
   266         ))
   770 
   267 
   771     # check?
   268     # check?
   772     if options.check_hosts :
   269     if options.check_hosts :
   827 
   324 
   828     return 0
   325     return 0
   829 
   326 
   830 if __name__ == '__main__':
   327 if __name__ == '__main__':
   831     import sys
   328     import sys
   832 
       
   833     sys.exit(main(sys.argv))
   329     sys.exit(main(sys.argv))