coulomb-social/scripts/backup_alerts.py
tegwick 267d502d93
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m16s
Implement durable recurring backup alert delivery candidate
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
2026-10-31 10:19:16 +01:00

266 lines
12 KiB
Python

"""Durable consumer outbox for the existing State Hub and Alertmanager owners.
No credentials or SMTP implementation. HTTP acceptance by Alertmanager is a
handoff, never proof of email delivery. Active events must be refreshed hourly;
their three-hour leases cannot replace independent monitoring of this producer.
"""
from __future__ import annotations
import fcntl
import json
import os
import stat
import tempfile
import uuid
from datetime import UTC, datetime, timedelta
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
CONDITIONS = {"backup_failed", "pending_backup", "backup_stale"}
RECEIVER = "coulomb-backup-platform-root-email"
MAX_EVENTS = 1024
MAX_BYTES = 16384
class Refused(ValueError):
pass
def private(path, directory=False):
info = path.lstat()
valid = stat.S_ISDIR(info.st_mode) if directory else stat.S_ISREG(info.st_mode)
if (not valid or info.st_uid != os.geteuid() or info.st_mode & 0o077
or (not directory and info.st_nlink != 1)):
raise Refused("unsafe_alert_state")
def checkpoint(path, record):
data = json.dumps(record, sort_keys=True).encode()
if len(data) > MAX_BYTES:
raise Refused("alert_record_too_large")
fd, name = tempfile.mkstemp(prefix=".checkpoint-", dir=path.parent)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
Path(name).unlink(missing_ok=True)
def timestamp(value):
parsed = datetime.fromisoformat(value)
if parsed.tzinfo is None:
raise Refused("timezone_required")
return parsed.astimezone(UTC)
def labels(record):
return {"alertname": "CoulombBackupProblem", "application": "coulomb-social",
"owner": "platform-root", "instance": "railiance01",
"condition": record["condition"], "event_id": record["event_id"],
"verification": "true" if record["test"] else "false"}
def message(record):
return {
"from_agent": "coulomb-social", "to_agent": "platform-root",
"subject": "Coulomb backup " + ("TEST " if record["test"] else "")
+ record["condition"],
"body": json.dumps({
"event_id": record["event_id"], "condition": record["condition"],
"observed_at": record["observed_at"], "test": record["test"],
"application": "coulomb-social", "host": "railiance01",
"workplan": "CSOC-WP-0012",
"action": "Inspect backup/freshness service and retained receipts. "
"Do not automatically restore, delete or discard a pending capture.",
}, sort_keys=True),
}
def request_json(url, payload=None):
request = Request(url, data=None if payload is None else json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urlopen(request, timeout=15) as response:
if response.status not in (200, 201):
raise Refused("unexpected_http_status")
data = response.read(65537)
if len(data) > 65536:
raise Refused("oversized_transport_response")
return json.loads(data) if data else None
def deliver_hub(base, record):
expected = message(record)
receipt = request_json(base.rstrip("/") + "/messages/", expected)
if not isinstance(receipt, dict) or any(receipt.get(k) != v for k, v in expected.items()):
raise Refused("central_inbox_receipt_required")
return str(uuid.UUID(receipt["id"]))
def deliver_alertmanager(base, record, now):
# Alertmanager's v2 API returns millisecond precision. Submit that same
# precision so exact readback does not falsely reject a successful handoff.
end = timestamp(record["resolved_at"]) if record.get("resolved_at") else (
now + timedelta(hours=3))
end = end.isoformat(timespec="milliseconds")
start = timestamp(record["observed_at"]).isoformat(timespec="milliseconds")
alert = {"labels": labels(record), "startsAt": start, "endsAt": end,
"annotations": {"summary": "Coulomb backup " + record["condition"],
"description": message(record)["body"]}}
endpoint = base.rstrip("/") + "/api/v2/alerts"
request_json(endpoint, [alert])
if not record.get("resolved_at"):
query = urlencode({"filter": 'event_id="' + record["event_id"] + '"'})
active = request_json(endpoint + "?" + query)
if not isinstance(active, list) or not any(
item.get("labels") == alert["labels"]
and timestamp(item["startsAt"]) == timestamp(alert["startsAt"])
and timestamp(item["endsAt"]) == timestamp(end)
and {r["name"] for r in item.get("receivers", [])} == {RECEIVER}
for item in active
):
raise Refused("reviewed_email_route_readback_required")
return {"status": "resolution_accepted" if record.get("resolved_at")
else "active_email_route_verified", "accepted_at": now.isoformat(),
"email_delivery_proven": False}
def load(path):
private(path)
if path.stat().st_size > MAX_BYTES:
raise Refused("alert_record_too_large")
with path.open() as stream:
record = json.load(stream)
if (record.get("schema") != "coulomb.backup-alert-event.v1"
or str(uuid.UUID(record["event_id"])) + ".json" != path.name
or record["condition"] not in CONDITIONS or type(record["test"]) is not bool):
raise Refused("invalid_alert_record")
timestamp(record["observed_at"])
if record.get("resolved_at"):
if timestamp(record["resolved_at"]) < timestamp(record["observed_at"]):
raise Refused("resolution_before_event")
return record
def drain(state, hub_base, alertmanager_base, *, event=None, resolve=None, now=None,
observations=None, recovered_capture=None,
hub_sender=deliver_hub, email_sender=deliver_alertmanager):
"""Serialize producers, checkpoint before sends, and attempt channels independently.
event is {event_id, condition, test}; a repeated ID must match. resolve names
an existing UUID. No automatic deletion: a full journal refuses new events
but still allows replay of existing events. Archive disposition is explicit.
"""
now = now or datetime.now(UTC)
if now.tzinfo is None:
raise Refused("timezone_required")
if event:
event = dict(event)
event["event_id"] = str(uuid.UUID(event["event_id"]))
if set(event) != {"event_id", "condition", "test"} or (
event["condition"] not in CONDITIONS or type(event["test"]) is not bool
):
raise Refused("invalid_event")
if resolve:
resolve = str(uuid.UUID(resolve))
if observations is not None and (set(observations) != CONDITIONS
or any(type(v) is not bool for v in observations.values())):
raise Refused("complete_health_observations_required")
if recovered_capture:
recovered_capture = timestamp(recovered_capture)
if observations is None or any(observations.values()) or recovered_capture > now:
raise Refused("healthy_new_capture_required")
state.mkdir(mode=0o700, parents=True, exist_ok=True)
private(state, directory=True)
if state.resolve() != state.absolute():
raise Refused("symlinked_alert_state")
fd = os.open(state / "notify.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as lock:
private(state / "notify.lock")
fcntl.flock(lock, fcntl.LOCK_EX)
paths = sorted(state.glob("*.json"))
records = {p.stem: (p, load(p)) for p in paths}
if event:
key = event["event_id"]
if key in records:
if any(records[key][1][k] != v for k, v in event.items()):
raise Refused("event_id_reused_with_different_content")
else:
if len(records) >= MAX_EVENTS:
raise Refused("alert_journal_full_requires_archival")
record = {"schema": "coulomb.backup-alert-event.v1", **event,
"origin": "explicit",
"observed_at": now.isoformat(), "hub_message_id": None,
"alertmanager": None}
path = state / (key + ".json")
checkpoint(path, record)
records[key] = (path, record)
if observations is not None:
for condition, firing in sorted(observations.items()):
active = [(p, r) for p, r in records.values()
if r.get("origin") == "health" and r["condition"] == condition
and not r["test"] and not r.get("resolved_at")]
if firing and not active:
if len(records) >= MAX_EVENTS:
raise Refused("alert_journal_full_requires_archival")
key = str(uuid.uuid4())
record = {"schema": "coulomb.backup-alert-event.v1", "event_id": key,
"condition": condition, "test": False, "origin": "health",
"observed_at": now.isoformat(), "hub_message_id": None,
"alertmanager": None}
path = state / (key + ".json")
checkpoint(path, record)
records[key] = (path, record)
elif not firing:
for path, record in active:
record["resolved_at"] = now.isoformat()
checkpoint(path, record)
if recovered_capture:
for path, record in records.values():
if (record.get("origin") == "explicit" and not record["test"]
and record["condition"] == "backup_failed"
and not record.get("resolved_at")
and timestamp(record["observed_at"]) < recovered_capture):
record["resolved_at"] = now.isoformat()
checkpoint(path, record)
if resolve:
path, record = records[resolve]
if not record.get("resolved_at"):
record["resolved_at"] = now.isoformat()
checkpoint(path, record)
failures = []
# Rotate unfinished work across bounded systemd invocations: a slow or
# unreachable channel must not permanently starve later events.
ordered = sorted(records.items(), key=lambda item: item[1][1].get("last_attempt_at", ""))
for key, (path, record) in ordered:
if (record.get("hub_message_id")
and (record.get("alertmanager") or {}).get("status") == "resolution_accepted"):
continue
record["last_attempt_at"] = now.isoformat()
checkpoint(path, record)
if not record.get("hub_message_id"):
try:
record["hub_message_id"] = hub_sender(hub_base, record)
checkpoint(path, record)
except Exception:
failures.append({"event_id": key, "channel": "state_hub"})
accepted = record.get("alertmanager") or {}
if accepted.get("status") == "resolution_accepted":
continue
try:
record["alertmanager"] = email_sender(alertmanager_base, record, now)
checkpoint(path, record)
except Exception:
failures.append({"event_id": key, "channel": "email_handoff"})
return {"status": "pending_channel_retry" if failures else "channel_handoffs_verified",
"failures": failures, "events": len(records), "email_delivery_proven": False,
"automatic_monitoring_active": False}