scavenge-unused.py 5.6 KB

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