123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- from datetime import timedelta
- from socket import gethostbyname
- from time import sleep
- from django.conf import settings
- from django.core.mail import get_connection, mail_admins
- from django.core.management import BaseCommand
- from django.utils import timezone
- import dns.exception, dns.message, dns.query, dns.rdatatype
- from desecapi import pdns
- from desecapi.models import Domain
- def query_serial(zone, server):
- """
- Checks a zone's serial on a server.
- :return: serial if received; None if the server did not know; False on error
- """
- query = dns.message.make_query(zone, "SOA")
- try:
- response = dns.query.tcp(query, server, timeout=5)
- except dns.exception.Timeout:
- return False
- for rrset in response.answer:
- if rrset.rdtype == dns.rdatatype.SOA:
- return int(rrset[0].serial)
- return None
- class Command(BaseCommand):
- help = "Check secondaries for consistency with nsmaster."
- def __init__(self, *args, **kwargs):
- self.servers = {
- gethostbyname(server): server for server in settings.WATCHDOG_SECONDARIES
- }
- super().__init__(*args, **kwargs)
- def add_arguments(self, parser):
- parser.add_argument(
- "domain-name",
- nargs="*",
- help="Domain name to check. If omitted, will check all recently published domains.",
- )
- parser.add_argument(
- "--delay",
- type=int,
- default=120,
- help="Delay SOA checks to allow pending AXFRs to finish.",
- )
- parser.add_argument(
- "--window",
- type=int,
- default=settings.WATCHDOG_WINDOW_SEC,
- help="Check domains that were published no longer than this many seconds ago.",
- )
- def find_outdated_servers(self, zone, local_serial):
- """
- Returns a dict, the key being the outdated secondary name, and the value being the node's current zone serial.
- """
- outdated = {}
- for server in self.servers:
- remote_serial = query_serial(zone, server)
- if not remote_serial or remote_serial < local_serial:
- outdated[self.servers[server]] = remote_serial
- return outdated
- def handle(self, *args, **options):
- threshold = timezone.now() - timedelta(seconds=options["window"])
- recent_domain_names = Domain.objects.filter(
- published__gt=threshold
- ).values_list("name", flat=True)
- serials = {
- zone: s
- for zone, s in pdns.get_serials().items()
- if zone.rstrip(".") in recent_domain_names
- }
- if options["domain-name"]:
- serials = {
- zone: serial
- for zone, serial in serials.items()
- if zone.rstrip(".") in options["domain-name"]
- }
- print(
- "Sleeping for {} seconds before checking {} domains ...".format(
- options["delay"], len(serials)
- )
- )
- sleep(options["delay"])
- outdated_zone_count = 0
- outdated_secondaries = set()
- output = []
- timeouts = {}
- for zone, local_serial in serials.items():
- outdated_serials = self.find_outdated_servers(zone, local_serial)
- for server, serial in outdated_serials.items():
- if serial is False:
- timeouts.setdefault(server, [])
- timeouts[server].append(zone)
- outdated_serials = {
- k: serial
- for k, serial in outdated_serials.items()
- if serial is not False
- }
- if outdated_serials:
- outdated_secondaries.update(outdated_serials.keys())
- output.append(
- f"{zone} ({local_serial}) is outdated on {outdated_serials}"
- )
- print(output[-1])
- outdated_zone_count += 1
- else:
- print(f"{zone} ok")
- output.append(
- f"Checked {len(serials)} domains, {outdated_zone_count} were outdated."
- )
- print(output[-1])
- self.report(outdated_secondaries, output, timeouts)
- def report(self, outdated_secondaries, output, timeouts):
- if not outdated_secondaries and not timeouts:
- return
- subject = f'{timeouts and "CRITICAL ALERT" or "ALERT"} {len(outdated_secondaries)} secondaries out of sync'
- message = ""
- if timeouts:
- message += f"The following servers had timeouts:\n\n{timeouts}\n\n"
- if outdated_secondaries:
- message += f"The following {len(outdated_secondaries)} secondaries are out of sync:\n"
- for outdated_secondary in outdated_secondaries:
- message += f"* {outdated_secondary}\n"
- message += "\n"
- message += f"Current secondary IPs: {self.servers}\n"
- message += "\n".join(output)
- mail_admins(
- subject,
- message,
- connection=get_connection("django.core.mail.backends.smtp.EmailBackend"),
- )
|