8 |
8 |
9 import optparse |
9 import optparse |
10 import codecs |
10 import codecs |
11 import logging |
11 import logging |
12 |
12 |
13 log = logging.getLogger() |
13 log = logging.getLogger('main') |
14 |
14 |
15 # command-line options, global state |
15 # command-line options, global state |
16 options = None |
16 options = None |
17 |
17 |
18 def parse_options (argv) : |
18 def parse_options (argv) : |
19 """ |
19 """ |
20 Parse command-line arguments. |
20 Parse command-line arguments. |
21 """ |
21 """ |
22 |
22 |
|
23 prog = argv[0] |
|
24 |
23 parser = optparse.OptionParser( |
25 parser = optparse.OptionParser( |
24 prog = argv[0], |
26 prog = prog, |
25 usage = '%prog: [options]', |
27 usage = '%prog: [options]', |
26 version = __version__, |
28 version = __version__, |
27 |
29 |
28 # module docstring |
30 # module docstring |
29 description = __doc__, |
31 description = __doc__, |
36 general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") |
38 general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") |
37 general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") |
39 general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") |
38 |
40 |
39 parser.add_option_group(general) |
41 parser.add_option_group(general) |
40 |
42 |
|
43 # input/output |
41 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
44 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
42 help="Encoding used for input files") |
45 help="Encoding used for input files") |
43 |
46 |
44 parser.add_option('-o', '--output', metavar='FILE', default='-', |
47 parser.add_option('-o', '--output', metavar='FILE', default='-', |
45 help="Write to output file; default stdout") |
48 help="Write to output file; default stdout") |
46 |
49 |
47 parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', |
50 parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', |
48 help="Encoding used for output files") |
51 help="Encoding used for output files") |
49 |
52 |
|
53 # check stage |
|
54 parser.add_option('--check-hosts', action='store_true', |
|
55 help="Check that host/IPs are unique. Use --quiet to silence warnings, and test exit status") |
|
56 |
|
57 parser.add_option('--check-exempt', action='append', |
|
58 help="Allow given names to have multiple records") |
|
59 |
|
60 # forward stage |
50 parser.add_option('--forward-zone', action='store_true', |
61 parser.add_option('--forward-zone', action='store_true', |
51 help="Generate forward zone") |
62 help="Generate forward zone") |
52 |
63 |
53 parser.add_option('--forward-txt', action='store_true', |
64 parser.add_option('--forward-txt', action='store_true', |
54 help="Generate TXT records for forward zone") |
65 help="Generate TXT records for forward zone") |
55 |
66 |
56 parser.add_option('--forward-mx', metavar='MX', |
67 parser.add_option('--forward-mx', metavar='MX', |
57 help="Generate MX records for forward zone") |
68 help="Generate MX records for forward zone") |
58 |
69 |
|
70 # reverse stage |
59 parser.add_option('--reverse-domain', metavar='DOMAIN', |
71 parser.add_option('--reverse-domain', metavar='DOMAIN', |
60 help="Domain to use for hosts in reverse zone") |
72 help="Domain to use for hosts in reverse zone") |
61 |
73 |
62 parser.add_option('--reverse-zone', metavar='NET', |
74 parser.add_option('--reverse-zone', metavar='NET', |
63 help="Generate forward zone for given subnet (x.z.y)") |
75 help="Generate forward zone for given subnet (x.z.y)") |
64 |
76 |
65 # defaults |
77 # defaults |
66 parser.set_defaults( |
78 parser.set_defaults( |
67 loglevel = logging.WARN, |
79 loglevel = logging.WARN, |
|
80 |
|
81 check_exempt = [], |
68 ) |
82 ) |
69 |
83 |
70 # parse |
84 # parse |
71 options, args = parser.parse_args(argv[1:]) |
85 options, args = parser.parse_args(argv[1:]) |
72 |
86 |
73 # configure |
87 # configure |
74 logging.basicConfig( |
88 logging.basicConfig( |
75 format = '%(processName)s: %(name)s: %(levelname)s %(funcName)s : %(message)s', |
89 format = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s', |
76 level = options.loglevel, |
90 level = options.loglevel, |
77 ) |
91 ) |
78 |
92 |
79 return options, args |
93 return options, args |
80 |
94 |
164 for lineno, line in enumerate(file) : |
178 for lineno, line in enumerate(file) : |
165 data = parse_record(line) |
179 data = parse_record(line) |
166 |
180 |
167 if data : |
181 if data : |
168 yield data |
182 yield data |
|
183 |
|
184 def check_zone_hosts (zone, whitelist=None) : |
|
185 """ |
|
186 Parse host/IP pairs from the zone, and verify that they are unique. |
|
187 |
|
188 As an exception, names listed in the given whitelist may have multiple IPs. |
|
189 """ |
|
190 |
|
191 by_name = {} |
|
192 by_ip = {} |
|
193 |
|
194 fail = None |
|
195 |
|
196 for item in zone : |
|
197 text = ' '.join(pp for pp in item if pp) |
|
198 name, ttl, type, data, comment = item |
|
199 |
|
200 # name |
|
201 if name not in by_name : |
|
202 by_name[name] = text |
|
203 |
|
204 elif name in whitelist : |
|
205 log.debug("Duplicate whitelist entry: %r", item) |
|
206 |
|
207 else : |
|
208 # fail! |
|
209 log.warn("Duplicate name: %s <-> %s", text, by_name[name]) |
|
210 fail = True |
|
211 |
|
212 # ip |
|
213 if type == 'A' : |
|
214 ip = data |
|
215 |
|
216 if ip not in by_ip : |
|
217 by_ip[ip] = text |
|
218 |
|
219 else : |
|
220 # fail! |
|
221 log.warn("Duplicate IP: %s <-> %s", text, by_ip[ip]) |
|
222 fail = True |
|
223 |
|
224 return fail |
169 |
225 |
170 def process_zone_forwards (zone, txt=False, mx=False) : |
226 def process_zone_forwards (zone, txt=False, mx=False) : |
171 """ |
227 """ |
172 Process zone data -> forward zone data. |
228 Process zone data -> forward zone data. |
173 """ |
229 """ |
305 for file in input_files : |
361 for file in input_files : |
306 log.info("Reading zone: %s", file) |
362 log.info("Reading zone: %s", file) |
307 |
363 |
308 zone += list(parse_zone(file)) |
364 zone += list(parse_zone(file)) |
309 |
365 |
|
366 # check? |
|
367 if options.check_hosts : |
|
368 whitelist = set(options.check_exempt) |
|
369 |
|
370 log.debug("checking hosts; whitelist=%r", whitelist) |
|
371 |
|
372 if check_zone_hosts(zone, whitelist=whitelist) : |
|
373 log.warn("Hosts check failed") |
|
374 return 2 |
|
375 |
|
376 else : |
|
377 log.info("Hosts check OK") |
|
378 |
310 # output file |
379 # output file |
311 output = open_file(options.output, 'w', options.output_charset) |
380 output = open_file(options.output, 'w', options.output_charset) |
312 |
381 |
313 if options.forward_zone : |
382 if options.forward_zone : |
314 log.info("Write forward zone: %s", output) |
383 log.info("Write forward zone: %s", output) |