terom@259: #!/usr/bin/env python terom@259: terom@259: """ terom@268: Import hosts from existing BIND or dhcpd files. terom@259: """ terom@259: terom@268: import pvl.args terom@259: import pvl.dns.zone terom@259: import pvl.dhcp.config terom@261: import pvl.ldap.args terom@259: terom@263: import ipaddr terom@263: import optparse terom@259: import collections terom@259: import re terom@266: import logging; log = logging.getLogger('pvl.hosts-import') terom@259: terom@259: __version__ = '0.1' terom@259: terom@259: def parse_options (argv) : terom@259: """ terom@259: Parse command-line arguments. terom@259: """ terom@259: terom@259: parser = optparse.OptionParser( terom@259: prog = argv[0], terom@259: usage = '%prog: [options]', terom@259: version = __version__, terom@259: terom@259: # module docstring terom@259: description = __doc__, terom@259: ) terom@259: terom@259: # logging terom@259: parser.add_option_group(pvl.args.parser(parser)) terom@261: parser.add_option_group(pvl.ldap.args.parser(parser)) terom@259: terom@259: parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', terom@259: help="Encoding used for input files") terom@259: terom@259: parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', terom@259: help="Encoding used for output files") terom@259: terom@259: # input terom@259: parser.add_option('--import-zone-hosts', metavar='FILE', terom@259: help="Load hosts from DNS zone") terom@259: terom@259: parser.add_option('--import-dhcp-hosts', metavar='FILE', terom@259: help="Load hosts from DHCP config") terom@259: terom@275: parser.add_option('--zone-comments-default-owner', action='store_const', terom@275: dest='zone_comments_default', const='owner', terom@275: help="Import DNS zone comment as owner comment") terom@275: terom@275: parser.add_option('--zone-comments-default-host', action='store_const', terom@275: dest='zone_comments_default', const='host', terom@275: help="Import DNS zone comment as host comment") terom@275: terom@261: parser.add_option('--dump-host-comments', action='store_true', terom@261: help="Dump out info on imported host comments") terom@261: terom@259: # defaults terom@259: parser.add_option('--hosts-domain', metavar='DOMAIN', terom@259: help="Default domain for hosts") terom@259: terom@259: parser.add_option('--zone-unused', metavar='HOST', terom@259: help="DNS name for unallocated hosts") terom@259: terom@259: # output terom@259: parser.add_option('--output-hosts', metavar='FILE', terom@259: help="Output hosts file") terom@259: terom@265: parser.add_option('--output-prefix', metavar='PREFIX', terom@265: help="Select hosts by ip prefix") terom@263: terom@259: # defaults terom@259: parser.set_defaults( terom@259: terom@259: ) terom@259: terom@259: # parse terom@259: options, args = parser.parse_args(argv[1:]) terom@259: terom@259: # apply terom@259: pvl.args.apply(options, argv[0]) terom@259: terom@259: return options, args terom@259: terom@263: def import_zone_hosts (options, file) : terom@259: """ terom@259: Yield host info from zonefile records. terom@259: """ terom@259: terom@279: for rr in pvl.dns.zone.ZoneRecord.load(file, terom@279: # generated hosts need to imported by hand... terom@279: expand_generate = False, terom@279: ) : terom@259: if options.zone_unused and rr.name == options.zone_unused : terom@259: log.debug("%s: skip %s", rr.name, rr) terom@259: continue terom@259: terom@275: elif rr.type in ('A', 'AAAA') : terom@259: ip, = rr.data terom@259: terom@265: yield rr.name, 'ip', ipaddr.IPAddress(ip) terom@259: terom@259: if rr.comment : terom@260: yield rr.name, 'comment', rr.comment terom@259: terom@275: if rr.origin : terom@275: yield rr.name, 'domain', rr.origin terom@275: terom@259: elif rr.type == 'CNAME' : terom@259: host, = rr.data terom@259: terom@259: yield host, 'alias', rr.name terom@259: terom@259: else : terom@259: log.warn("%s: unknown rr: %s", rr.name, rr) terom@259: terom@263: def import_dhcp_host (options, host, items) : terom@259: """ terom@259: Yield host infos from a dhcp host ... { ... } terom@259: """ terom@259: terom@259: hostname = None terom@259: ethernet = [] terom@259: fixed_address = None terom@259: terom@259: for item in items : terom@259: item, args = item[0], item[1:] terom@259: terom@259: if item == 'hardware' : terom@259: _ethernet, ethernet = args terom@259: assert _ethernet == 'ethernet' terom@259: elif item == 'fixed-address' : terom@259: fixed_address, = args terom@259: elif item == 'option' : terom@259: option = args.pop(0) terom@259: terom@259: if option == 'host-name' : terom@259: hostname, = args terom@259: else : terom@259: log.warn("host %s: ignore unknown option: %s", host, option) terom@259: else : terom@259: log.warn("host %s: ignore unknown item: %s", host, item) terom@259: terom@259: # determine hostname terom@264: suffix = None terom@264: terom@264: if '-' in host : terom@259: hostname, suffix = host.rsplit('-', 1) terom@259: else : terom@259: hostname = host terom@259: terom@264: if fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) : terom@264: hostname, domain = fixed_address.split('.', 1) terom@264: terom@264: #if suffix : terom@268: # yield hostname, ('ethernet', suffix), ethernet terom@264: if hostname and ethernet : terom@259: yield hostname, 'ethernet', ethernet terom@264: else : terom@264: log.warn("%s: no hostname/ethernet: %s/%s", host, hostname, ethernet) terom@259: terom@263: def import_dhcp_hosts (options, blocks) : terom@259: """ terom@259: Process hosts from a parsed block terom@259: """ terom@259: terom@259: for block, items, blocks in blocks : terom@259: log.info("%s", block) terom@259: terom@259: block, args = block[0], block[1:] terom@259: terom@259: if block == 'group' : terom@263: for info in import_dhcp_hosts(options, blocks) : terom@259: yield info terom@259: elif block == 'host' : terom@259: host, = args terom@259: terom@259: try : terom@263: for info in import_dhcp_host(options, host, items) : terom@259: yield info terom@259: except ValueError as error : terom@259: log.warn("%s: invalid host: %s", host, error) terom@259: else: terom@259: log.warn("ignore unknown block: %s", block) terom@259: terom@263: def import_dhcp_conf (options, file) : terom@259: items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file) terom@259: terom@259: for item in items : terom@259: item, args = item[0], item[1:] terom@259: terom@259: if item == 'include' : terom@259: include, = args terom@263: for info in import_dhcp_conf(options, pvl.args.apply_file(include)) : terom@259: yield info terom@259: else : terom@259: log.warn("ignore unknown item: %s", item) terom@259: terom@263: for info in import_dhcp_hosts(options, blocks) : terom@259: yield info terom@259: terom@260: ZONE_COMMENTS = ( terom@260: re.compile(r'(?P[^/]+)\s*-\s+(?P.+)'), terom@260: re.compile(r'(?P.+?)\s*/\s*(?P.+)\s+[/-]\s+(?P.+)'), terom@260: re.compile(r'(?P.+?)\s*/\s*(?P.+)\s+[(]\s*(?P.+)[)]'), terom@260: re.compile(r'(?P.+?)\s*/\s*(?P.+)'), terom@260: ) terom@260: terom@260: ZONE_OWNER_MAIL = re.compile(r'(?P.*?)\s*<(?P.+?)>') terom@260: terom@260: def process_zone_comment (options, hostname, comment) : terom@260: """ terom@260: Attempt to parse a host comment field... :D terom@260: terom@260: Yields (field, value) bits terom@260: """ terom@260: terom@260: for regex in ZONE_COMMENTS : terom@260: match = regex.match(comment) terom@260: terom@260: if match : terom@275: matches = match.groupdict() terom@279: terom@279: log.info("%s: matched comment: %s", hostname, comment) terom@260: break terom@260: else : terom@275: if options.zone_comments_default : terom@279: log.info("%s: default comment: %s", hostname, comment) terom@275: matches = { options.zone_comments_default: comment } terom@275: else : terom@275: log.warn("%s: unknown comment: %s", hostname, comment) terom@275: return terom@260: terom@260: owner = matches.pop('owner', None) terom@260: terom@260: if owner : terom@260: mail_match = ZONE_OWNER_MAIL.match(owner) terom@260: terom@260: if mail_match : terom@260: mail_matches = mail_match.groupdict() terom@260: terom@260: owner = mail_matches['owner'] terom@261: yield 'mail', mail_matches['mail'].strip() terom@275: terom@275: yield 'owner', owner.strip() terom@260: terom@260: for field, value in matches.iteritems() : terom@260: if value : terom@261: yield field, value.strip() terom@261: terom@263: NONE_OWNERS = set(( terom@263: u'tech', terom@263: u'atk', terom@263: u'toimisto', terom@263: )) terom@261: terom@263: def process_host_owner_ldap (options, host, info) : terom@261: """ terom@261: Yield guesses for user from LDAP. terom@261: """ terom@261: terom@261: if info.get('mail') : terom@261: for user in options.ldap.users.filter( terom@261: { 'mailLocalAddress': info['mail'] }, terom@261: { 'uid': info['mail'] }, terom@261: ) : terom@263: yield user, None terom@261: terom@261: if info.get('group') and info.get('owner') : terom@261: groups = options.ldap.groups.filter(cn=info['group']) terom@261: terom@261: for group in groups : terom@261: for user in options.ldap.users.filter({ terom@261: 'gidNumber': group['gidNumber'], terom@261: 'cn': info['owner'], terom@261: }) : terom@263: yield user, group terom@261: terom@261: if info.get('owner') : terom@261: for user in options.ldap.users.filter({ terom@261: 'cn': info['owner'], terom@261: }) : terom@263: yield user, None terom@263: terom@263: def process_host_owner (options, host, info) : terom@263: """ terom@263: Return (owner, comment) for host based on info, or None. terom@263: """ terom@263: terom@275: owner = info.get('owner') terom@275: terom@275: if owner and owner.lower() in NONE_OWNERS : terom@264: return False terom@263: terom@263: # from ldap? terom@263: for ldap in process_host_owner_ldap(options, host, info) : terom@263: user, group = ldap terom@263: terom@263: if not group : terom@263: # get group from ldap terom@263: group = options.ldap.users.group(user) terom@263: terom@263: return user['uid'], u"{group} / {user}".format( terom@263: user = user.getunicode('cn'), terom@263: group = group.getunicode('cn'), terom@263: ) terom@261: terom@261: def process_host_comments (options, host, info) : terom@261: """ terom@261: Process host fields from comment. terom@261: terom@261: Attempts to find owner from LDAP.. terom@261: """ terom@261: terom@261: log.debug("%s: %s", host, info) terom@260: terom@263: owner = process_host_owner(options, host, info) terom@261: terom@264: if owner is False : terom@264: # do not mark any owner terom@264: pass terom@264: terom@264: elif owner : terom@263: owner, comment = owner terom@263: terom@263: log.info("%s: %s (%s)", host, owner, comment) terom@263: terom@268: yield 'comment-owner', comment terom@261: yield 'owner', owner, terom@264: terom@279: elif 'group' in info or 'owner' in info : terom@264: log.warn("%s: unknown owner: %s", host, info) terom@268: yield 'comment-owner', "{group} / {owner}".format( terom@264: group = info.get('group', ''), terom@264: owner = info.get('owner', ''), terom@264: ) terom@262: terom@262: if info.get('host') : terom@268: yield 'comment-host', info['host'] terom@260: terom@260: def process_hosts_comments (options, import_hosts) : terom@260: """ terom@260: Parse out comments from host imports.. terom@260: """ terom@260: terom@260: for host, field, value in import_hosts : terom@260: if field != 'comment': terom@260: yield host, field, value terom@260: continue terom@260: terom@260: fields = dict(process_zone_comment(options, host, value)) terom@261: terom@261: if options.dump_host_comments : terom@261: print u"{host:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {hostinfo}".format( terom@261: host = host, terom@261: comment = value, terom@261: group = fields.get('group', ''), terom@261: owner = fields.get('owner', ''), terom@261: mail = fields.get('mail', ''), terom@261: hostinfo = fields.get('host', ''), terom@261: ).encode('utf-8') terom@261: terom@260: terom@261: for field, value in process_host_comments(options, host, fields) : terom@261: yield host, field, value terom@263: terom@263: def apply_hosts_import (options) : terom@259: """ terom@263: Import host infos from given files. terom@259: """ terom@259: terom@263: if options.import_zone_hosts: terom@263: for info in import_zone_hosts(options, terom@268: pvl.args.apply_file(options.import_zone_hosts, 'r', options.input_charset)) : terom@263: yield info terom@263: terom@263: if options.import_dhcp_hosts: terom@263: for info in import_dhcp_conf(options, terom@268: pvl.args.apply_file(options.import_dhcp_hosts, 'r', options.input_charset)) : terom@263: yield info terom@263: terom@263: def import_hosts (options) : terom@263: """ terom@263: Import hosts from dns/dhcp. terom@263: """ terom@263: terom@263: import_hosts = apply_hosts_import(options) terom@263: import_hosts = process_hosts_comments(options, import_hosts) terom@263: terom@263: # gather terom@259: hosts = collections.defaultdict(lambda: collections.defaultdict(list)) terom@259: terom@259: for host, field, value in import_hosts : terom@259: hosts[host][field].append(value) terom@259: terom@259: return hosts.iteritems() terom@259: terom@265: def process_export_hosts (options, hosts) : terom@265: if options.output_prefix : terom@265: prefix = ipaddr.IPNetwork(options.output_prefix) terom@265: else : terom@265: prefix = None terom@265: terom@263: for host, fields in hosts : terom@263: ip = fields.get('ip') terom@265: terom@265: # sort by IP terom@265: if ip : terom@268: sort = ip[0] terom@265: else : terom@265: # fake, to sort correctly terom@265: sort = ipaddr.IPAddress(0) terom@265: terom@265: # select terom@265: if prefix: terom@265: if not (ip and ip in prefix) : terom@265: continue terom@263: terom@265: yield sort, host, fields terom@263: terom@263: def export_hosts (options, hosts) : terom@263: """ terom@263: Export hosts to file. terom@263: """ terom@263: terom@263: file = pvl.args.apply_file(options.output_hosts, 'w', options.output_charset) terom@263: terom@265: # filter + sort terom@265: hosts = [(host, fields) for sort, host, fields in sorted(process_export_hosts(options, hosts))] terom@263: terom@263: for host, fields in hosts : terom@268: for comment in fields.get('comment-host', ()): terom@263: print >>file, u"# {comment}".format(comment=comment) terom@263: terom@263: print >>file, u"[{host}]".format(host=host) terom@263: terom@263: for field, fmt in ( terom@263: ('ip', None), terom@263: ('ethernet', None), terom@268: ('owner', u"\t{field:15} = {value} # {fields[comment-owner][0]}"), terom@268: ('alias', None), terom@263: ) : terom@263: if not fmt : terom@263: fmt = u"\t{field:15} = {value}" terom@268: terom@268: values = fields.get(field, ()) terom@263: terom@268: if len(values) > 1 : terom@268: for index, value in enumerate(values, 1) : terom@268: print >>file, fmt.format( terom@268: field = "{field}.{index}".format(field=field, index=index), terom@268: value = value, terom@268: fields = fields terom@268: ) terom@268: elif len(values) > 0 : terom@268: value, = values terom@263: print >>file, fmt.format(field=field, value=value, fields=fields) terom@263: terom@263: print >>file terom@259: terom@259: def main (argv) : terom@259: options, args = parse_options(argv) terom@261: terom@261: options.ldap = pvl.ldap.args.apply(options) terom@259: terom@259: if args : terom@259: # direct from file terom@259: hosts = pvl.args.apply_files(args, 'r', options.input_charset) terom@259: else : terom@259: # import terom@263: hosts = import_hosts(options) terom@259: terom@259: # output terom@259: if options.output_hosts : terom@263: export_hosts(options, hosts) terom@259: terom@259: if __name__ == '__main__': terom@259: pvl.args.main(main)