1 #!/usr/bin/env python |
|
2 |
|
3 """ |
|
4 Process zonefiles. |
|
5 """ |
|
6 |
|
7 __version__ = '0.0.1-dev' |
|
8 |
|
9 import optparse |
|
10 import codecs |
|
11 from datetime import datetime |
|
12 import logging |
|
13 |
|
14 import ipaddr |
|
15 |
|
16 log = logging.getLogger('main') |
|
17 |
|
18 # command-line options, global state |
|
19 options = None |
|
20 |
|
21 def parse_options (argv) : |
|
22 """ |
|
23 Parse command-line arguments. |
|
24 """ |
|
25 |
|
26 prog = argv[0] |
|
27 |
|
28 parser = optparse.OptionParser( |
|
29 prog = prog, |
|
30 usage = '%prog: [options]', |
|
31 version = __version__, |
|
32 |
|
33 # module docstring |
|
34 description = __doc__, |
|
35 ) |
|
36 |
|
37 # logging |
|
38 general = optparse.OptionGroup(parser, "General Options") |
|
39 |
|
40 general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") |
|
41 general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") |
|
42 general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") |
|
43 |
|
44 parser.add_option_group(general) |
|
45 |
|
46 # input/output |
|
47 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
|
48 help="Encoding used for input files") |
|
49 |
|
50 parser.add_option('-o', '--output', metavar='FILE', default='-', |
|
51 help="Write to output file; default stdout") |
|
52 |
|
53 parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', |
|
54 help="Encoding used for output files") |
|
55 |
|
56 # check stage |
|
57 parser.add_option('--check-hosts', action='store_true', |
|
58 help="Check that host/IPs are unique. Use --quiet to silence warnings, and test exit status") |
|
59 |
|
60 parser.add_option('--check-exempt', metavar='HOST', action='append', |
|
61 help="Allow given names to have multiple records") |
|
62 |
|
63 # meta stage |
|
64 parser.add_option('--meta-zone', action='store_true', |
|
65 help="Generate host metadata zone; requires --input-line-date") |
|
66 |
|
67 parser.add_option('--meta-ignore', metavar='HOST', action='append', |
|
68 help="Ignore given hostnames in metadata output") |
|
69 |
|
70 parser.add_option('--input-line-date', action='store_true', |
|
71 help="Parse timestamp prefix from each input line (e.g. `hg blame | ...`)") |
|
72 |
|
73 # forward stage |
|
74 parser.add_option('--forward-zone', action='store_true', |
|
75 help="Generate forward zone") |
|
76 |
|
77 parser.add_option('--forward-txt', action='store_true', |
|
78 help="Generate TXT records for forward zone") |
|
79 |
|
80 parser.add_option('--forward-mx', metavar='MX', |
|
81 help="Generate MX records for forward zone") |
|
82 |
|
83 # reverse stage |
|
84 parser.add_option('--reverse-domain', metavar='DOMAIN', |
|
85 help="Domain to use for hosts in reverse zone") |
|
86 |
|
87 parser.add_option('--reverse-zone', metavar='NET', |
|
88 help="Generate forward zone for given subnet (x.z.y | a:b:c:d)") |
|
89 |
|
90 # |
|
91 parser.add_option('--doctest', action='store_true', |
|
92 help="Run module doctests") |
|
93 |
|
94 # defaults |
|
95 parser.set_defaults( |
|
96 loglevel = logging.WARN, |
|
97 |
|
98 # XXX: combine |
|
99 check_exempt = [], |
|
100 meta_ignore = [], |
|
101 ) |
|
102 |
|
103 # parse |
|
104 options, args = parser.parse_args(argv[1:]) |
|
105 |
|
106 # configure |
|
107 logging.basicConfig( |
|
108 format = prog + ': %(name)s: %(levelname)s %(funcName)s : %(message)s', |
|
109 level = options.loglevel, |
|
110 ) |
|
111 |
|
112 return options, args |
|
113 |
|
114 class ZoneError (Exception) : |
|
115 pass |
|
116 |
|
117 class ZoneLineError (ZoneError) : |
|
118 """ |
|
119 ZoneLine-related error |
|
120 """ |
|
121 |
|
122 def __init__ (self, line, msg, *args, **kwargs) : |
|
123 super(ZoneLineError, self).__init__("%s: %s" % (line, msg.format(*args, **kwargs))) |
|
124 |
|
125 class ZoneLine (object) : |
|
126 """ |
|
127 A line in a zonefile. |
|
128 """ |
|
129 |
|
130 file = None |
|
131 lineno = None |
|
132 |
|
133 # data |
|
134 indent = None # was the line indented? |
|
135 data = None |
|
136 parts = None # split line fields |
|
137 |
|
138 # optional |
|
139 timestamp = None |
|
140 comment = None |
|
141 |
|
142 PARSE_DATETIME_FORMAT = '%Y-%m-%d' |
|
143 |
|
144 @classmethod |
|
145 def parse (cls, file, lineno, line, line_timestamp_prefix=False) : |
|
146 """ |
|
147 Parse out given line and build. |
|
148 """ |
|
149 |
|
150 log.debug("parse: %s:%d: %s", file, lineno, line) |
|
151 |
|
152 ts = None |
|
153 |
|
154 if line_timestamp_prefix : |
|
155 if ': ' not in line : |
|
156 raise ZoneError("%s:%d: Missing timestamp prefix: %s" % (file, lineno, line)) |
|
157 |
|
158 # split prefix |
|
159 prefix, line = line.split(': ', 1) |
|
160 |
|
161 # parse it out |
|
162 ts = datetime.strptime(prefix, cls.PARSE_DATETIME_FORMAT) |
|
163 |
|
164 log.debug(" ts=%r", ts) |
|
165 |
|
166 # was line indented? |
|
167 indent = line.startswith(' ') or line.startswith('\t') |
|
168 |
|
169 # strip |
|
170 line = line.strip() |
|
171 |
|
172 log.debug(" indent=%r, line=%r", indent, line) |
|
173 |
|
174 # parse comment out? |
|
175 if ';' in line : |
|
176 line, comment = line.split(';', 1) |
|
177 |
|
178 line = line.strip() |
|
179 comment = comment.strip() |
|
180 |
|
181 else : |
|
182 line = line.strip() |
|
183 comment = None |
|
184 |
|
185 log.debug(" line=%r, comment=%r", line, comment) |
|
186 |
|
187 # parse fields |
|
188 if '"' in line : |
|
189 pre, data, post = line.split('"', 2) |
|
190 parts = pre.split() + [data] + post.split() |
|
191 |
|
192 else : |
|
193 parts = line.split() |
|
194 |
|
195 log.debug(" parts=%r", parts) |
|
196 |
|
197 # build |
|
198 return cls(file, lineno, indent, line, parts, timestamp=ts, comment=comment) |
|
199 |
|
200 def __init__ (self, file, lineno, indent, data, parts, timestamp=None, comment=None) : |
|
201 self.file = file |
|
202 self.lineno = lineno |
|
203 |
|
204 self.indent = indent |
|
205 self.data = data |
|
206 self.parts = parts |
|
207 |
|
208 self.timestamp = timestamp |
|
209 self.comment = comment |
|
210 |
|
211 def __str__ (self) : |
|
212 return "{file}:{lineno}".format(file=self.file, lineno=self.lineno) |
|
213 |
|
214 class ZoneRecord (object) : |
|
215 """ |
|
216 A record from a zonefile. |
|
217 """ |
|
218 |
|
219 # the underlying line |
|
220 line = None |
|
221 |
|
222 # possible $ORIGIN context |
|
223 origin = None |
|
224 |
|
225 # record fields |
|
226 name = None |
|
227 type = None |
|
228 |
|
229 # list of data fields |
|
230 data = None |
|
231 |
|
232 # optional |
|
233 ttl = None |
|
234 cls = None |
|
235 |
|
236 @classmethod |
|
237 def parse (cls, line, parts=None, origin=None) : |
|
238 """ |
|
239 Parse from ZoneLine. Returns None if there is no record on the line.. |
|
240 """ |
|
241 |
|
242 if parts is None : |
|
243 parts = list(line.parts) |
|
244 |
|
245 if not parts : |
|
246 # skip |
|
247 return |
|
248 |
|
249 # XXX: indented lines keep name from previous record |
|
250 if line.indent : |
|
251 name = None |
|
252 |
|
253 else : |
|
254 name = parts.pop(0) |
|
255 |
|
256 log.debug(" name=%r, origin=%r", name, origin) |
|
257 |
|
258 if len(parts) < 2 : |
|
259 raise ZoneLineError(line, "Too few parts to parse: {0!r}", line.data) |
|
260 |
|
261 # parse ttl/cls/type |
|
262 ttl = _cls = None |
|
263 |
|
264 if parts and parts[0][0].isdigit() : |
|
265 ttl = parts.pop(0) |
|
266 |
|
267 if parts and parts[0].upper() in ('IN', 'CH') : |
|
268 _cls = parts.pop(0) |
|
269 |
|
270 # always have type |
|
271 type = parts.pop(0) |
|
272 |
|
273 # remaining parts are data |
|
274 data = parts |
|
275 |
|
276 log.debug(" ttl=%r, cls=%r, type=%r, data=%r", ttl, _cls, type, data) |
|
277 |
|
278 return cls(name, type, data, |
|
279 origin = origin, |
|
280 ttl = ttl, |
|
281 cls = _cls, |
|
282 line = line, |
|
283 ) |
|
284 |
|
285 def __init__ (self, name, type, data, origin=None, ttl=None, cls=None, line=None, comment=None) : |
|
286 self.name = name |
|
287 self.type = type |
|
288 self.data = data |
|
289 |
|
290 self.ttl = ttl |
|
291 self.cls = cls |
|
292 |
|
293 self.origin = origin |
|
294 self.line = line |
|
295 |
|
296 # XXX: within line |
|
297 self._comment = comment |
|
298 |
|
299 def build_line (self) : |
|
300 """ |
|
301 Construct a zonefile-format line..." |
|
302 """ |
|
303 |
|
304 # XXX: comment? |
|
305 if self._comment : |
|
306 comment = '\t; ' + self._comment |
|
307 else : |
|
308 comment = '' |
|
309 |
|
310 return u"{name:25} {ttl:4} {cls:2} {type:5} {data}{comment}".format( |
|
311 name = self.name or '', |
|
312 ttl = self.ttl or '', |
|
313 cls = self.cls or '', |
|
314 type = self.type, |
|
315 data = ' '.join(unicode(data) for data in self.data), |
|
316 comment = comment, |
|
317 ) |
|
318 |
|
319 def __str__ (self) : |
|
320 return ' '.join((self.name or '', self.type, ' '.join(self.data))) |
|
321 |
|
322 class TXTRecord (ZoneRecord) : |
|
323 """ |
|
324 TXT record. |
|
325 """ |
|
326 |
|
327 def __init__ (self, name, text, **opts) : |
|
328 return super(TXTRecord, self).__init__(name, 'TXT', |
|
329 [u'"{0}"'.format(text.replace('"', '\\"'))], |
|
330 **opts |
|
331 ) |
|
332 |
|
333 class OffsetValue (object) : |
|
334 def __init__ (self, value) : |
|
335 self.value = value |
|
336 |
|
337 def __getitem__ (self, offset) : |
|
338 value = self.value + offset |
|
339 |
|
340 #log.debug("OffsetValue: %d[%d] -> %d", self.value, offset, value) |
|
341 |
|
342 return value |
|
343 |
|
344 def parse_generate_field (line, field) : |
|
345 """ |
|
346 Parse a $GENERATE lhs/rhs field: |
|
347 $ |
|
348 ${<offset>[,<width>[,<base>]]} |
|
349 \$ |
|
350 $$ |
|
351 |
|
352 Returns a wrapper that builds the field-value when called with the index. |
|
353 |
|
354 >>> parse_generate_field(None, "foo")(1) |
|
355 'foo' |
|
356 >>> parse_generate_field(None, "foo-$")(1) |
|
357 'foo-1' |
|
358 >>> parse_generate_field(None, "foo-$$")(1) |
|
359 'foo-$' |
|
360 >>> parse_generate_field(None, "\$")(1) |
|
361 '$' |
|
362 >>> parse_generate_field(None, "10.0.0.${100}")(1) |
|
363 '10.0.0.101' |
|
364 >>> parse_generate_field(None, "foo-${0,2,d}")(1) |
|
365 'foo-01' |
|
366 |
|
367 """ |
|
368 |
|
369 input = field |
|
370 expr = [] |
|
371 |
|
372 while '$' in field : |
|
373 # defaults |
|
374 offset = 0 |
|
375 width = 0 |
|
376 base = 'd' |
|
377 escape = False |
|
378 |
|
379 # different forms |
|
380 if '${' in field : |
|
381 pre, body = field.split('${', 1) |
|
382 body, post = body.split('}', 1) |
|
383 |
|
384 # parse body |
|
385 parts = body.split(',') |
|
386 |
|
387 # offset |
|
388 offset = int(parts.pop(0)) |
|
389 |
|
390 # width |
|
391 if parts : |
|
392 width = int(parts.pop(0)) |
|
393 |
|
394 # base |
|
395 if parts : |
|
396 base = parts.pop(0) |
|
397 |
|
398 if parts: |
|
399 # fail |
|
400 raise ZoneLineError(line, "extra data in ${...} body: {0!r}", parts) |
|
401 |
|
402 elif '$$' in field : |
|
403 pre, post = field.split('$$', 1) |
|
404 escape = True |
|
405 |
|
406 elif '\\$' in field : |
|
407 pre, post = field.split('\\$', 1) |
|
408 escape = True |
|
409 |
|
410 else : |
|
411 pre, post = field.split('$', 1) |
|
412 |
|
413 expr.append(pre) |
|
414 |
|
415 if escape : |
|
416 expr.append('$') |
|
417 |
|
418 else : |
|
419 # meta-format |
|
420 fmt = '{value[%d]:0%d%s}' % (offset, width, base) |
|
421 |
|
422 log.debug("field=%r -> pre=%r, fmt=%r, post=%r", field, expr, fmt, post) |
|
423 |
|
424 expr.append(fmt) |
|
425 |
|
426 field = post |
|
427 |
|
428 # final |
|
429 if field : |
|
430 expr.append(field) |
|
431 |
|
432 # combine |
|
433 expr = ''.join(expr) |
|
434 |
|
435 log.debug("%s: %s", input, expr) |
|
436 |
|
437 # processed |
|
438 def value_func (value) : |
|
439 # magic wrapper to implement offsets |
|
440 return expr.format(value=OffsetValue(value)) |
|
441 |
|
442 return value_func |
|
443 |
|
444 def process_generate (line, origin, parts) : |
|
445 """ |
|
446 Process a |
|
447 $GENERATE <start>-<stop>[/<step>] lhs [ttl] [class] type rhs [comment] |
|
448 directive into a series of ZoneResource's. |
|
449 """ |
|
450 |
|
451 range = parts.pop(0) |
|
452 |
|
453 # parse range |
|
454 if '/' in range : |
|
455 range, step = range.split('/') |
|
456 step = int(step) |
|
457 else : |
|
458 step = 1 |
|
459 |
|
460 start, stop = range.split('-') |
|
461 start = int(start) |
|
462 stop = int(stop) |
|
463 |
|
464 log.debug(" range: start=%r, stop=%r, step=%r", start, stop, step) |
|
465 |
|
466 # inclusive |
|
467 range = xrange(start, stop + 1, step) |
|
468 |
|
469 lhs_func = parse_generate_field(line, parts.pop(0)) |
|
470 rhs_func = parse_generate_field(line, parts.pop(-1)) |
|
471 body = parts |
|
472 |
|
473 for i in range : |
|
474 # build |
|
475 parts = [lhs_func(i)] + body + [rhs_func(i)] |
|
476 |
|
477 log.debug(" %03d: %r", i, parts) |
|
478 |
|
479 # parse |
|
480 yield ZoneRecord.parse(line, parts=parts, origin=origin) |
|
481 |
|
482 def parse_zone_records (file, origin=None, **opts) : |
|
483 """ |
|
484 Parse ZoneRecord items from the given zonefile, ignoring non-record lines. |
|
485 """ |
|
486 |
|
487 ttl = None |
|
488 |
|
489 skip_multiline = False |
|
490 |
|
491 for lineno, raw_line in enumerate(file) : |
|
492 # parse comment |
|
493 if ';' in raw_line : |
|
494 line, comment = raw_line.split(';', 1) |
|
495 else : |
|
496 line = raw_line |
|
497 comment = None |
|
498 |
|
499 # XXX: handle multi-line statements... |
|
500 # start |
|
501 if '(' in line : |
|
502 skip_multiline = True |
|
503 |
|
504 log.warn("%s:%d: Start of multi-line statement: %s", file.name, lineno, raw_line) |
|
505 |
|
506 # end? |
|
507 if ')' in line : |
|
508 skip_multiline = False |
|
509 |
|
510 log.warn("%s:%d: End of multi-line statement: %s", file.name, lineno, raw_line) |
|
511 |
|
512 continue |
|
513 |
|
514 elif skip_multiline : |
|
515 log.warn("%s:%d: Multi-line statement: %s", file.name, lineno, raw_line) |
|
516 |
|
517 continue |
|
518 |
|
519 # parse |
|
520 line = ZoneLine.parse(file.name, lineno, raw_line, **opts) |
|
521 |
|
522 if not line.data : |
|
523 log.debug("%s: skip empty line: %s", line, raw_line) |
|
524 |
|
525 continue |
|
526 |
|
527 elif line.data.startswith('$') : |
|
528 # control record |
|
529 type = line.parts[0] |
|
530 |
|
531 if type == '$ORIGIN': |
|
532 # update |
|
533 origin = line.parts[1] |
|
534 |
|
535 log.info("%s: origin: %s", line, origin) |
|
536 |
|
537 elif type == '$GENERATE': |
|
538 # process... |
|
539 log.info("%s: generate: %s", line, line.parts) |
|
540 |
|
541 for record in process_generate(line, origin, line.parts[1:]) : |
|
542 yield record |
|
543 |
|
544 else : |
|
545 log.warning("%s: skip control record: %s", line, line.data) |
|
546 |
|
547 # XXX: passthrough! |
|
548 continue |
|
549 |
|
550 # normal record? |
|
551 record = ZoneRecord.parse(line, origin=origin) |
|
552 |
|
553 if record : |
|
554 yield record |
|
555 |
|
556 else : |
|
557 # unknown |
|
558 log.warning("%s: skip unknown line: %s", line, line.data) |
|
559 |
|
560 def check_zone_hosts (zone, whitelist=None, whitelist_types=set(['TXT'])) : |
|
561 """ |
|
562 Parse host/IP pairs from the zone, and verify that they are unique. |
|
563 |
|
564 As an exception, names listed in the given whitelist may have multiple IPs. |
|
565 """ |
|
566 |
|
567 by_name = {} |
|
568 by_ip = {} |
|
569 |
|
570 fail = None |
|
571 |
|
572 last_name = None |
|
573 |
|
574 for r in zone : |
|
575 name = r.name or last_name |
|
576 |
|
577 name = (r.origin, name) |
|
578 |
|
579 # name |
|
580 if r.type not in whitelist_types : |
|
581 if name not in by_name : |
|
582 by_name[name] = r |
|
583 |
|
584 elif r.name in whitelist : |
|
585 log.debug("Duplicate whitelist entry: %s", r) |
|
586 |
|
587 else : |
|
588 # fail! |
|
589 log.warn("%s: Duplicate name: %s <-> %s", r.line, r, by_name[name]) |
|
590 fail = True |
|
591 |
|
592 # ip |
|
593 if r.type == 'A' : |
|
594 ip, = r.data |
|
595 |
|
596 if ip not in by_ip : |
|
597 by_ip[ip] = r |
|
598 |
|
599 else : |
|
600 # fail! |
|
601 log.warn("%s: Duplicate IP: %s <-> %s", r.line, r, by_ip[ip]) |
|
602 fail = True |
|
603 |
|
604 return fail |
|
605 |
|
606 def process_zone_forwards (zone, txt=False, mx=False) : |
|
607 """ |
|
608 Process zone data -> forward zone data. |
|
609 """ |
|
610 |
|
611 for r in zone : |
|
612 yield r |
|
613 |
|
614 if r.type == 'A' : |
|
615 if txt : |
|
616 # comment? |
|
617 comment = r.line.comment |
|
618 |
|
619 if comment : |
|
620 yield TXTRecord(None, comment, ttl=r.ttl) |
|
621 |
|
622 |
|
623 # XXX: RP, do we need it? |
|
624 |
|
625 if mx : |
|
626 # XXX: is this a good idea? |
|
627 yield ZoneRecord(None, 'MX', [10, mx], ttl=r.ttl) |
|
628 |
|
629 def process_zone_meta (zone, ignore=None) : |
|
630 """ |
|
631 Process zone metadata -> output. |
|
632 """ |
|
633 |
|
634 TIMESTAMP_FORMAT='%Y/%m/%d' |
|
635 |
|
636 for r in zone : |
|
637 if ignore and r.name in ignore : |
|
638 # skip |
|
639 log.debug("Ignore record: %s", r) |
|
640 continue |
|
641 |
|
642 # for hosts.. |
|
643 if r.type == 'A' : |
|
644 # timestamp? |
|
645 timestamp = r.line.timestamp |
|
646 |
|
647 if timestamp : |
|
648 yield TXTRecord(r.name, timestamp.strftime(TIMESTAMP_FORMAT), ttl=r.ttl) |
|
649 |
|
650 def reverse_ipv4 (ip) : |
|
651 """ |
|
652 Return in-addr.arpa reverse for given IPv4 prefix. |
|
653 """ |
|
654 |
|
655 # parse |
|
656 octets = tuple(int(part) for part in ip.split('.')) |
|
657 |
|
658 for octet in octets : |
|
659 assert 0 <= octet <= 255 |
|
660 |
|
661 return '.'.join([str(octet) for octet in reversed(octets)] + ['in-addr', 'arpa']) |
|
662 |
|
663 def reverse_ipv6 (ip6) : |
|
664 """ |
|
665 Return ip6.arpa reverse for given IPv6 prefix. |
|
666 """ |
|
667 |
|
668 parts = [int(part, 16) for part in ip6.split(':')] |
|
669 parts = ['{0:04x}'.format(part) for part in parts] |
|
670 parts = ''.join(parts) |
|
671 |
|
672 return '.'.join(tuple(reversed(parts)) + ( 'ip6', 'arpa')) |
|
673 |
|
674 def fqdn (*parts) : |
|
675 fqdn = '.'.join(parts) |
|
676 |
|
677 # we may be given an fqdn in parts |
|
678 if not fqdn.endswith('.') : |
|
679 fqdn += '.' |
|
680 |
|
681 return fqdn |
|
682 |
|
683 def process_zone_reverse (zone, origin, domain) : |
|
684 """ |
|
685 Process zone data -> reverse zone data. |
|
686 """ |
|
687 |
|
688 name = None |
|
689 |
|
690 for r in zone : |
|
691 # keep name from previous.. |
|
692 if r.name : |
|
693 name = r.name |
|
694 |
|
695 if r.type == 'A' : |
|
696 ip, = r.data |
|
697 ptr = reverse_ipv4(ip) |
|
698 |
|
699 elif r.type == 'AAAA' : |
|
700 ip, = r.data |
|
701 ptr = reverse_ipv6(ip) |
|
702 |
|
703 else : |
|
704 continue |
|
705 |
|
706 # verify |
|
707 if zone and ptr.endswith(origin) : |
|
708 ptr = ptr[:-(len(origin) + 1)] |
|
709 |
|
710 else : |
|
711 log.warning("Reverse does not match zone origin, skipping: (%s) -> %s <-> %s", ip, ptr, origin) |
|
712 continue |
|
713 |
|
714 # domain to use |
|
715 host_domain = r.origin or domain |
|
716 host_fqdn = fqdn(name, host_domain) |
|
717 |
|
718 yield ZoneRecord(ptr, 'PTR', [host_fqdn]) |
|
719 |
|
720 def write_zone_records (file, zone) : |
|
721 for r in zone : |
|
722 file.write(r.build_line() + u'\n') |
|
723 |
|
724 def open_file (path, mode, charset) : |
|
725 """ |
|
726 Open unicode-enabled file from path, with - using stdio. |
|
727 """ |
|
728 |
|
729 if path == '-' : |
|
730 # use stdin/out based on mode |
|
731 stream, func = { |
|
732 'r': (sys.stdin, codecs.getreader), |
|
733 'w': (sys.stdout, codecs.getwriter), |
|
734 }[mode[0]] |
|
735 |
|
736 # wrap |
|
737 return func(charset)(stream) |
|
738 |
|
739 else : |
|
740 # open |
|
741 return codecs.open(path, mode, charset) |
|
742 |
|
743 def main (argv) : |
|
744 global options |
|
745 |
|
746 options, args = parse_options(argv) |
|
747 |
|
748 if options.doctest : |
|
749 import doctest |
|
750 fail, total = doctest.testmod() |
|
751 return fail |
|
752 |
|
753 if args : |
|
754 # open files |
|
755 input_files = [open_file(path, 'r', options.input_charset) for path in args] |
|
756 |
|
757 else : |
|
758 # default to stdout |
|
759 input_files = [open_file('-', 'r', options.input_charset)] |
|
760 |
|
761 # process zone data |
|
762 zone = [] |
|
763 |
|
764 for file in input_files : |
|
765 log.info("Reading zone: %s", file) |
|
766 |
|
767 zone += list(parse_zone_records(file, |
|
768 line_timestamp_prefix = options.input_line_date, |
|
769 )) |
|
770 |
|
771 # check? |
|
772 if options.check_hosts : |
|
773 whitelist = set(options.check_exempt) |
|
774 |
|
775 log.debug("checking hosts; whitelist=%r", whitelist) |
|
776 |
|
777 if check_zone_hosts(zone, whitelist=whitelist) : |
|
778 log.warn("Hosts check failed") |
|
779 return 2 |
|
780 |
|
781 else : |
|
782 log.info("Hosts check OK") |
|
783 |
|
784 # output file |
|
785 output = open_file(options.output, 'w', options.output_charset) |
|
786 |
|
787 if options.forward_zone : |
|
788 log.info("Write forward zone: %s", output) |
|
789 |
|
790 zone = list(process_zone_forwards(zone, txt=options.forward_txt, mx=options.forward_mx)) |
|
791 |
|
792 elif options.meta_zone : |
|
793 log.info("Write metadata zone: %s", output) |
|
794 |
|
795 if not options.input_line_date : |
|
796 log.error("--meta-zone requires --input-line-date") |
|
797 return 1 |
|
798 |
|
799 zone = list(process_zone_meta(zone, ignore=set(options.meta_ignore))) |
|
800 |
|
801 elif options.reverse_zone : |
|
802 if ':' in options.reverse_zone : |
|
803 # IPv6 |
|
804 origin = reverse_ipv6(options.reverse_zone) |
|
805 |
|
806 else : |
|
807 # IPv4 |
|
808 origin = reverse_ipv4(options.reverse_zone) |
|
809 |
|
810 domain = options.reverse_domain |
|
811 |
|
812 if not domain : |
|
813 log.error("--reverse-zone requires --reverse-domain") |
|
814 return 1 |
|
815 |
|
816 zone = list(process_zone_reverse(zone, origin=origin, domain=domain)) |
|
817 |
|
818 elif options.check_hosts : |
|
819 # we only did that, done |
|
820 return 0 |
|
821 |
|
822 else : |
|
823 log.warn("Nothing to do") |
|
824 return 1 |
|
825 |
|
826 write_zone_records(output, zone) |
|
827 |
|
828 return 0 |
|
829 |
|
830 if __name__ == '__main__': |
|
831 import sys |
|
832 |
|
833 sys.exit(main(sys.argv)) |
|