Archived
236 lines
8.9 KiB
Python
236 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from datetime import UTC, datetime
|
|
|
|
from ..models import UpdateResult
|
|
from .base import StoreConnectionMixin
|
|
from .types import RecordRow, UpdateEventRow, record_row, update_event_row
|
|
|
|
|
|
class RecordsEventsMixin(StoreConnectionMixin):
|
|
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:
|
|
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:
|
|
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[RecordRow]:
|
|
return self.list_records_page(limit=10_000, offset=0)
|
|
|
|
def list_records_page(self, limit: int = 100, offset: int = 0) -> list[RecordRow]:
|
|
with self.connect() as conn:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT hostname, ipv4, ipv6, updated_at
|
|
FROM records
|
|
ORDER BY hostname
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
(limit, offset),
|
|
).fetchall()
|
|
return [record_row(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]:
|
|
with self.connect() as conn:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, count
|
|
FROM (
|
|
SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count
|
|
FROM update_events
|
|
UNION ALL
|
|
SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count
|
|
FROM update_event_rollups
|
|
)
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
(limit, offset),
|
|
).fetchall()
|
|
return [update_event_row(row) for row in rows]
|
|
|
|
def list_update_events_before(self, limit: int = 100, before: str | None = None) -> list[UpdateEventRow]:
|
|
where_clause = "WHERE created_at < ?" if before else ""
|
|
params: tuple[object, ...] = (before, limit) if before else (limit,)
|
|
with self.connect() as conn:
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, count
|
|
FROM (
|
|
SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count
|
|
FROM update_events
|
|
UNION ALL
|
|
SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count
|
|
FROM update_event_rollups
|
|
)
|
|
{where_clause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
""",
|
|
params,
|
|
).fetchall()
|
|
return [update_event_row(row) for row in rows]
|
|
|
|
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:
|
|
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 = [update_event_row(row) for row in event_rows]
|
|
rows.extend(update_event_row(row) for row in rollup_rows)
|
|
rows.sort(key=lambda row: str(row["created_at"]), reverse=True)
|
|
return rows[offset : offset + limit]
|