process-zone: implement --check-hosts; fix logging processName
authorTero Marttila <terom@paivola.fi>
Fri, 16 Mar 2012 15:03:11 +0200
changeset 535 bddc9a060a73
parent 534 879917cebb9c
child 536 7d02a07e0354
process-zone: implement --check-hosts; fix logging processName
bin/process-zone
--- a/bin/process-zone	Fri Mar 16 15:02:50 2012 +0200
+++ b/bin/process-zone	Fri Mar 16 15:03:11 2012 +0200
@@ -10,7 +10,7 @@
 import codecs
 import logging
 
-log = logging.getLogger()
+log = logging.getLogger('main')
 
 # command-line options, global state
 options = None
@@ -20,8 +20,10 @@
         Parse command-line arguments.
     """
 
+    prog = argv[0]
+
     parser = optparse.OptionParser(
-            prog        = argv[0],
+            prog        = prog,
             usage       = '%prog: [options]',
             version     = __version__,
 
@@ -38,6 +40,7 @@
 
     parser.add_option_group(general)
 
+    # input/output
     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
             help="Encoding used for input files")
 
@@ -47,6 +50,14 @@
     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',         action='append',
+            help="Allow given names to have multiple records")
+
+    # forward stage
     parser.add_option('--forward-zone',         action='store_true', 
             help="Generate forward zone")
 
@@ -56,6 +67,7 @@
     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")
 
@@ -65,6 +77,8 @@
     # defaults
     parser.set_defaults(
         loglevel            = logging.WARN,
+
+        check_exempt        = [],
     )
     
     # parse
@@ -72,7 +86,7 @@
 
     # configure
     logging.basicConfig(
-        format  = '%(processName)s: %(name)s: %(levelname)s %(funcName)s : %(message)s',
+        format  = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s',
         level   = options.loglevel,
     )
 
@@ -167,6 +181,48 @@
         if data :
             yield data
 
+def check_zone_hosts (zone, whitelist=None) :
+    """
+        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
+
+    for item in zone :
+        text = ' '.join(pp for pp in item if pp)
+        name, ttl, type, data, comment = item
+
+        # name
+        if name not in by_name :
+            by_name[name] = text
+
+        elif name in whitelist :
+            log.debug("Duplicate whitelist entry: %r", item)
+
+        else :
+            # fail!
+            log.warn("Duplicate name: %s <-> %s", text, by_name[name])
+            fail = True
+
+        # ip
+        if type == 'A' :
+            ip = data
+
+            if ip not in by_ip :
+                by_ip[ip] = text
+
+            else :
+                # fail!
+                log.warn("Duplicate IP: %s <-> %s", text, by_ip[ip])
+                fail = True
+
+    return fail
+
 def process_zone_forwards (zone, txt=False, mx=False) :
     """
         Process zone data -> forward zone data.
@@ -307,6 +363,19 @@
 
         zone += list(parse_zone(file))
 
+    # 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)
 
@@ -325,8 +394,13 @@
 
         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(output, zone)