check_dns_sync 3.6 KB

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