diff -r 1ad9cec4f556 -r 65b483fb862c 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[^/]+)\s*-\s+(?P.+)'), + re.compile(r'(?P.+?)\s*/\s*(?P.+)\s+[/-]\s+(?P.+)'), + re.compile(r'(?P.+?)\s*/\s*(?P.+)\s+[(]\s*(?P.+)[)]'), + re.compile(r'(?P.+?)\s*/\s*(?P.+)'), + re.compile(r'(?P.+)'), +) + +ZONE_OWNER_MAIL = re.compile(r'(?P.*?)\s*<(?P.+?)>') + +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)