scavenge-unused.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import datetime
  2. from django.core.mail import get_connection, mail_admins
  3. from django.core.management import BaseCommand
  4. from django.db import transaction
  5. from django.db.models import F, Max, OuterRef, Subquery
  6. from django.db.models.functions import Greatest
  7. from django.utils import timezone
  8. from desecapi import models, serializers, views
  9. from desecapi.pdns_change_tracker import PDNSChangeTracker
  10. fresh_days = 183
  11. notice_days_notify = 28
  12. notice_days_warn = 7
  13. class Command(BaseCommand):
  14. base_queryset = models.Domain.objects.exclude(
  15. renewal_state=models.Domain.RenewalState.IMMORTAL
  16. ).filter(owner__is_active=True)
  17. _rrsets_outer_queryset = models.RRset.objects.filter(domain=OuterRef("pk")).values(
  18. "domain"
  19. ) # values() is GROUP BY
  20. _max_touched = Subquery(
  21. _rrsets_outer_queryset.annotate(max_touched=Max("touched")).values(
  22. "max_touched"
  23. )
  24. )
  25. @classmethod
  26. def renew_touched_domains(cls):
  27. recently_active_domains = cls.base_queryset.annotate(
  28. last_active=Greatest(cls._max_touched, "published")
  29. ).filter(
  30. last_active__date__gte=timezone.localdate() - datetime.timedelta(days=183),
  31. renewal_changed__lt=F("last_active"),
  32. )
  33. print(
  34. "Renewing domains:", *recently_active_domains.values_list("name", flat=True)
  35. )
  36. recently_active_domains.update(
  37. renewal_state=models.Domain.RenewalState.FRESH,
  38. renewal_changed=F("last_active"),
  39. )
  40. @classmethod
  41. def warn_domain_deletion(cls, renewal_state, notice_days, inactive_days):
  42. # We act when `renewal_changed` is at least this date (or older)
  43. inactive_threshold = timezone.localdate() - datetime.timedelta(
  44. days=inactive_days
  45. )
  46. # Filter candidates which have the state of interest, at least since the calculated date
  47. expiry_candidates = cls.base_queryset.filter(
  48. renewal_state=renewal_state, renewal_changed__date__lte=inactive_threshold
  49. )
  50. # Group domains by user, so that we can send one message per user
  51. domain_user_map = {}
  52. for domain in expiry_candidates.order_by("name"):
  53. if domain.owner not in domain_user_map:
  54. domain_user_map[domain.owner] = []
  55. domain_user_map[domain.owner].append(domain)
  56. # Prepare and send emails, and keep renewal status in sync
  57. context = {
  58. "deletion_date": timezone.localdate() + datetime.timedelta(days=notice_days)
  59. }
  60. for user, domains in domain_user_map.items():
  61. with transaction.atomic():
  62. # Update renewal status of the user's affected domains, but don't commit before sending the email
  63. actions = []
  64. for domain in domains:
  65. domain.renewal_state += 1
  66. domain.renewal_changed = timezone.now()
  67. domain.save(update_fields=["renewal_state", "renewal_changed"])
  68. actions.append(
  69. models.AuthenticatedRenewDomainBasicUserAction(
  70. user=user, domain=domain
  71. )
  72. )
  73. serializers.AuthenticatedRenewDomainBasicUserActionSerializer(
  74. actions, many=True, context=context
  75. ).save()
  76. @classmethod
  77. def delete_domains(cls, inactive_days):
  78. expired_domains = (
  79. cls.base_queryset.filter(renewal_state=models.Domain.RenewalState.WARNED)
  80. .annotate(last_active=Greatest(cls._max_touched, "published"))
  81. .filter(
  82. renewal_changed__date__lte=timezone.localdate()
  83. - datetime.timedelta(days=notice_days_warn),
  84. last_active__date__lte=timezone.localdate()
  85. - datetime.timedelta(days=inactive_days),
  86. )
  87. )
  88. for domain in expired_domains:
  89. with PDNSChangeTracker():
  90. domain.delete()
  91. if not domain.owner.domains.exists():
  92. domain.owner.delete()
  93. # Do one large delegation update
  94. with PDNSChangeTracker():
  95. for domain in expired_domains:
  96. views.DomainViewSet.auto_delegate(domain)
  97. def handle(self, *args, **kwargs):
  98. try:
  99. # Reset renewal status for domains that have recently been touched
  100. self.renew_touched_domains()
  101. # Announce domain deletion in `notice_days_notice` days if not yet notified (FRESH) and inactive for
  102. # `inactive_days` days. Updates status from FRESH to NOTIFIED.
  103. self.warn_domain_deletion(
  104. models.Domain.RenewalState.FRESH, notice_days_notify, fresh_days
  105. )
  106. # After `notice_days_notify - notice_days_warn` more days, warn again if the status has not changed
  107. # Updates status from NOTIFIED to WARNED.
  108. self.warn_domain_deletion(
  109. models.Domain.RenewalState.NOTIFIED,
  110. notice_days_warn,
  111. notice_days_notify - notice_days_warn,
  112. )
  113. # Finally, delete domains inactive for `inactive_days + notice_days_notify` days if status has not changed
  114. self.delete_domains(fresh_days + notice_days_notify)
  115. except Exception as e:
  116. subject = "Renewal Exception!"
  117. message = f"{type(e)}\n\n{str(e)}"
  118. print(f"Chores exception: {type(e)}, {str(e)}")
  119. mail_admins(
  120. subject,
  121. message,
  122. connection=get_connection(
  123. "django.core.mail.backends.smtp.EmailBackend"
  124. ),
  125. )