Build KarlDNS self-hosted DynDNS service

This commit is contained in:
StasonJatham committed 2026-05-20 23:55:27 +02:00
1 parent 45bc38d950
commit ca92dc211a
65 files changed
+9200 -3570

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
from __future__ import annotations
import sqlite3
from datetime import UTC, datetime
from ..ddns_models import UpdateResult
from .types import UpdateEventRow
class RecordsEventsMixin:
def upsert(
self,
hostname: str,
ipv4: str | None,
ipv6: str | None,
update_ipv4: bool = True,
update_ipv6: bool = True,
) -> UpdateResult:
now = datetime.now(UTC).isoformat()
with self.connect() as conn:
conn.row_factory = sqlite3.Row
existing = conn.execute("SELECT ipv4, ipv6 FROM records WHERE hostname = ?", (hostname,)).fetchone()
current_ipv4 = existing["ipv4"] if existing else None
current_ipv6 = existing["ipv6"] if existing else None
effective_ipv4 = ipv4 if update_ipv4 else current_ipv4
effective_ipv6 = ipv6 if update_ipv6 else current_ipv6
changed = existing is None or current_ipv4 != effective_ipv4 or current_ipv6 != effective_ipv6
conn.execute(
"""
INSERT INTO records(hostname, ipv4, ipv6, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(hostname) DO UPDATE SET
ipv4=excluded.ipv4,
ipv6=excluded.ipv6,
updated_at=excluded.updated_at
""",
(hostname, effective_ipv4, effective_ipv6, now),
)
return UpdateResult(
hostname=hostname,
ipv4=effective_ipv4,
ipv6=effective_ipv6,
changed=changed,
update_ipv4=update_ipv4,
update_ipv6=update_ipv6,
)
def commit_update_result(self, result: UpdateResult) -> UpdateResult:
now = datetime.now(UTC).isoformat()
with self.connect() as conn:
conn.execute(
"""
INSERT INTO records(hostname, ipv4, ipv6, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(hostname) DO UPDATE SET
ipv4=excluded.ipv4,
ipv6=excluded.ipv6,
updated_at=excluded.updated_at
""",
(result.hostname, result.ipv4, result.ipv6, now),
)
return result
def preview_update(
self,
hostname: str,
ipv4: str | None,
ipv6: str | None,
update_ipv4: bool = True,
update_ipv6: bool = True,
) -> UpdateResult:
with self.connect() as conn:
conn.row_factory = sqlite3.Row
existing = conn.execute("SELECT ipv4, ipv6 FROM records WHERE hostname = ?", (hostname,)).fetchone()
current_ipv4 = existing["ipv4"] if existing else None
current_ipv6 = existing["ipv6"] if existing else None
effective_ipv4 = ipv4 if update_ipv4 else current_ipv4
effective_ipv6 = ipv6 if update_ipv6 else current_ipv6
changed = existing is None or current_ipv4 != effective_ipv4 or current_ipv6 != effective_ipv6
return UpdateResult(
hostname=hostname,
ipv4=effective_ipv4,
ipv6=effective_ipv6,
changed=changed,
update_ipv4=update_ipv4,
update_ipv6=update_ipv6,
)
def list_records(self) -> list[dict[str, str | None]]:
return self.list_records_page(limit=10_000, offset=0)
def list_records_page(self, limit: int = 100, offset: int = 0) -> list[dict[str, str | None]]:
with self.connect() as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT hostname, ipv4, ipv6, updated_at
FROM records
ORDER BY hostname
LIMIT ? OFFSET ?
""",
(limit, offset),
).fetchall()
return [dict(row) for row in rows]
def count_records(self) -> int:
with self.connect() as conn:
row = conn.execute("SELECT COUNT(*) FROM records").fetchone()
return int(row[0])
def log_update_event(
self,
hostname: str,
ipv4: str | None,
ipv6: str | None,
status: str,
detail: str,
source_ip: str,
) -> None:
with self.connect() as conn:
conn.execute(
"""
INSERT INTO update_events(hostname, ipv4, ipv6, status, detail, source_ip, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(hostname, ipv4, ipv6, status, detail[:500], source_ip, datetime.now(UTC).isoformat()),
)
def log_update_rollup(
self,
hostname: str,
ipv4: str | None,
ipv6: str | None,
status: str,
detail: str,
source_ip: str,
) -> None:
now = datetime.now(UTC).isoformat()
with self.connect() as conn:
conn.execute(
"""
INSERT INTO update_event_rollups(
hostname, ipv4, ipv6, status, detail, source_ip, first_seen_at, last_seen_at, count
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
ON CONFLICT(hostname, ipv4, ipv6, status, detail, source_ip) DO UPDATE SET
last_seen_at=excluded.last_seen_at,
count=count + 1
""",
(hostname, ipv4 or "", ipv6 or "", status, detail[:500], source_ip, now, now),
)
def list_update_events(self, limit: int = 100, offset: int = 0) -> list[UpdateEventRow]:
return self.list_update_events_page(limit=limit, offset=offset)
def list_update_events_page(self, limit: int = 100, offset: int = 0) -> list[UpdateEventRow]:
source_limit = limit + offset
with self.connect() as conn:
conn.row_factory = sqlite3.Row
event_rows = conn.execute(
"""
SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count
FROM update_events
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""",
(source_limit, 0),
).fetchall()
rollup_rows = conn.execute(
"""
SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count
FROM update_event_rollups
ORDER BY last_seen_at DESC
LIMIT ? OFFSET ?
""",
(source_limit, 0),
).fetchall()
return _merge_event_rows(event_rows, rollup_rows, limit, offset)
def count_update_events(self) -> int:
with self.connect() as conn:
event_count = conn.execute("SELECT COUNT(*) FROM update_events").fetchone()[0]
rollup_count = conn.execute("SELECT COUNT(*) FROM update_event_rollups").fetchone()[0]
return int(event_count) + int(rollup_count)
def list_update_events_for_hostname(self, hostname: str, limit: int = 25) -> list[UpdateEventRow]:
with self.connect() as conn:
conn.row_factory = sqlite3.Row
event_rows = conn.execute(
"""
SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count
FROM update_events
WHERE hostname = ?
ORDER BY created_at DESC
LIMIT ?
""",
(hostname, limit),
).fetchall()
rollup_rows = conn.execute(
"""
SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count
FROM update_event_rollups
WHERE hostname = ?
ORDER BY last_seen_at DESC
LIMIT ?
""",
(hostname, limit),
).fetchall()
return _merge_event_rows(event_rows, rollup_rows, limit, 0)
def _merge_event_rows(
event_rows: list[sqlite3.Row],
rollup_rows: list[sqlite3.Row],
limit: int,
offset: int,
) -> list[UpdateEventRow]:
rows = [dict(row) for row in event_rows]
rows.extend(dict(row) for row in rollup_rows)
rows.sort(key=lambda row: str(row["created_at"]), reverse=True)
return rows[offset : offset + limit]