check-slaves.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. from datetime import timedelta
  2. from socket import gethostbyname
  3. from time import sleep
  4. from django.conf import settings
  5. from django.core.mail import get_connection, mail_admins
  6. from django.core.management import BaseCommand
  7. from django.utils import timezone
  8. import dns.message, dns.query, dns.rdatatype
  9. from desecapi import pdns
  10. from desecapi.models import Domain
  11. def query_serial(zone, server):
  12. query = dns.message.make_query(zone, 'SOA')
  13. response = dns.query.tcp(query, server)
  14. for rrset in response.answer:
  15. if rrset.rdtype == dns.rdatatype.SOA:
  16. return int(rrset[0].serial)
  17. return None
  18. class Command(BaseCommand):
  19. help = 'Check slaves for consistency with nsmaster.'
  20. def __init__(self, *args, **kwargs):
  21. self.servers = {gethostbyname(server): server for server in settings.WATCHDOG_SLAVES}
  22. super().__init__(*args, **kwargs)
  23. def add_arguments(self, parser):
  24. parser.add_argument('domain-name', nargs='*',
  25. help='Domain name to check. If omitted, will check all recently published domains.')
  26. parser.add_argument('--delay', type=int, default=120, help='Delay SOA checks to allow pending AXFRs to finish.')
  27. parser.add_argument('--window', type=int, default=settings.WATCHDOG_WINDOW_SEC,
  28. help='Check domains that were published no longer than this many seconds ago.')
  29. def find_outdated_servers(self, zone, local_serial):
  30. """
  31. Returns a dict, the key being the outdated slave name, and the value being the slave's current zone serial.
  32. """
  33. outdated = {}
  34. for server in self.servers:
  35. remote_serial = query_serial(zone, server)
  36. if not remote_serial or remote_serial < local_serial:
  37. outdated[self.servers[server]] = remote_serial
  38. return outdated
  39. def handle(self, *args, **options):
  40. threshold = timezone.now() - timedelta(seconds=options['window'])
  41. recent_domain_names = Domain.objects.filter(published__gt=threshold).values_list('name', flat=True)
  42. serials = {zone: s for zone, s in pdns.get_serials().items() if zone.rstrip('.') in recent_domain_names}
  43. if options['domain-name']:
  44. serials = {zone: serial for zone, serial in serials.items() if zone.rstrip('.') in options['domain-name']}
  45. print('Sleeping for {} seconds before checking {} domains ...'.format(options['delay'], len(serials)))
  46. sleep(options['delay'])
  47. outdated_zone_count = 0
  48. outdated_slaves = set()
  49. output = []
  50. for zone, local_serial in serials.items():
  51. outdated_serials = self.find_outdated_servers(zone, local_serial)
  52. outdated_slaves.update(outdated_serials.keys())
  53. if outdated_serials:
  54. output.append(f'{zone} ({local_serial}) is outdated on {outdated_serials}')
  55. print(output[-1])
  56. outdated_zone_count += 1
  57. else:
  58. print(f'{zone} ok')
  59. output.append(f'Checked {len(serials)} domains, {outdated_zone_count} were outdated.')
  60. print(output[-1])
  61. self.report(outdated_slaves, output)
  62. def report(self, outdated_slaves, output):
  63. if not outdated_slaves:
  64. return
  65. subject = f'ALERT {len(outdated_slaves)} slaves out of sync'
  66. message = f'The following {len(outdated_slaves)} slaves are out of sync:\n'
  67. for outdated_slave in outdated_slaves:
  68. message += f'* {outdated_slave}\n'
  69. message += '\n'
  70. message += f'Current slave IPs: {self.servers}'
  71. message += '\n'
  72. message += '\n'.join(output)
  73. mail_admins(subject, message, connection=get_connection('django.core.mail.backends.smtp.EmailBackend'))