etc/services - sync with NetBSD-8
[minix.git] / external / bsd / bind / dist / bin / python / dnssec-coverage.py.in
blob0f352c15a34ea7b79581aaaed43ca61f8a9e95c5
1 #!@PYTHON@
2 ############################################################################
3 # Copyright (C) 2013, 2014 Internet Systems Consortium, Inc. ("ISC")
5 # Permission to use, copy, modify, and/or distribute this software for any
6 # purpose with or without fee is hereby granted, provided that the above
7 # copyright notice and this permission notice appear in all copies.
9 # THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
10 # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 # AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
12 # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14 # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 # PERFORMANCE OF THIS SOFTWARE.
16 ############################################################################
18 import argparse
19 import os
20 import glob
21 import sys
22 import re
23 import time
24 import calendar
25 from collections import defaultdict
26 import pprint
28 prog='dnssec-coverage'
30 # These routines permit platform-independent location of BIND 9 tools
31 if os.name == 'nt':
32 import win32con
33 import win32api
35 def prefix(bindir = ''):
36 if os.name != 'nt':
37 return os.path.join('@prefix@', bindir)
39 bind_subkey = "Software\\ISC\\BIND"
40 hKey = None
41 keyFound = True
42 try:
43 hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
44 except:
45 keyFound = False
46 if keyFound:
47 try:
48 (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir")
49 except:
50 keyFound = False
51 win32api.RegCloseKey(hKey)
52 if keyFound:
53 return os.path.join(namedBase, bindir)
54 return os.path.join(win32api.GetSystemDirectory(), bindir)
56 ########################################################################
57 # Class Event
58 ########################################################################
59 class Event:
60 """ A discrete key metadata event, e.g., Publish, Activate, Inactive,
61 Delete. Stores the date of the event, and identifying information about
62 the key to which the event will occur."""
64 def __init__(self, _what, _key):
65 now = time.time()
66 self.what = _what
67 self.when = _key.metadata[_what]
68 self.key = _key
69 self.keyid = _key.keyid
70 self.sep = _key.sep
71 self.zone = _key.zone
72 self.alg = _key.alg
74 def __repr__(self):
75 return repr((self.when, self.what, self.keyid, self.sep,
76 self.zone, self.alg))
78 def showtime(self):
79 return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when)
81 def showkey(self):
82 return self.key.showkey()
84 def showkeytype(self):
85 return self.key.showkeytype()
87 ########################################################################
88 # Class Key
89 ########################################################################
90 class Key:
91 """An individual DNSSEC key. Identified by path, zone, algorithm, keyid.
92 Contains a dictionary of metadata events."""
94 def __init__(self, keyname):
95 directory = os.path.dirname(keyname)
96 key = os.path.basename(keyname)
97 (zone, alg, keyid) = key.split('+')
98 keyid = keyid.split('.')[0]
99 key = [zone, alg, keyid]
100 key_file = directory + os.sep + '+'.join(key) + ".key"
101 private_file = directory + os.sep + '+'.join(key) + ".private"
103 self.zone = zone[1:-1]
104 self.alg = int(alg)
105 self.keyid = int(keyid)
107 kfp = open(key_file, "r")
108 for line in kfp:
109 if line[0] == ';':
110 continue
111 tokens = line.split()
112 if not tokens:
113 continue
115 if tokens[1].lower() in ('in', 'ch', 'hs'):
116 septoken = 3
117 self.ttl = args.keyttl
118 if not self.ttl:
119 vspace()
120 print("WARNING: Unable to determine TTL for DNSKEY %s." %
121 self.showkey())
122 print("\t Using 1 day (86400 seconds); re-run with the -d "
123 "option for more\n\t accurate results.")
124 self.ttl = 86400
125 else:
126 septoken = 4
127 self.ttl = int(tokens[1]) if not args.keyttl else args.keyttl
129 if (int(tokens[septoken]) & 0x1) == 1:
130 self.sep = True
131 else:
132 self.sep = False
133 kfp.close()
135 pfp = open(private_file, "rU")
136 propDict = dict()
137 for propLine in pfp:
138 propDef = propLine.strip()
139 if len(propDef) == 0:
140 continue
141 if propDef[0] in ('!', '#'):
142 continue
143 punctuation = [propDef.find(c) for c in ':= '] + [len(propDef)]
144 found = min([ pos for pos in punctuation if pos != -1 ])
145 name = propDef[:found].rstrip()
146 value = propDef[found:].lstrip(":= ").rstrip()
147 propDict[name] = value
149 if("Publish" in propDict):
150 propDict["Publish"] = time.strptime(propDict["Publish"],
151 "%Y%m%d%H%M%S")
153 if("Activate" in propDict):
154 propDict["Activate"] = time.strptime(propDict["Activate"],
155 "%Y%m%d%H%M%S")
157 if("Inactive" in propDict):
158 propDict["Inactive"] = time.strptime(propDict["Inactive"],
159 "%Y%m%d%H%M%S")
161 if("Delete" in propDict):
162 propDict["Delete"] = time.strptime(propDict["Delete"],
163 "%Y%m%d%H%M%S")
165 if("Revoke" in propDict):
166 propDict["Revoke"] = time.strptime(propDict["Revoke"],
167 "%Y%m%d%H%M%S")
168 pfp.close()
169 self.metadata = propDict
171 def showkey(self):
172 return "%s/%03d/%05d" % (self.zone, self.alg, self.keyid);
174 def showkeytype(self):
175 return ("KSK" if self.sep else "ZSK")
177 # ensure that the gap between Publish and Activate is big enough
178 def check_prepub(self):
179 now = time.time()
181 if (not "Activate" in self.metadata):
182 debug_print("No Activate information in key: %s" % self.showkey())
183 return False
184 a = calendar.timegm(self.metadata["Activate"])
186 if (not "Publish" in self.metadata):
187 debug_print("No Publish information in key: %s" % self.showkey())
188 if a > now:
189 vspace()
190 print("WARNING: Key %s (%s) is scheduled for activation but \n"
191 "\t not for publication." %
192 (self.showkey(), self.showkeytype()))
193 return False
194 p = calendar.timegm(self.metadata["Publish"])
196 now = time.time()
197 if p < now and a < now:
198 return True
200 if p == a:
201 vspace()
202 print ("WARNING: %s (%s) is scheduled to be published and\n"
203 "\t activated at the same time. This could result in a\n"
204 "\t coverage gap if the zone was previously signed." %
205 (self.showkey(), self.showkeytype()))
206 print("\t Activation should be at least %s after publication."
207 % duration(self.ttl))
208 return True
210 if a < p:
211 vspace()
212 print("WARNING: Key %s (%s) is active before it is published" %
213 (self.showkey(), self.showkeytype()))
214 return False
216 if (a - p < self.ttl):
217 vspace()
218 print("WARNING: Key %s (%s) is activated too soon after\n"
219 "\t publication; this could result in coverage gaps due to\n"
220 "\t resolver caches containing old data."
221 % (self.showkey(), self.showkeytype()))
222 print("\t Activation should be at least %s after publication." %
223 duration(self.ttl))
224 return False
226 return True
228 # ensure that the gap between Inactive and Delete is big enough
229 def check_postpub(self, timespan = None):
230 if not timespan:
231 timespan = self.ttl
233 now = time.time()
235 if (not "Delete" in self.metadata):
236 debug_print("No Delete information in key: %s" % self.showkey())
237 return False
238 d = calendar.timegm(self.metadata["Delete"])
240 if (not "Inactive" in self.metadata):
241 debug_print("No Inactive information in key: %s" % self.showkey())
242 if d > now:
243 vspace()
244 print("WARNING: Key %s (%s) is scheduled for deletion but\n"
245 "\t not for inactivation." %
246 (self.showkey(), self.showkeytype()))
247 return False
248 i = calendar.timegm(self.metadata["Inactive"])
250 if d < now and i < now:
251 return True
253 if (d < i):
254 vspace()
255 print("WARNING: Key %s (%s) is scheduled for deletion before\n"
256 "\t inactivation." % (self.showkey(), self.showkeytype()))
257 return False
259 if (d - i < timespan):
260 vspace()
261 print("WARNING: Key %s (%s) scheduled for deletion too soon after\n"
262 "\t deactivation; this may result in coverage gaps due to\n"
263 "\t resolver caches containing old data."
264 % (self.showkey(), self.showkeytype()))
265 print("\t Deletion should be at least %s after inactivation." %
266 duration(timespan))
267 return False
269 return True
271 ########################################################################
272 # class Zone
273 ########################################################################
274 class Zone:
275 """Stores data about a specific zone"""
277 def __init__(self, _name, _keyttl = None, _maxttl = None):
278 self.name = _name
279 self.keyttl = _keyttl
280 self.maxttl = _maxttl
282 def load(self, filename):
283 if not args.compilezone:
284 sys.stderr.write(prog + ': FATAL: "named-compilezone" not found\n')
285 exit(1)
287 if not self.name:
288 return
290 maxttl = keyttl = None
292 fp = os.popen("%s -o - %s %s 2> /dev/null" %
293 (args.compilezone, self.name, filename))
294 for line in fp:
295 fields = line.split()
296 if not maxttl or int(fields[1]) > maxttl:
297 maxttl = int(fields[1])
298 if fields[3] == "DNSKEY":
299 keyttl = int(fields[1])
300 fp.close()
302 self.keyttl = keyttl
303 self.maxttl = maxttl
305 ############################################################################
306 # debug_print:
307 ############################################################################
308 def debug_print(debugVar):
309 """pretty print a variable iff debug mode is enabled"""
310 if not args.debug_mode:
311 return
312 if type(debugVar) == str:
313 print("DEBUG: " + debugVar)
314 else:
315 print("DEBUG: " + pprint.pformat(debugVar))
316 return
318 ############################################################################
319 # vspace:
320 ############################################################################
321 _firstline = True
322 def vspace():
323 """adds vertical space between two sections of output text if and only
324 if this is *not* the first section being printed"""
325 global _firstline
326 if _firstline:
327 _firstline = False
328 else:
329 print
331 ############################################################################
332 # vreset:
333 ############################################################################
334 def vreset():
335 """reset vertical spacing"""
336 global _firstline
337 _firstline = True
339 ############################################################################
340 # getunit
341 ############################################################################
342 def getunit(secs, size):
343 """given a number of seconds, and a number of seconds in a larger unit of
344 time, calculate how many of the larger unit there are and return both
345 that and a remainder value"""
346 bigunit = secs // size
347 if bigunit:
348 secs %= size
349 return (bigunit, secs)
351 ############################################################################
352 # addtime
353 ############################################################################
354 def addtime(output, unit, t):
355 """add a formatted unit of time to an accumulating string"""
356 if t:
357 output += ("%s%d %s%s" %
358 ((", " if output else ""),
359 t, unit, ("s" if t > 1 else "")))
361 return output
363 ############################################################################
364 # duration:
365 ############################################################################
366 def duration(secs):
367 """given a length of time in seconds, print a formatted human duration
368 in larger units of time
370 # define units:
371 minute = 60
372 hour = minute * 60
373 day = hour * 24
374 month = day * 30
375 year = day * 365
377 # calculate time in units:
378 (years, secs) = getunit(secs, year)
379 (months, secs) = getunit(secs, month)
380 (days, secs) = getunit(secs, day)
381 (hours, secs) = getunit(secs, hour)
382 (minutes, secs) = getunit(secs, minute)
384 output = ''
385 output = addtime(output, "year", years)
386 output = addtime(output, "month", months)
387 output = addtime(output, "day", days)
388 output = addtime(output, "hour", hours)
389 output = addtime(output, "minute", minutes)
390 output = addtime(output, "second", secs)
391 return output
393 ############################################################################
394 # parse_time
395 ############################################################################
396 def parse_time(s):
397 """convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds"""
398 s = s.strip()
400 # if s is an integer, we're done already
401 try:
402 n = int(s)
403 return n
404 except:
405 pass
407 # try to parse as a number with a suffix indicating unit of time
408 r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)')
409 m = r.match(s)
410 if not m:
411 raise Exception("Cannot parse %s" % s)
412 (n, unit) = m.groups()
413 n = int(n)
414 unit = unit.lower()
415 if unit[0] == 'y':
416 return n * 31536000
417 elif unit[0] == 'm' and unit[1] == 'o':
418 return n * 2592000
419 elif unit[0] == 'w':
420 return n * 604800
421 elif unit[0] == 'd':
422 return n * 86400
423 elif unit[0] == 'h':
424 return n * 3600
425 elif unit[0] == 'm' and unit[1] == 'i':
426 return n * 60
427 elif unit[0] == 's':
428 return n
429 else:
430 raise Exception("Invalid suffix %s" % unit)
432 ############################################################################
433 # algname:
434 ############################################################################
435 def algname(alg):
436 """return the mnemonic for a DNSSEC algorithm"""
437 names = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
438 'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
439 'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
440 'ECDSAP384SHA384')
441 name = None
442 if alg in range(len(names)):
443 name = names[alg]
444 return (name if name else str(alg))
446 ############################################################################
447 # list_events:
448 ############################################################################
449 def list_events(eventgroup):
450 """print a list of the events in an eventgroup"""
451 if not eventgroup:
452 return
453 print (" " + eventgroup[0].showtime() + ":")
454 for event in eventgroup:
455 print (" %s: %s (%s)" %
456 (event.what, event.showkey(), event.showkeytype()))
458 ############################################################################
459 # process_events:
460 ############################################################################
461 def process_events(eventgroup, active, published):
462 """go through the events in an event group in time-order, add to active
463 list upon Activate event, add to published list upon Publish event,
464 remove from active list upon Inactive event, and remove from published
465 upon Delete event. Emit warnings when inconsistant states are reached"""
466 for event in eventgroup:
467 if event.what == "Activate":
468 active.add(event.keyid)
469 elif event.what == "Publish":
470 published.add(event.keyid)
471 elif event.what == "Inactive":
472 if event.keyid not in active:
473 vspace()
474 print ("\tWARNING: %s (%s) scheduled to become inactive "
475 "before it is active" %
476 (event.showkey(), event.showkeytype()))
477 else:
478 active.remove(event.keyid)
479 elif event.what == "Delete":
480 if event.keyid in published:
481 published.remove(event.keyid)
482 else:
483 vspace()
484 print ("WARNING: key %s (%s) is scheduled for deletion before "
485 "it is published, at %s" %
486 (event.showkey(), event.showkeytype()))
487 elif event.what == "Revoke":
488 # We don't need to worry about the logic of this one;
489 # just stop counting this key as either active or published
490 if event.keyid in published:
491 published.remove(event.keyid)
492 if event.keyid in active:
493 active.remove(event.keyid)
495 return (active, published)
497 ############################################################################
498 # check_events:
499 ############################################################################
500 def check_events(eventsList, ksk):
501 """create lists of events happening at the same time, check for
502 inconsistancies"""
503 active = set()
504 published = set()
505 eventgroups = list()
506 eventgroup = list()
507 keytype = ("KSK" if ksk else "ZSK")
509 # collect up all events that have the same time
510 eventsfound = False
511 for event in eventsList:
512 # if checking ZSKs, skip KSKs, and vice versa
513 if (ksk and not event.sep) or (event.sep and not ksk):
514 continue
516 # we found an appropriate (ZSK or KSK event)
517 eventsfound = True
519 # add event to current eventgroup
520 if (not eventgroup or eventgroup[0].when == event.when):
521 eventgroup.append(event)
523 # if we're at the end of the list, we're done. if
524 # we've found an event with a later time, start a new
525 # eventgroup
526 if (eventgroup[0].when != event.when):
527 eventgroups.append(eventgroup)
528 eventgroup = list()
529 eventgroup.append(event)
531 if eventgroup:
532 eventgroups.append(eventgroup)
534 for eventgroup in eventgroups:
535 if (args.checklimit and
536 calendar.timegm(eventgroup[0].when) > args.checklimit):
537 print("Ignoring events after %s" %
538 time.strftime("%a %b %d %H:%M:%S UTC %Y",
539 time.gmtime(args.checklimit)))
540 return True
542 (active, published) = \
543 process_events(eventgroup, active, published)
545 list_events(eventgroup)
547 # and then check for inconsistencies:
548 if len(active) == 0:
549 print ("ERROR: No %s's are active after this event" % keytype)
550 return False
551 elif len(published) == 0:
552 sys.stdout.write("ERROR: ")
553 print ("ERROR: No %s's are published after this event" % keytype)
554 return False
555 elif len(published.intersection(active)) == 0:
556 sys.stdout.write("ERROR: ")
557 print (("ERROR: No %s's are both active and published " +
558 "after this event") % keytype)
559 return False
561 if not eventsfound:
562 print ("ERROR: No %s events found in '%s'" %
563 (keytype, args.path))
564 return False
566 return True
568 ############################################################################
569 # check_zones:
570 # ############################################################################
571 def check_zones(eventsList):
572 """scan events per zone, algorithm, and key type, in order of occurrance,
573 noting inconsistent states when found"""
574 global foundprob
576 foundprob = False
577 zonesfound = False
578 for zone in eventsList:
579 if args.zone and zone != args.zone:
580 continue
582 zonesfound = True
583 for alg in eventsList[zone]:
584 if not args.no_ksk:
585 vspace()
586 print("Checking scheduled KSK events for zone %s, algorithm %s..." %
587 (zone, algname(alg)))
588 if not check_events(eventsList[zone][alg], True):
589 foundprob = True
590 else:
591 print ("No errors found")
593 if not args.no_zsk:
594 vspace()
595 print("Checking scheduled ZSK events for zone %s, algorithm %s..." %
596 (zone, algname(alg)))
597 if not check_events(eventsList[zone][alg], False):
598 foundprob = True
599 else:
600 print ("No errors found")
602 if not zonesfound:
603 print("ERROR: No key events found for %s in '%s'" %
604 (args.zone, args.path))
605 exit(1)
607 ############################################################################
608 # fill_eventsList:
609 ############################################################################
610 def fill_eventsList(eventsList):
611 """populate the list of events"""
612 for zone, algorithms in keyDict.items():
613 for alg, keys in algorithms.items():
614 for keyid, keydata in keys.items():
615 if("Publish" in keydata.metadata):
616 eventsList[zone][alg].append(Event("Publish", keydata))
617 if("Activate" in keydata.metadata):
618 eventsList[zone][alg].append(Event("Activate", keydata))
619 if("Inactive" in keydata.metadata):
620 eventsList[zone][alg].append(Event("Inactive", keydata))
621 if("Delete" in keydata.metadata):
622 eventsList[zone][alg].append(Event("Delete", keydata))
624 eventsList[zone][alg] = sorted(eventsList[zone][alg],
625 key=lambda event: event.when)
627 foundprob = False
628 if not keyDict:
629 print("ERROR: No key events found in '%s'" % args.path)
630 exit(1)
632 ############################################################################
633 # set_path:
634 ############################################################################
635 def set_path(command, default=None):
636 """find the location of a specified command. if a default is supplied
637 and it works, we use it; otherwise we search PATH for a match. If
638 not found, error and exit"""
639 fpath = default
640 if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
641 path = os.environ["PATH"]
642 if not path:
643 path = os.path.defpath
644 for directory in path.split(os.pathsep):
645 fpath = directory + os.sep + command
646 if os.path.isfile(fpath) or os.access(fpath, os.X_OK):
647 break
648 fpath = None
650 return fpath
652 ############################################################################
653 # parse_args:
654 ############################################################################
655 def parse_args():
656 """Read command line arguments, set global 'args' structure"""
657 global args
658 compilezone = set_path('named-compilezone',
659 os.path.join(prefix('bin'), 'named-compilezone'))
661 parser = argparse.ArgumentParser(description=prog + ': checks future ' +
662 'DNSKEY coverage for a zone')
664 parser.add_argument('zone', type=str, help='zone to check')
665 parser.add_argument('-K', dest='path', default='.', type=str,
666 help='a directory containing keys to process',
667 metavar='dir')
668 parser.add_argument('-f', dest='filename', type=str,
669 help='zone master file', metavar='file')
670 parser.add_argument('-m', dest='maxttl', type=str,
671 help='the longest TTL in the zone(s)',
672 metavar='time')
673 parser.add_argument('-d', dest='keyttl', type=str,
674 help='the DNSKEY TTL', metavar='time')
675 parser.add_argument('-r', dest='resign', default='1944000',
676 type=int, help='the RRSIG refresh interval '
677 'in seconds [default: 22.5 days]',
678 metavar='time')
679 parser.add_argument('-c', dest='compilezone',
680 default=compilezone, type=str,
681 help='path to \'named-compilezone\'',
682 metavar='path')
683 parser.add_argument('-l', dest='checklimit',
684 type=str, default='0',
685 help='Length of time to check for '
686 'DNSSEC coverage [default: 0 (unlimited)]',
687 metavar='time')
688 parser.add_argument('-z', dest='no_ksk',
689 action='store_true', default=False,
690 help='Only check zone-signing keys (ZSKs)')
691 parser.add_argument('-k', dest='no_zsk',
692 action='store_true', default=False,
693 help='Only check key-signing keys (KSKs)')
694 parser.add_argument('-D', '--debug', dest='debug_mode',
695 action='store_true', default=False,
696 help='Turn on debugging output')
697 parser.add_argument('-v', '--version', action='version', version='9.9.1')
699 args = parser.parse_args()
701 if args.no_zsk and args.no_ksk:
702 print("ERROR: -z and -k cannot be used together.");
703 exit(1)
705 # convert from time arguments to seconds
706 try:
707 if args.maxttl:
708 m = parse_time(args.maxttl)
709 args.maxttl = m
710 except:
711 pass
713 try:
714 if args.keyttl:
715 k = parse_time(args.keyttl)
716 args.keyttl = k
717 except:
718 pass
720 try:
721 if args.resign:
722 r = parse_time(args.resign)
723 args.resign = r
724 except:
725 pass
727 try:
728 if args.checklimit:
729 lim = args.checklimit
730 r = parse_time(args.checklimit)
731 if r == 0:
732 args.checklimit = None
733 else:
734 args.checklimit = time.time() + r
735 except:
736 pass
738 # if we've got the values we need from the command line, stop now
739 if args.maxttl and args.keyttl:
740 return
742 # load keyttl and maxttl data from zonefile
743 if args.zone and args.filename:
744 try:
745 zone = Zone(args.zone)
746 zone.load(args.filename)
747 if not args.maxttl:
748 args.maxttl = zone.maxttl
749 if not args.keyttl:
750 args.keyttl = zone.maxttl
751 except Exception as e:
752 print("Unable to load zone data from %s: " % args.filename, e)
754 if not args.maxttl:
755 vspace()
756 print ("WARNING: Maximum TTL value was not specified. Using 1 week\n"
757 "\t (604800 seconds); re-run with the -m option to get more\n"
758 "\t accurate results.")
759 args.maxttl = 604800
761 ############################################################################
762 # Main
763 ############################################################################
764 def main():
765 global keyDict
767 parse_args()
768 path=args.path
770 print ("PHASE 1--Loading keys to check for internal timing problems")
771 keyDict = defaultdict(lambda : defaultdict(dict))
772 files = glob.glob(os.path.join(path, '*.private'))
773 for infile in files:
774 key = Key(infile)
775 if args.zone and key.zone != args.zone:
776 continue
777 keyDict[key.zone][key.alg][key.keyid] = key
778 key.check_prepub()
779 if key.sep:
780 key.check_postpub()
781 else:
782 key.check_postpub(args.maxttl + args.resign)
784 vspace()
785 print ("PHASE 2--Scanning future key events for coverage failures")
786 vreset()
788 eventsList = defaultdict(lambda : defaultdict(list))
789 fill_eventsList(eventsList)
790 check_zones(eventsList)
792 if foundprob:
793 exit(1)
794 else:
795 exit(0)
797 if __name__ == "__main__":
798 main()