check_dns_sync 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/python3
  2. #
  3. # Maximilian Wilhelm <max@rfc2324.org>
  4. # -- Mon 30 Mar 2020 11:55:47 PM CEST
  5. #
  6. import argparse
  7. from dns.flags import to_text
  8. from dns.resolver import Resolver
  9. from ipaddress import ip_address
  10. import sys
  11. import time
  12. # Exit code definitions
  13. OK = 0
  14. WARNING = 1
  15. CRITICAL = 2
  16. UNKNOWN = 3
  17. # Track start time
  18. time_start = time.time ()
  19. parser = argparse.ArgumentParser (description = 'Check DNS sync')
  20. parser.add_argument ('--reference-ns', required = True, help = 'IP address of reference NS')
  21. parser.add_argument ('--replica-ns', required = True, help = 'IP address of NS to be checked')
  22. parser.add_argument ('--check-mode', choices = [ 'serial', 'axfr' ], default = 'serial', help = 'Compare only serial or full zone content?')
  23. parser.add_argument ('--timeout', type = int, default = 10, help = 'Timeout for DNS operations')
  24. parser.add_argument ('--verbose', '-v', action = 'store_true', help = 'Be verbose in the output')
  25. parser.add_argument ('zones', nargs = '+', help = 'Zones to compare')
  26. args = parser.parse_args ()
  27. if args.check_mode == 'axfr':
  28. print ("AXFR check mode not implemented yet. Send patches :)")
  29. sys.exit (UNKNOWN)
  30. #
  31. # Helpers
  32. #
  33. def is_ip (ns):
  34. try:
  35. ip = ip_address (ns)
  36. except ValueError:
  37. return False
  38. return True
  39. def check_zone (zone):
  40. res = {
  41. 'state' : UNKNOWN,
  42. 'diff' : '',
  43. 'errors' : '',
  44. }
  45. if args.check_mode == 'serial':
  46. try:
  47. # Query reference NS
  48. reference = reference_res.query (zone, 'SOA')
  49. # Check is answer is authoritive
  50. if not 'AA' in to_text (reference.response.flags):
  51. res['state'] = CRITICAL
  52. res['errors'] = "Got non-authoritive answer from reference NS: %s" % args.reference_ns
  53. return res
  54. except Exception as e:
  55. res['errors'] = "Error while checking reference NS %s: %s" % (args.reference_ns, e)
  56. return res
  57. try:
  58. # Query replica NS
  59. replica = replica_res.query (zone, 'SOA')
  60. # Check is answer is authoritive
  61. if not 'AA' in to_text (replica.response.flags):
  62. res['state'] = CRITICAL
  63. res['errors'] = "Got non-authoritive answer from replica NS: %s" % args.replica_ns
  64. return res
  65. except Exception as e:
  66. res['errors'] = "Error while checking replica NS %s: %s" % (args.replica_ns, e)
  67. return res
  68. try:
  69. reference_serial = str (reference.response.answer[0]).split ()[6]
  70. replica_serial = str (replica.response.answer[0]).split ()[6]
  71. except AttributeError as a:
  72. res['errors'] = a
  73. return res
  74. except IndexError as i:
  75. res['errors'] = i
  76. return res
  77. if reference_serial == replica_serial:
  78. res['state'] = OK
  79. else:
  80. res['state'] = CRITICAL
  81. res['errors'] = "Serial mismatch: %s vs. %s" % (reference_serial, replica_serial)
  82. return res
  83. #
  84. # Setup
  85. #
  86. # Check for possible badness
  87. if not is_ip (args.reference_ns):
  88. print ("Error: Reference NS has to an IP address.")
  89. sys.exit (CRITICAL)
  90. if not is_ip (args.replica_ns):
  91. print ("Error: Replica NS has to an IP address.")
  92. sys.exit (CRITICAL)
  93. if args.reference_ns == args.replica_ns:
  94. print ("Error: Reference NS and replica NS must not be the same!")
  95. sys.exit (CRITICAL)
  96. # Resolver for reference NS
  97. reference_res = Resolver (configure = False)
  98. reference_res.nameservers = [args.reference_ns]
  99. reference_res.lifetime = args.timeout
  100. # Resolver for NS to be checked
  101. replica_res = Resolver (configure = False)
  102. replica_res.nameservers = [args.replica_ns]
  103. replica_res.lifetime = args.timeout
  104. #
  105. # Let#s go
  106. #
  107. codes = {}
  108. ret_code = OK
  109. errors = ""
  110. in_sync = []
  111. for zone in args.zones:
  112. check = check_zone (zone)
  113. # Keep track of states
  114. state = check['state']
  115. codes[state] = codes.get (state, 0) + 1
  116. if state == OK:
  117. in_sync.append (zone)
  118. continue
  119. errors += "Zone '%s': %s\n" % (zone, check['errors'])
  120. if state > ret_code:
  121. ret_code = check['state']
  122. if errors:
  123. print (errors)
  124. if in_sync:
  125. if args.verbose:
  126. print ("Zones in sync: %s" % ", ".join (sorted (in_sync)))
  127. time_delta = int (1000 * (time.time () - time_start))
  128. print ("Checked %d zones in %d ms. %d OK, %d WARN, %d CRIT, %d UNKN" % (
  129. len (args.zones),
  130. time_delta,
  131. codes.get (OK, 0),
  132. codes.get (WARNING, 0),
  133. codes.get (CRITICAL ,0),
  134. codes.get (UNKNOWN, 0),
  135. ))
  136. sys.exit (ret_code)