bin/pvl.hosts-import
changeset 266 4eb3d73d852c
parent 265 5f2807999222
child 268 560ba0544254
equal deleted inserted replaced
265:5f2807999222 266:4eb3d73d852c
       
     1 #!/usr/bin/env python
       
     2 
       
     3 """
       
     4     Manipulate host definitions for dns/dhcp.
       
     5 """
       
     6 
       
     7 import pvl.args, optparse
       
     8 import pvl.dns.zone
       
     9 import pvl.dhcp.config
       
    10 import pvl.ldap.args
       
    11 
       
    12 import ipaddr
       
    13 import optparse
       
    14 import collections
       
    15 import re
       
    16 import logging; log = logging.getLogger('pvl.hosts-import')
       
    17 
       
    18 __version__ = '0.1'
       
    19 
       
    20 def parse_options (argv) :
       
    21     """
       
    22         Parse command-line arguments.
       
    23     """
       
    24 
       
    25     parser = optparse.OptionParser(
       
    26             prog        = argv[0],
       
    27             usage       = '%prog: [options]',
       
    28             version     = __version__,
       
    29 
       
    30             # module docstring
       
    31             description = __doc__,
       
    32     )
       
    33 
       
    34     # logging
       
    35     parser.add_option_group(pvl.args.parser(parser))
       
    36     parser.add_option_group(pvl.ldap.args.parser(parser))
       
    37 
       
    38     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
       
    39             help="Encoding used for input files")
       
    40 
       
    41     parser.add_option('--output-charset',       metavar='CHARSET',  default='utf-8', 
       
    42             help="Encoding used for output files")
       
    43 
       
    44     # input
       
    45     parser.add_option('--import-zone-hosts',    metavar='FILE',
       
    46             help="Load hosts from DNS zone")
       
    47 
       
    48     parser.add_option('--import-dhcp-hosts',    metavar='FILE',
       
    49             help="Load hosts from DHCP config")
       
    50 
       
    51     parser.add_option('--dump-host-comments',   action='store_true',
       
    52             help="Dump out info on imported host comments")
       
    53 
       
    54     # defaults
       
    55     parser.add_option('--hosts-domain',         metavar='DOMAIN',
       
    56             help="Default domain for hosts")
       
    57     
       
    58     parser.add_option('--zone-unused',          metavar='HOST',
       
    59             help="DNS name for unallocated hosts")
       
    60 
       
    61     # output
       
    62     parser.add_option('--output-hosts',         metavar='FILE',
       
    63             help="Output hosts file")
       
    64 
       
    65     parser.add_option('--output-prefix',        metavar='PREFIX',
       
    66             help="Select hosts by ip prefix")
       
    67 
       
    68     # defaults
       
    69     parser.set_defaults(
       
    70 
       
    71     )
       
    72     
       
    73     # parse
       
    74     options, args = parser.parse_args(argv[1:])
       
    75 
       
    76     # apply
       
    77     pvl.args.apply(options, argv[0])
       
    78 
       
    79     return options, args
       
    80 
       
    81 def import_zone_hosts (options, file) :
       
    82     """
       
    83         Yield host info from zonefile records.
       
    84     """
       
    85 
       
    86     for rr in pvl.dns.zone.ZoneRecord.load(file) :
       
    87         if options.zone_unused and rr.name == options.zone_unused :
       
    88             log.debug("%s: skip %s", rr.name, rr)
       
    89             continue
       
    90 
       
    91         elif rr.type == 'A' :
       
    92             ip, = rr.data
       
    93 
       
    94             yield rr.name, 'ip', ipaddr.IPAddress(ip)
       
    95 
       
    96             if rr.comment :
       
    97                 yield rr.name, 'comment', rr.comment
       
    98 
       
    99         elif rr.type == 'CNAME' :
       
   100             host, = rr.data
       
   101 
       
   102             yield host, 'alias', rr.name
       
   103 
       
   104         else :
       
   105             log.warn("%s: unknown rr: %s", rr.name, rr)
       
   106 
       
   107 def import_dhcp_host (options, host, items) :
       
   108     """
       
   109         Yield host infos from a dhcp host ... { ... }
       
   110     """
       
   111 
       
   112     hostname = None
       
   113     ethernet = []
       
   114     fixed_address = None
       
   115 
       
   116     for item in items :
       
   117         item, args = item[0], item[1:]
       
   118 
       
   119         if item == 'hardware' :
       
   120             _ethernet, ethernet = args
       
   121             assert _ethernet == 'ethernet'
       
   122         elif item == 'fixed-address' :
       
   123             fixed_address, = args
       
   124         elif item == 'option' :
       
   125             option = args.pop(0)
       
   126 
       
   127             if option == 'host-name' :
       
   128                 hostname, = args
       
   129             else :
       
   130                 log.warn("host %s: ignore unknown option: %s", host, option)
       
   131         else :
       
   132             log.warn("host %s: ignore unknown item: %s", host, item)
       
   133 
       
   134     # determine hostname
       
   135     suffix = None
       
   136 
       
   137     if '-' in host :
       
   138         hostname, suffix = host.rsplit('-', 1)
       
   139     else :
       
   140         hostname = host
       
   141 
       
   142     if fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) :
       
   143         hostname, domain = fixed_address.split('.', 1)
       
   144 
       
   145     #if suffix :
       
   146     #    yield hostname, 'ethernet:{suffix}'.format(suffix=suffix), ethernet
       
   147     if hostname and ethernet :
       
   148         yield hostname, 'ethernet', ethernet
       
   149     else :
       
   150         log.warn("%s: no hostname/ethernet: %s/%s", host, hostname, ethernet)
       
   151 
       
   152 def import_dhcp_hosts (options, blocks) :
       
   153     """
       
   154         Process hosts from a parsed block
       
   155     """
       
   156 
       
   157     for block, items, blocks in blocks :
       
   158         log.info("%s", block)
       
   159         
       
   160         block, args = block[0], block[1:]
       
   161 
       
   162         if block == 'group' :
       
   163             for info in import_dhcp_hosts(options, blocks) :
       
   164                 yield info
       
   165         elif block == 'host' :
       
   166             host, = args
       
   167 
       
   168             try :
       
   169                 for info in import_dhcp_host(options, host, items) :
       
   170                     yield info
       
   171             except ValueError as error :
       
   172                 log.warn("%s: invalid host: %s", host, error)
       
   173         else:
       
   174             log.warn("ignore unknown block: %s", block)
       
   175 
       
   176 def import_dhcp_conf (options, file) :
       
   177     items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file)
       
   178 
       
   179     for item in items :
       
   180         item, args = item[0], item[1:]
       
   181 
       
   182         if item == 'include' :
       
   183             include, = args
       
   184             for info in import_dhcp_conf(options, pvl.args.apply_file(include)) :
       
   185                 yield info
       
   186         else :
       
   187             log.warn("ignore unknown item: %s", item)
       
   188     
       
   189     for info in import_dhcp_hosts(options, blocks) :
       
   190         yield info
       
   191 
       
   192 ZONE_COMMENTS = (
       
   193         re.compile(r'(?P<owner>[^/]+)\s*-\s+(?P<host>.+)'),
       
   194         re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[/-]\s+(?P<host>.+)'),
       
   195         re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[(]\s*(?P<host>.+)[)]'),
       
   196         re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)'),
       
   197         re.compile(r'(?P<owner>.+)'),
       
   198 )
       
   199 
       
   200 ZONE_OWNER_MAIL = re.compile(r'(?P<owner>.*?)\s*<(?P<mail>.+?)>')
       
   201 
       
   202 def process_zone_comment (options, hostname, comment) :
       
   203     """
       
   204         Attempt to parse a host comment field... :D
       
   205 
       
   206         Yields (field, value) bits
       
   207     """
       
   208 
       
   209     for regex in ZONE_COMMENTS :
       
   210         match = regex.match(comment)
       
   211 
       
   212         if match :
       
   213             break
       
   214     else :
       
   215         log.warn("%s: unparsed comment: %s", hostname, comment)
       
   216         return
       
   217     
       
   218     matches = match.groupdict()
       
   219     owner = matches.pop('owner', None)
       
   220     
       
   221     if owner :
       
   222         mail_match = ZONE_OWNER_MAIL.match(owner)
       
   223 
       
   224         if mail_match :
       
   225             mail_matches = mail_match.groupdict()
       
   226             
       
   227             owner = mail_matches['owner']
       
   228             yield 'mail', mail_matches['mail'].strip()
       
   229 
       
   230     yield 'owner', owner.strip()
       
   231 
       
   232     for field, value in matches.iteritems() :
       
   233         if value :
       
   234             yield field, value.strip()
       
   235 
       
   236 NONE_OWNERS = set((
       
   237     u'tech',
       
   238     u'atk',
       
   239     u'toimisto',
       
   240 ))
       
   241 
       
   242 def process_host_owner_ldap (options, host, info) :
       
   243     """
       
   244         Yield guesses for user from LDAP.
       
   245     """
       
   246 
       
   247     if info.get('mail') :
       
   248         for user in options.ldap.users.filter(
       
   249                 { 'mailLocalAddress': info['mail'] },
       
   250                 { 'uid': info['mail'] },
       
   251         ) :
       
   252             yield user, None
       
   253 
       
   254     if info.get('group') and info.get('owner') :
       
   255         groups = options.ldap.groups.filter(cn=info['group'])
       
   256 
       
   257         for group in groups :
       
   258             for user in options.ldap.users.filter({
       
   259                 'gidNumber': group['gidNumber'],
       
   260                 'cn': info['owner'],
       
   261             }) :
       
   262                 yield user, group
       
   263 
       
   264     if info.get('owner') :
       
   265             for user in options.ldap.users.filter({
       
   266                 'cn': info['owner'],
       
   267             }) :
       
   268                 yield user, None
       
   269 
       
   270 def process_host_owner (options, host, info) :
       
   271     """
       
   272         Return (owner, comment) for host based on info, or None.
       
   273     """
       
   274 
       
   275     if info.get('owner').lower() in NONE_OWNERS :
       
   276         return False
       
   277     
       
   278     # from ldap?
       
   279     for ldap in process_host_owner_ldap(options, host, info) :
       
   280         user, group = ldap
       
   281         
       
   282         if not group :
       
   283             # get group from ldap
       
   284             group = options.ldap.users.group(user)
       
   285         
       
   286         return user['uid'], u"{group} / {user}".format(
       
   287                 user    = user.getunicode('cn'),
       
   288                 group   = group.getunicode('cn'),
       
   289         )
       
   290 
       
   291 def process_host_comments (options, host, info) :
       
   292     """
       
   293         Process host fields from comment.
       
   294 
       
   295         Attempts to find owner from LDAP..
       
   296     """
       
   297 
       
   298     log.debug("%s: %s", host, info)
       
   299     
       
   300     owner = process_host_owner(options, host, info) 
       
   301 
       
   302     if owner is False :
       
   303         # do not mark any owner
       
   304         pass
       
   305 
       
   306     elif owner :
       
   307         owner, comment = owner
       
   308         
       
   309         log.info("%s: %s (%s)", host, owner, comment)
       
   310         
       
   311         yield 'owner-comment', comment
       
   312         yield 'owner', owner,
       
   313 
       
   314     else :
       
   315         log.warn("%s: unknown owner: %s", host, info)
       
   316         yield 'comment', "owner: {group} / {owner}".format(
       
   317                 group   = info.get('group', ''),
       
   318                 owner   = info.get('owner', ''),
       
   319         )
       
   320     
       
   321     if info.get('host') :
       
   322         yield 'comment', info['host']
       
   323 
       
   324 def process_hosts_comments (options, import_hosts) :
       
   325     """
       
   326         Parse out comments from host imports..
       
   327     """
       
   328 
       
   329     for host, field, value in import_hosts :
       
   330         if field != 'comment':
       
   331             yield host, field, value
       
   332             continue
       
   333 
       
   334         fields = dict(process_zone_comment(options, host, value))
       
   335         
       
   336         if options.dump_host_comments :
       
   337             print u"{host:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {hostinfo}".format(
       
   338                     host        = host,
       
   339                     comment     = value,
       
   340                     group       = fields.get('group', ''),
       
   341                     owner       = fields.get('owner', ''),
       
   342                     mail        = fields.get('mail', ''),
       
   343                     hostinfo    = fields.get('host', ''),
       
   344             ).encode('utf-8')
       
   345         
       
   346 
       
   347         for field, value in process_host_comments(options, host, fields) :
       
   348             yield host, field, value
       
   349 
       
   350 def apply_hosts_import (options) :
       
   351     """
       
   352         Import host infos from given files.
       
   353     """
       
   354 
       
   355     if options.import_zone_hosts:
       
   356         for info in import_zone_hosts(options,
       
   357                 pvl.args.apply_file(options.import_zone_hosts)) :
       
   358             yield info
       
   359     
       
   360     if options.import_dhcp_hosts:
       
   361         for info in import_dhcp_conf(options,
       
   362                 pvl.args.apply_file(options.import_dhcp_hosts)) :
       
   363             yield info
       
   364        
       
   365 def import_hosts (options) :
       
   366     """
       
   367         Import hosts from dns/dhcp.
       
   368     """
       
   369 
       
   370     import_hosts = apply_hosts_import(options)
       
   371     import_hosts = process_hosts_comments(options, import_hosts)
       
   372     
       
   373     # gather
       
   374     hosts = collections.defaultdict(lambda: collections.defaultdict(list))
       
   375 
       
   376     for host, field, value in import_hosts :
       
   377         hosts[host][field].append(value)
       
   378     
       
   379     return hosts.iteritems()
       
   380 
       
   381 def process_export_hosts (options, hosts) :
       
   382     if options.output_prefix :
       
   383         prefix = ipaddr.IPNetwork(options.output_prefix)
       
   384     else :
       
   385         prefix = None
       
   386 
       
   387     for host, fields in hosts :
       
   388         ip = fields.get('ip')
       
   389         
       
   390         # sort by IP
       
   391         if ip :
       
   392             sort = ip = ip[0]
       
   393         else :
       
   394             # fake, to sort correctly
       
   395             sort = ipaddr.IPAddress(0)
       
   396         
       
   397         # select
       
   398         if prefix:
       
   399             if not (ip and ip in prefix) :
       
   400                 continue
       
   401 
       
   402         yield sort, host, fields
       
   403 
       
   404 def export_hosts (options, hosts) :
       
   405     """
       
   406         Export hosts to file.
       
   407     """
       
   408 
       
   409     file = pvl.args.apply_file(options.output_hosts, 'w', options.output_charset)
       
   410 
       
   411     # filter + sort
       
   412     hosts = [(host, fields) for sort, host, fields in sorted(process_export_hosts(options, hosts))]
       
   413 
       
   414     for host, fields in hosts :
       
   415         for comment in fields.get('comment', ()) :
       
   416             print >>file, u"# {comment}".format(comment=comment)
       
   417 
       
   418         print >>file, u"[{host}]".format(host=host)
       
   419         
       
   420         for field, fmt in (
       
   421                 ('ip',              None),
       
   422                 ('ethernet',        None),
       
   423                 ('owner',           u"\t{field:15} = {value} # {fields[owner-comment][0]}"),
       
   424         ) :
       
   425             if not fmt :
       
   426                 fmt = u"\t{field:15} = {value}"
       
   427 
       
   428             for value in fields.get(field, ()) :
       
   429                 print >>file, fmt.format(field=field, value=value, fields=fields)
       
   430         
       
   431         print >>file
       
   432 
       
   433 def main (argv) :
       
   434     options, args = parse_options(argv)
       
   435 
       
   436     options.ldap = pvl.ldap.args.apply(options)
       
   437     
       
   438     if args :
       
   439         # direct from file
       
   440         hosts = pvl.args.apply_files(args, 'r', options.input_charset)
       
   441     else :
       
   442         # import
       
   443         hosts = import_hosts(options)
       
   444    
       
   445     # output
       
   446     if options.output_hosts :
       
   447         export_hosts(options, hosts)
       
   448 
       
   449 if __name__ == '__main__':
       
   450     pvl.args.main(main)