Add rollback logic when db update fails after DNS changes

This commit is contained in:
2026-02-05 20:19:43 +01:00
parent adaf08f4d1
commit 215fbb116e
2 changed files with 122 additions and 49 deletions

View File

@@ -4,7 +4,7 @@ import logging
import threading
from . import now_utc
from .models import Hostname, User
from .models import DatabaseError, Hostname, User
from datetime import timedelta
@@ -31,13 +31,15 @@ def cleanup_expired(app, start_time=None):
ipv6_expired = False
if hostname.last_ipv4:
last_update = max(hostname.last_ipv4_update, start_time)
last_update = max(hostname.last_ipv4_update, start_time) if start_time \
else hostname.last_ipv4_update
expiry_time = last_update + timedelta(seconds=hostname.expiry_ttl)
if now > expiry_time:
ipv4_expired = True
if hostname.last_ipv6:
last_update = max(hostname.last_ipv6_update, start_time)
last_update = max(hostname.last_ipv6_update, start_time) if start_time \
else hostname.last_ipv6_update
expiry_time = last_update + timedelta(seconds=hostname.expiry_ttl)
if now > expiry_time:
ipv6_expired = True
@@ -45,58 +47,92 @@ def cleanup_expired(app, start_time=None):
if not ipv4_expired and not ipv6_expired:
continue
ipv4_deleted = False
ipv6_deleted = False
old_ipv4 = hostname.last_ipv4
old_ipv6 = hostname.last_ipv6
ipv4_dns_deleted = False
ipv6_dns_deleted = False
if app.dns_service:
if ipv4_expired:
logging.info(
f"Cleanup: Host expired: hostname={hostname.hostname} zone={hostname.zone} "
f"ipv4={hostname.last_ipv4}"
if ipv4_expired:
logging.info(
f"Cleanup: Host expired: hostname={hostname.hostname} zone={hostname.zone} "
f"ipv4={hostname.last_ipv4}"
)
try:
ipv4_exists = app.dns_service.query_record(
hostname.hostname, hostname.zone, "A")
if ipv4_exists:
app.dns_service.delete_record(
hostname.hostname, hostname.zone, "A")
ipv4_dns_deleted = True
hostname.last_ipv4 = None
except Exception as e:
logging.error(f"DNS error: {e}")
logging.error(
f"Cleanup failed: hostname={hostname.hostname} "
f"zone={hostname.zone} type=A"
)
try:
app.dns_service.delete_record(hostname.hostname, hostname.zone, "A")
ipv4_deleted = True
except Exception as e:
logging.error(f"DNS error: {e}")
logging.error(
f"Cleanup failed: hostname={hostname.hostname} "
f"zone={hostname.zone} type=A"
)
if ipv6_expired:
logging.info(
f"Cleanup: Host expired: hostname={hostname.hostname} zone={hostname.zone} "
f"ipv6={hostname.last_ipv6}"
if ipv6_expired:
logging.info(
f"Cleanup: Host expired: hostname={hostname.hostname} zone={hostname.zone} "
f"ipv6={hostname.last_ipv6}"
)
try:
ipv6_exists = app.dns_service.query_record(
hostname.hostname, hostname.zone, "AAAA")
if ipv6_exists:
app.dns_service.delete_record(
hostname.hostname, hostname.zone, "AAAA")
ipv6_dns_deleted = True
hostname.last_ipv6 = None
except Exception as e:
logging.error(f"DNS error: {e}")
logging.error(
f"Cleanup failed: hostname={hostname.hostname} "
f"zone={hostname.zone} type=AAAA"
)
try:
app.dns_service.delete_record(hostname.hostname, hostname.zone, "AAAA")
ipv6_deleted = True
except Exception as e:
logging.error(f"DNS error: {e}")
logging.error(
f"Cleanup failed: hostname={hostname.hostname} "
f"zone={hostname.zone} type=AAAA"
)
if not (ipv4_deleted or ipv6_deleted):
if hostname.last_ipv4 == old_ipv4 and hostname.last_ipv6 == old_ipv6:
continue
try:
hostname.save()
except DatabaseError as e:
logging.error(
f"DB save failed after retries: hostname={hostname.hostname} "
f"zone={hostname.zone}: {e}"
)
# Rollback: re-add DNS records that were deleted
if ipv4_dns_deleted:
try:
app.dns_service.update_record(
hostname.hostname, hostname.zone,
old_ipv4, hostname.dns_ttl)
except Exception as e:
logging.error(f"DNS rollback failed (A): {e}")
if ipv6_dns_deleted:
try:
app.dns_service.update_record(
hostname.hostname, hostname.zone,
old_ipv6, hostname.dns_ttl)
except Exception as e:
logging.error(f"DNS rollback failed (AAAA): {e}")
continue
if app.email_service:
# Restore old IPs on in-memory model for email template
hostname.last_ipv4 = old_ipv4
hostname.last_ipv6 = old_ipv6
app.email_service.send_expiry_notification(
hostname.user.email,
hostname,
ipv4_deleted,
ipv6_deleted
hostname.last_ipv4 == old_ipv4,
hostname.last_ipv6 == old_ipv6
)
# Clear IP addresses only if DNS delete succeeded
if ipv4_deleted:
hostname.last_ipv4 = None
if ipv6_deleted:
hostname.last_ipv6 = None
hostname.save()
expired_count += 1
return expired_count

View File

@@ -452,14 +452,29 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
return hostname
def _rollback_dns(self, hostname, old_ip, record_type):
"""Roll back a DNS record to its previous value."""
try:
if old_ip:
self.app.dns_service.update_record(
hostname.hostname, hostname.zone,
old_ip, hostname.dns_ttl)
else:
self.app.dns_service.delete_record(
hostname.hostname, hostname.zone, record_type)
except Exception as e:
logging.error(f"DNS rollback failed ({record_type}): {e}")
def _process_ip_update(self, client_ip, user, hostname, ipv4, ipv6, notify_change):
"""Process IP update for hostname."""
now = now_utc()
old_ipv4 = hostname.last_ipv4
old_ipv6 = hostname.last_ipv6
ipv4_changed = False
ipv6_changed = False
if ipv4:
hostname.last_ipv4_update = now
if ipv4 != hostname.last_ipv4:
# Update DNS IPv4 record
try:
@@ -469,10 +484,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
ipv4,
hostname.dns_ttl
)
hostname.last_ipv4 = ipv4
ipv4_changed = True
except Exception as e:
hostname.save()
logging.error(f"DNS error: {e}")
raise DDNSError(
"Update failed",
@@ -484,7 +497,6 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
)
if ipv6:
hostname.last_ipv6_update = now
if ipv6 != hostname.last_ipv6:
# Update DNS IPv6 record
try:
@@ -494,11 +506,12 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
ipv6,
hostname.dns_ttl
)
hostname.last_ipv6 = ipv6
ipv6_changed = True
except Exception as e:
hostname.save()
logging.error(f"DNS error: {e}")
# Roll back IPv4 DNS if it was changed
if ipv4_changed:
self._rollback_dns(hostname, old_ipv4, "A")
raise DDNSError(
"Update failed",
STATUS_DNSERR,
@@ -508,8 +521,32 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
ipv6=ipv6
)
if ipv4_changed:
hostname.last_ipv4 = ipv4
hostname.last_ipv4_update = now
if ipv6_changed:
hostname.last_ipv6 = ipv6
hostname.last_ipv6_update = now
# Update database
hostname.save()
try:
hostname.save()
except DatabaseError as e:
logging.error(
f"DB save failed after retries: hostname={hostname.hostname} "
f"zone={hostname.zone}: {e}"
)
if ipv4_changed:
self._rollback_dns(hostname, old_ipv4, "A")
if ipv6_changed:
self._rollback_dns(hostname, old_ipv6, "AAAA")
raise DDNSError(
"Update failed",
STATUS_DNSERR,
client=client_ip,
hostname=hostname.hostname,
zone=hostname.zone
)
changed_addrs = ""
if ipv4_changed: