pvl.dns-hosts: import hosts from dns/dhcp
authorTero Marttila <terom@paivola.fi>
Mon, 16 Dec 2013 11:41:59 +0200
changeset 259 65b483fb862c
parent 258 1ad9cec4f556
child 260 e58baab6b4cd
pvl.dns-hosts: import hosts from dns/dhcp
bin/pvl.dns-hosts
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/pvl.dns-hosts	Mon Dec 16 11:41:59 2013 +0200
@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+
+"""
+    Manipulate host definitions for dns/dhcp.
+"""
+
+import pvl.args, optparse
+import pvl.dns.zone
+import pvl.dhcp.config
+
+import collections
+import re
+import logging; log = logging.getLogger('main')
+
+__version__ = '0.1'
+
+def parse_options (argv) :
+    """
+        Parse command-line arguments.
+    """
+
+    parser = optparse.OptionParser(
+            prog        = argv[0],
+            usage       = '%prog: [options]',
+            version     = __version__,
+
+            # module docstring
+            description = __doc__,
+    )
+
+    # logging
+    parser.add_option_group(pvl.args.parser(parser))
+
+    parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
+            help="Encoding used for input files")
+
+    parser.add_option('--output-charset',       metavar='CHARSET',  default='utf-8', 
+            help="Encoding used for output files")
+
+    # input
+    parser.add_option('--import-zone-hosts',    metavar='FILE',
+            help="Load hosts from DNS zone")
+
+    parser.add_option('--import-dhcp-hosts',    metavar='FILE',
+            help="Load hosts from DHCP config")
+
+    # defaults
+    parser.add_option('--hosts-domain',         metavar='DOMAIN',
+            help="Default domain for hosts")
+    
+    parser.add_option('--zone-unused',          metavar='HOST',
+            help="DNS name for unallocated hosts")
+
+    # output
+    parser.add_option('--output-hosts',         metavar='FILE',
+            help="Output hosts file")
+
+    # defaults
+    parser.set_defaults(
+
+    )
+    
+    # parse
+    options, args = parser.parse_args(argv[1:])
+
+    # apply
+    pvl.args.apply(options, argv[0])
+
+    return options, args
+
+ZONE_COMMENTS = (
+        re.compile(r'(?P<owner>[^/]+)\s*-\s+(?P<host>.+)'),
+        re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[/-]\s+(?P<host>.+)'),
+        re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[(]\s*(?P<host>.+)[)]'),
+        re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)'),
+        re.compile(r'(?P<owner>.+)'),
+)
+
+ZONE_OWNER_MAIL = re.compile(r'(?P<owner>.*?)\s*<(?P<mail>.+?)>')
+
+def process_zone_comment (options, hostname, comment) :
+    """
+        Attempt to parse a host comment field... :D
+    """
+
+    yield 'comment', comment
+    
+    for regex in ZONE_COMMENTS :
+        match = regex.match(comment)
+
+        if match :
+            break
+    else :
+        log.warn("%s: unparsed comment: %s", hostname, comment)
+        return
+    
+    matches = match.groupdict()
+    owner = matches.pop('owner', None)
+    
+    if owner :
+        mail_match = ZONE_OWNER_MAIL.match(owner)
+
+        if mail_match :
+            mail_matches = mail_match.groupdict()
+            
+            owner = mail_matches['owner']
+            yield 'comment-mail', mail_matches['mail']
+        else :
+            mail_matches = { }
+    else :
+        mail_matches = { }
+
+    yield 'comment-owner', owner
+
+    for group, value in matches.iteritems() :
+        if value :
+            yield 'comment-{group}'.format(group=group), value.strip()
+    
+    print u"{hostname:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {host}".format(
+            hostname    = hostname,
+            comment     = comment,
+            group       = matches.get('group', ''),
+            owner       = owner,
+            mail        = mail_matches.get('mail', ''),
+            host        = matches.get('host', ''),
+    ).encode('utf-8')
+
+def process_zone_hosts (options, file) :
+    """
+        Yield host info from zonefile records.
+    """
+
+    for rr in pvl.dns.zone.ZoneRecord.load(file) :
+        if options.zone_unused and rr.name == options.zone_unused :
+            log.debug("%s: skip %s", rr.name, rr)
+            continue
+
+        elif rr.type == 'A' :
+            ip, = rr.data
+
+            yield rr.name, 'ip', ip
+
+            if rr.comment :
+                for field, value in process_zone_comment(options, rr.name, rr.comment) :
+                    yield rr.name, field, value
+
+        elif rr.type == 'CNAME' :
+            host, = rr.data
+
+            yield host, 'alias', rr.name
+
+        else :
+            log.warn("%s: unknown rr: %s", rr.name, rr)
+
+def process_dhcp_host (options, host, items) :
+    """
+        Yield host infos from a dhcp host ... { ... }
+    """
+
+    hostname = None
+    ethernet = []
+    fixed_address = None
+
+    for item in items :
+        item, args = item[0], item[1:]
+
+        if item == 'hardware' :
+            _ethernet, ethernet = args
+            assert _ethernet == 'ethernet'
+        elif item == 'fixed-address' :
+            fixed_address, = args
+        elif item == 'option' :
+            option = args.pop(0)
+
+            if option == 'host-name' :
+                hostname, = args
+            else :
+                log.warn("host %s: ignore unknown option: %s", host, option)
+        else :
+            log.warn("host %s: ignore unknown item: %s", host, item)
+
+    # determine hostname
+    if hostname :
+        pass
+    elif fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) :
+        hostname, domain = fixed_address.split('.', 1)
+    elif '-' in host :
+        hostname, suffix = host.rsplit('-', 1)
+    else :
+        log.warn("%s: guess hostname: %s", host, host)
+        hostname = host
+
+    if hostname :
+        yield hostname, 'ethernet', ethernet
+
+def process_dhcp_hosts (options, blocks) :
+    """
+        Process hosts from a parsed block
+    """
+
+    for block, items, blocks in blocks :
+        log.info("%s", block)
+        
+        block, args = block[0], block[1:]
+
+        if block == 'group' :
+            for info in process_dhcp_hosts(options, blocks) :
+                yield info
+        elif block == 'host' :
+            host, = args
+
+            try :
+                for info in process_dhcp_host(options, host, items) :
+                    yield info
+            except ValueError as error :
+                log.warn("%s: invalid host: %s", host, error)
+        else:
+            log.warn("ignore unknown block: %s", block)
+
+def process_dhcp_conf (options, file) :
+    items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file)
+
+    for item in items :
+        item, args = item[0], item[1:]
+
+        if item == 'include' :
+            include, = args
+            for info in process_dhcp_conf(options, pvl.args.apply_file(include)) :
+                yield info
+        else :
+            log.warn("ignore unknown item: %s", item)
+    
+    for info in process_dhcp_hosts(options, blocks) :
+        yield info
+
+def apply_hosts_import (options) :
+    """
+        Import host infos from given files.
+    """
+
+    if options.import_zone_hosts:
+        for info in process_zone_hosts(options,
+                pvl.args.apply_file(options.import_zone_hosts)) :
+            yield info
+    
+    if options.import_dhcp_hosts:
+        for info in process_dhcp_conf(options,
+                pvl.args.apply_file(options.import_dhcp_hosts)) :
+            yield info
+ 
+def process_hosts_import (options, import_hosts) :
+    """
+        Import host definitions from given infos
+    """
+
+    hosts = collections.defaultdict(lambda: collections.defaultdict(list))
+
+    for host, field, value in import_hosts :
+        hosts[host][field].append(value)
+    
+    return hosts.iteritems()
+
+
+def main (argv) :
+    options, args = parse_options(argv)
+    
+    if args :
+        # direct from file
+        hosts = pvl.args.apply_files(args, 'r', options.input_charset)
+    else :
+        # import
+        import_hosts = apply_hosts_import(options)
+        hosts = process_hosts_import(options, import_hosts)
+   
+    # output
+    if options.output_hosts :
+        for host, fields in hosts :
+            print host
+
+            for field, values in fields.iteritems() :
+                for value in values :
+                    print "\t", field, "\t", value.encode(options.output_charset)
+
+    return 0
+
+if __name__ == '__main__':
+    pvl.args.main(main)