bin/pvl.dns-zone
changeset 293 6351acf3eb3b
parent 258 1ad9cec4f556
child 294 29720bbc5379
equal deleted inserted replaced
292:cc4a4a2f2b76 293:6351acf3eb3b
    95     # apply
    95     # apply
    96     pvl.args.apply(options, prog)
    96     pvl.args.apply(options, prog)
    97 
    97 
    98     return options, args
    98     return options, args
    99 
    99 
       
   100 def apply_zone_input (options, args) :
       
   101     """
       
   102         Yield ZoneLine, ZoneRecord pairs from files.
       
   103     """
       
   104 
       
   105     for file in pvl.args.apply_files(args, 'r', options.input_charset) :
       
   106         log.info("Reading zone: %s", file)
       
   107 
       
   108         for line, record in pvl.dns.zone.ZoneLine.load(file, 
       
   109                 line_timestamp_prefix   = options.input_line_date,
       
   110         ) :
       
   111             yield line, record
       
   112 
       
   113 # TODO: --check-types to limit this to A/AAAA/CNAME etc
   100 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) :
   114 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) :
   101     """
   115     """
   102         Parse host/IP pairs from the zone, and verify that they are unique.
   116         Parse host/IP pairs from the zone, and verify that they are unique.
   103 
   117 
   104         As an exception, names listed in the given whitelist may have multiple IPs.
   118         As an exception, names listed in the given whitelist may have multiple IPs.
   109 
   123 
   110     fail = None
   124     fail = None
   111 
   125 
   112     last_name = None
   126     last_name = None
   113 
   127 
   114     for r in zone :
   128     for l, r in zone :
   115         name = r.name or last_name
   129         if r :
   116 
   130             name = r.name or last_name
   117         name = (r.origin, name)
   131 
   118 
   132             name = (r.origin, name)
   119         # name
   133 
   120         if r.type not in whitelist_types :
   134             # name
   121             if name not in by_name :
   135             if r.type not in whitelist_types :
   122                 by_name[name] = r
   136                 if name not in by_name :
   123 
   137                     by_name[name] = r
   124             elif r.name in whitelist :
   138 
   125                 log.debug("Duplicate whitelist entry: %s", r)
   139                 elif r.name in whitelist :
   126 
   140                     log.debug("Duplicate whitelist entry: %s", r)
   127             else :
   141 
   128                 # fail!
   142                 else :
   129                 log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name])
   143                     # fail!
   130                 fail = True
   144                     log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name])
   131 
   145                     fail = True
   132         # ip
   146 
   133         if r.type == 'A' :
   147             # ip
   134             ip, = r.data
   148             if r.type == 'A' :
   135 
   149                 ip, = r.data
   136             if ip not in by_ip :
   150 
   137                 by_ip[ip] = r
   151                 if ip not in by_ip :
   138 
   152                     by_ip[ip] = r
   139             else :
   153 
   140                 # fail!
   154                 else :
   141                 log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip])
   155                     # fail!
   142                 fail = True
   156                     log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip])
   143 
   157                     fail = True
   144     return fail
   158     
   145 
   159     if fail :
   146 def process_zone_soa (soa, serial) :
   160         log.error("Check failed, see warnings")
   147     return pvl.dns.zone.SOA(
   161         sys.exit(2)
   148         soa.master, soa.contact,
   162 
   149         serial, soa.refresh, soa.retry, soa.expire, soa.nxttl
   163     yield l, r
   150     )
       
   151 
   164 
   152 def process_zone_serial (zone, serial) :
   165 def process_zone_serial (zone, serial) :
   153     for rr in zone :
   166     """
   154         if rr.type == 'SOA' :
   167         Update the serial in the SOA record.
       
   168     """
       
   169 
       
   170     for line, rr in zone :
       
   171         if rr and rr.type == 'SOA' :
   155             # XXX: as SOA record..
   172             # XXX: as SOA record..
   156             yield process_zone_soa(pvl.dns.zone.SOA.parse(rr.line), serial)
   173             soa = pvl.dns.zone.SOA.parse(line)
   157         else :
   174 
   158             yield rr
   175             yield line, pvl.dns.zone.SOA(
       
   176                     soa.master, soa.contact,
       
   177                     serial, soa.refresh, soa.retry, soa.expire, soa.nxttl
       
   178             )
       
   179         else :
       
   180             yield line, rr
   159 
   181 
   160 def process_zone_forwards (zone, txt=False, mx=False) :
   182 def process_zone_forwards (zone, txt=False, mx=False) :
   161     """
   183     """
   162         Process zone data -> forward zone data.
   184         Process zone data -> forward zone data.
   163     """
   185     """
   164 
   186 
   165     for r in zone :
   187     for line, r in zone :
   166         yield r
   188         yield line, r
   167 
   189 
   168         if r.type == 'A' :
   190         if r and r.type == 'A' :
   169             if txt :
   191             if txt :
   170                 # comment?
   192                 # comment?
   171                 comment = r.line.comment
   193                 comment = r.line.comment
   172 
   194 
   173                 if comment :
   195                 if comment :
   174                     yield ZoneRecord.TXT(None, comment, ttl=r.ttl)
   196                     yield line, ZoneRecord.TXT(None, comment, ttl=r.ttl)
   175 
   197 
   176            
   198            
   177             # XXX: RP, do we need it?
   199             # XXX: RP, do we need it?
   178 
   200 
   179             if mx :
   201             if mx :
   180                 # XXX: is this even a good idea?
   202                 # XXX: is this even a good idea?
   181                 yield ZoneRecord.MX(None, 10, mx, ttl=r.ttl)
   203                 yield line, ZoneRecord.MX(None, 10, mx, ttl=r.ttl)
   182 
   204 
   183 def process_zone_meta (zone, ignore=None) :
   205 def process_zone_meta (zone, ignore=None) :
   184     """
   206     """
   185         Process zone metadata -> output.
   207         Process zone metadata -> output.
   186     """
   208     """
   187     
   209     
   188     TIMESTAMP_FORMAT = '%Y/%m/%d'
   210     TIMESTAMP_FORMAT = '%Y/%m/%d'
   189     
   211     
   190     for r in zone :
   212     for line, r in zone :
   191         if ignore and r.name in ignore :
   213         if ignore and r.name in ignore :
   192             # skip
   214             # skip
   193             log.debug("Ignore record: %s", r)
   215             log.debug("Ignore record: %s", r)
   194             continue
   216             continue
   195 
   217 
   197         if r.type == 'A' :
   219         if r.type == 'A' :
   198             # timestamp?
   220             # timestamp?
   199             timestamp = r.line.timestamp
   221             timestamp = r.line.timestamp
   200 
   222 
   201             if timestamp :
   223             if timestamp :
   202                 yield ZoneRecord.TXT(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl)
   224                 yield line, ZoneRecord.TXT(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl)
   203      
   225      
   204 def process_zone_reverse (zone, origin, domain) :
   226 def process_zone_reverse (zone, origin, domain) :
   205     """
   227     """
   206         Process zone data -> reverse zone data.
   228         Process zone data -> reverse zone data.
   207     """
   229     """
   208 
   230 
   209     name = None
   231     for line, r in zone :
   210 
   232         if r and r.type == 'A' :
   211     for r in zone :
       
   212         # keep name from previous..
       
   213         if r.name :
       
   214             name = r.name
       
   215 
       
   216         if r.type == 'A' :
       
   217             ip, = r.data
   233             ip, = r.data
   218             ptr = reverse_ipv4(ip)
   234             ptr = reverse_ipv4(ip)
   219 
   235 
   220         elif r.type == 'AAAA' :
   236         elif r and r.type == 'AAAA' :
   221             ip, = r.data
   237             ip, = r.data
   222             ptr = reverse_ipv6(ip)
   238             ptr = reverse_ipv6(ip)
   223             
   239             
   224         else :
   240         else :
       
   241             yield line, r
   225             continue
   242             continue
   226 
   243 
   227         # verify
   244         # verify
   228         if zone and ptr.endswith(origin) :
   245         if zone and ptr.endswith(origin) :
   229             ptr = ptr[:-(len(origin) + 1)]
   246             ptr = ptr[:-(len(origin) + 1)]
   234 
   251 
   235         # domain to use
   252         # domain to use
   236         host_domain = r.origin or domain
   253         host_domain = r.origin or domain
   237         host_fqdn = fqdn(name, host_domain)
   254         host_fqdn = fqdn(name, host_domain)
   238 
   255 
   239         yield ZoneRecord.PTR(ptr, host_fqdn)
   256         yield line, ZoneRecord.PTR(ptr, host_fqdn)
   240 
   257 
   241 def write_zone_records (file, zone) :
   258 def apply_zone_output (options, zone) :
   242     for r in zone :
   259     """
   243         file.write(unicode(r))
   260         Write out the resulting zonefile.
       
   261     """
       
   262 
       
   263     file = pvl.args.apply_file(options.output, 'w', options.output_charset)
       
   264 
       
   265     for line, r in zone :
       
   266         if r :
       
   267             file.write(unicode(r))
       
   268         else :
       
   269             file.write(line.line)
   244         file.write('\n')
   270         file.write('\n')
   245 
   271 
   246 def main (argv) :
   272 def main (argv) :
   247     options, args = parse_options(argv)
   273     options, args = parse_options(argv)
   248     
   274     
   249     # open files, default to stdout
   275     # input
   250     input_files = pvl.args.apply_files(args, 'r', options.input_charset)
   276     zone = apply_zone_input(options, args)
   251    
   277    
   252     # process zone data
       
   253     zone = []
       
   254 
       
   255     for file in input_files :
       
   256         log.info("Reading zone: %s", file)
       
   257 
       
   258         zone += list(pvl.dns.zone.ZoneRecord.load(file, 
       
   259             line_timestamp_prefix   = options.input_line_date,
       
   260         ))
       
   261 
       
   262     # check?
       
   263     if options.check_hosts :
   278     if options.check_hosts :
   264         whitelist = set(options.check_exempt)
   279         whitelist = set(options.check_exempt)
   265 
   280 
   266         log.debug("checking hosts; whitelist=%r", whitelist)
   281         log.info("Checking hosts: whitelist=%r", whitelist)
   267 
   282 
   268         if check_zone_hosts(zone, whitelist=whitelist) :
   283         zone = list(check_zone_hosts(zone, whitelist=whitelist))
   269             log.warn("Hosts check failed")
       
   270             return 2
       
   271 
       
   272         else :
       
   273             log.info("Hosts check OK")
       
   274 
   284 
   275     if options.serial :
   285     if options.serial :
   276         log.info("Set zone serial: %s", options.serial)
   286         log.info("Set zone serial: %s", options.serial)
   277 
   287 
   278         zone = list(process_zone_serial(zone, serial=options.serial))
   288         zone = list(process_zone_serial(zone, serial=options.serial))
   279 
   289 
   280     # output file
       
   281     output = open_file(options.output, 'w', options.output_charset)
       
   282 
       
   283     if options.forward_zone :
   290     if options.forward_zone :
   284         log.info("Write forward zone: %s", output)
   291         log.info("Generate forward zone...")
   285 
   292 
   286         zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx))
   293         zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx))
   287 
   294 
   288     elif options.meta_zone :
   295     if options.meta_zone :
   289         log.info("Write metadata zone: %s", output)
   296         log.info("Generate metadata zone...")
   290 
   297 
   291         if not options.input_line_date :
   298         if not options.input_line_date :
   292             log.error("--meta-zone requires --input-line-date")
   299             log.error("--meta-zone requires --input-line-date")
   293             return 1
   300             return 1
   294 
   301 
   295         zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore)))
   302         zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore)))
   296 
   303 
   297     elif options.reverse_zone :
   304     if options.reverse_zone :
   298         if ':' in options.reverse_zone :
   305         if ':' in options.reverse_zone :
   299             # IPv6
   306             # IPv6
   300             origin = reverse_ipv6(options.reverse_zone)
   307             origin = reverse_ipv6(options.reverse_zone)
   301 
   308 
   302         else :
   309         else :
   308         if not domain :
   315         if not domain :
   309             log.error("--reverse-zone requires --reverse-domain")
   316             log.error("--reverse-zone requires --reverse-domain")
   310             return 1
   317             return 1
   311 
   318 
   312         zone = list(process_zone_reverse(zone, origin=origin, domain=domain))
   319         zone = list(process_zone_reverse(zone, origin=origin, domain=domain))
   313 
   320     
   314     elif options.check_hosts :
   321     # output
   315         # we only did that, done
   322     apply_zone_output(options, zone)
   316         return 0
       
   317 
       
   318     else :
       
   319         # pass-through
       
   320         log.info("Passing through zonefile")
       
   321 
       
   322     write_zone_records(output, zone)
       
   323 
   323 
   324     return 0
   324     return 0
   325 
   325 
   326 if __name__ == '__main__':
   326 if __name__ == '__main__':
   327     import sys
   327     import sys