bin/pvl.dns-hosts
changeset 259 65b483fb862c
child 260 e58baab6b4cd
equal deleted inserted replaced
258:1ad9cec4f556 259:65b483fb862c
       
     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 
       
    11 import collections
       
    12 import re
       
    13 import logging; log = logging.getLogger('main')
       
    14 
       
    15 __version__ = '0.1'
       
    16 
       
    17 def parse_options (argv) :
       
    18     """
       
    19         Parse command-line arguments.
       
    20     """
       
    21 
       
    22     parser = optparse.OptionParser(
       
    23             prog        = argv[0],
       
    24             usage       = '%prog: [options]',
       
    25             version     = __version__,
       
    26 
       
    27             # module docstring
       
    28             description = __doc__,
       
    29     )
       
    30 
       
    31     # logging
       
    32     parser.add_option_group(pvl.args.parser(parser))
       
    33 
       
    34     parser.add_option('-c', '--input-charset',  metavar='CHARSET',  default='utf-8', 
       
    35             help="Encoding used for input files")
       
    36 
       
    37     parser.add_option('--output-charset',       metavar='CHARSET',  default='utf-8', 
       
    38             help="Encoding used for output files")
       
    39 
       
    40     # input
       
    41     parser.add_option('--import-zone-hosts',    metavar='FILE',
       
    42             help="Load hosts from DNS zone")
       
    43 
       
    44     parser.add_option('--import-dhcp-hosts',    metavar='FILE',
       
    45             help="Load hosts from DHCP config")
       
    46 
       
    47     # defaults
       
    48     parser.add_option('--hosts-domain',         metavar='DOMAIN',
       
    49             help="Default domain for hosts")
       
    50     
       
    51     parser.add_option('--zone-unused',          metavar='HOST',
       
    52             help="DNS name for unallocated hosts")
       
    53 
       
    54     # output
       
    55     parser.add_option('--output-hosts',         metavar='FILE',
       
    56             help="Output hosts file")
       
    57 
       
    58     # defaults
       
    59     parser.set_defaults(
       
    60 
       
    61     )
       
    62     
       
    63     # parse
       
    64     options, args = parser.parse_args(argv[1:])
       
    65 
       
    66     # apply
       
    67     pvl.args.apply(options, argv[0])
       
    68 
       
    69     return options, args
       
    70 
       
    71 ZONE_COMMENTS = (
       
    72         re.compile(r'(?P<owner>[^/]+)\s*-\s+(?P<host>.+)'),
       
    73         re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[/-]\s+(?P<host>.+)'),
       
    74         re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)\s+[(]\s*(?P<host>.+)[)]'),
       
    75         re.compile(r'(?P<group>.+?)\s*/\s*(?P<owner>.+)'),
       
    76         re.compile(r'(?P<owner>.+)'),
       
    77 )
       
    78 
       
    79 ZONE_OWNER_MAIL = re.compile(r'(?P<owner>.*?)\s*<(?P<mail>.+?)>')
       
    80 
       
    81 def process_zone_comment (options, hostname, comment) :
       
    82     """
       
    83         Attempt to parse a host comment field... :D
       
    84     """
       
    85 
       
    86     yield 'comment', comment
       
    87     
       
    88     for regex in ZONE_COMMENTS :
       
    89         match = regex.match(comment)
       
    90 
       
    91         if match :
       
    92             break
       
    93     else :
       
    94         log.warn("%s: unparsed comment: %s", hostname, comment)
       
    95         return
       
    96     
       
    97     matches = match.groupdict()
       
    98     owner = matches.pop('owner', None)
       
    99     
       
   100     if owner :
       
   101         mail_match = ZONE_OWNER_MAIL.match(owner)
       
   102 
       
   103         if mail_match :
       
   104             mail_matches = mail_match.groupdict()
       
   105             
       
   106             owner = mail_matches['owner']
       
   107             yield 'comment-mail', mail_matches['mail']
       
   108         else :
       
   109             mail_matches = { }
       
   110     else :
       
   111         mail_matches = { }
       
   112 
       
   113     yield 'comment-owner', owner
       
   114 
       
   115     for group, value in matches.iteritems() :
       
   116         if value :
       
   117             yield 'comment-{group}'.format(group=group), value.strip()
       
   118     
       
   119     print u"{hostname:20} {comment:80} = {group:15} / {owner:20} <{mail:20}> / {host}".format(
       
   120             hostname    = hostname,
       
   121             comment     = comment,
       
   122             group       = matches.get('group', ''),
       
   123             owner       = owner,
       
   124             mail        = mail_matches.get('mail', ''),
       
   125             host        = matches.get('host', ''),
       
   126     ).encode('utf-8')
       
   127 
       
   128 def process_zone_hosts (options, file) :
       
   129     """
       
   130         Yield host info from zonefile records.
       
   131     """
       
   132 
       
   133     for rr in pvl.dns.zone.ZoneRecord.load(file) :
       
   134         if options.zone_unused and rr.name == options.zone_unused :
       
   135             log.debug("%s: skip %s", rr.name, rr)
       
   136             continue
       
   137 
       
   138         elif rr.type == 'A' :
       
   139             ip, = rr.data
       
   140 
       
   141             yield rr.name, 'ip', ip
       
   142 
       
   143             if rr.comment :
       
   144                 for field, value in process_zone_comment(options, rr.name, rr.comment) :
       
   145                     yield rr.name, field, value
       
   146 
       
   147         elif rr.type == 'CNAME' :
       
   148             host, = rr.data
       
   149 
       
   150             yield host, 'alias', rr.name
       
   151 
       
   152         else :
       
   153             log.warn("%s: unknown rr: %s", rr.name, rr)
       
   154 
       
   155 def process_dhcp_host (options, host, items) :
       
   156     """
       
   157         Yield host infos from a dhcp host ... { ... }
       
   158     """
       
   159 
       
   160     hostname = None
       
   161     ethernet = []
       
   162     fixed_address = None
       
   163 
       
   164     for item in items :
       
   165         item, args = item[0], item[1:]
       
   166 
       
   167         if item == 'hardware' :
       
   168             _ethernet, ethernet = args
       
   169             assert _ethernet == 'ethernet'
       
   170         elif item == 'fixed-address' :
       
   171             fixed_address, = args
       
   172         elif item == 'option' :
       
   173             option = args.pop(0)
       
   174 
       
   175             if option == 'host-name' :
       
   176                 hostname, = args
       
   177             else :
       
   178                 log.warn("host %s: ignore unknown option: %s", host, option)
       
   179         else :
       
   180             log.warn("host %s: ignore unknown item: %s", host, item)
       
   181 
       
   182     # determine hostname
       
   183     if hostname :
       
   184         pass
       
   185     elif fixed_address and not re.match(r'\d+\.\d+\.\d+.\d+', fixed_address) :
       
   186         hostname, domain = fixed_address.split('.', 1)
       
   187     elif '-' in host :
       
   188         hostname, suffix = host.rsplit('-', 1)
       
   189     else :
       
   190         log.warn("%s: guess hostname: %s", host, host)
       
   191         hostname = host
       
   192 
       
   193     if hostname :
       
   194         yield hostname, 'ethernet', ethernet
       
   195 
       
   196 def process_dhcp_hosts (options, blocks) :
       
   197     """
       
   198         Process hosts from a parsed block
       
   199     """
       
   200 
       
   201     for block, items, blocks in blocks :
       
   202         log.info("%s", block)
       
   203         
       
   204         block, args = block[0], block[1:]
       
   205 
       
   206         if block == 'group' :
       
   207             for info in process_dhcp_hosts(options, blocks) :
       
   208                 yield info
       
   209         elif block == 'host' :
       
   210             host, = args
       
   211 
       
   212             try :
       
   213                 for info in process_dhcp_host(options, host, items) :
       
   214                     yield info
       
   215             except ValueError as error :
       
   216                 log.warn("%s: invalid host: %s", host, error)
       
   217         else:
       
   218             log.warn("ignore unknown block: %s", block)
       
   219 
       
   220 def process_dhcp_conf (options, file) :
       
   221     items, blocks = pvl.dhcp.config.DHCPConfigParser().load(file)
       
   222 
       
   223     for item in items :
       
   224         item, args = item[0], item[1:]
       
   225 
       
   226         if item == 'include' :
       
   227             include, = args
       
   228             for info in process_dhcp_conf(options, pvl.args.apply_file(include)) :
       
   229                 yield info
       
   230         else :
       
   231             log.warn("ignore unknown item: %s", item)
       
   232     
       
   233     for info in process_dhcp_hosts(options, blocks) :
       
   234         yield info
       
   235 
       
   236 def apply_hosts_import (options) :
       
   237     """
       
   238         Import host infos from given files.
       
   239     """
       
   240 
       
   241     if options.import_zone_hosts:
       
   242         for info in process_zone_hosts(options,
       
   243                 pvl.args.apply_file(options.import_zone_hosts)) :
       
   244             yield info
       
   245     
       
   246     if options.import_dhcp_hosts:
       
   247         for info in process_dhcp_conf(options,
       
   248                 pvl.args.apply_file(options.import_dhcp_hosts)) :
       
   249             yield info
       
   250  
       
   251 def process_hosts_import (options, import_hosts) :
       
   252     """
       
   253         Import host definitions from given infos
       
   254     """
       
   255 
       
   256     hosts = collections.defaultdict(lambda: collections.defaultdict(list))
       
   257 
       
   258     for host, field, value in import_hosts :
       
   259         hosts[host][field].append(value)
       
   260     
       
   261     return hosts.iteritems()
       
   262 
       
   263 
       
   264 def main (argv) :
       
   265     options, args = parse_options(argv)
       
   266     
       
   267     if args :
       
   268         # direct from file
       
   269         hosts = pvl.args.apply_files(args, 'r', options.input_charset)
       
   270     else :
       
   271         # import
       
   272         import_hosts = apply_hosts_import(options)
       
   273         hosts = process_hosts_import(options, import_hosts)
       
   274    
       
   275     # output
       
   276     if options.output_hosts :
       
   277         for host, fields in hosts :
       
   278             print host
       
   279 
       
   280             for field, values in fields.iteritems() :
       
   281                 for value in values :
       
   282                     print "\t", field, "\t", value.encode(options.output_charset)
       
   283 
       
   284     return 0
       
   285 
       
   286 if __name__ == '__main__':
       
   287     pvl.args.main(main)