Files
awoooi/scripts/ops/backup-alert-live-visibility-check.py
ogt a535fa652f
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m22s
CD Pipeline / build-and-deploy (push) Successful in 11m52s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 21s
CD Pipeline / post-deploy-checks (push) Successful in 2m29s
fix(backup): preserve alert receipt identity
2026-07-15 01:08:44 +08:00

288 lines
11 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Verify live visibility for backup gap alerts.
This read-only check closes the gap between "metrics exist" and "alerts are
actually visible". If the offsite or credential-escrow gap metrics are present,
the corresponding Prometheus firing alerts must be visible. When Alertmanager is
provided, those same alerts must also be active there.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
class VisibilityError(RuntimeError):
pass
@dataclass(frozen=True)
class RequiredAlert:
name: str
labels: dict[str, str]
COMMON_LABELS = {
"host": "110",
"auto_repair": "false",
"alert_category": "infrastructure",
"notification_type": "TYPE-1",
"severity": "warning",
}
def _json_get(url: str, timeout: int) -> Any:
with urllib.request.urlopen(url, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def _prom_query(base_url: str, expr: str, timeout: int) -> list[dict[str, Any]]:
query = urllib.parse.urlencode({"query": expr})
url = f"{base_url.rstrip('/')}/api/v1/query?{query}"
payload = _json_get(url, timeout)
if payload.get("status") != "success":
raise VisibilityError(f"Prometheus query failed for {expr}: {payload}")
return payload.get("data", {}).get("result") or []
def _prom_alerts(base_url: str, timeout: int) -> list[dict[str, Any]]:
url = f"{base_url.rstrip('/')}/api/v1/alerts"
payload = _json_get(url, timeout)
if payload.get("status") != "success":
raise VisibilityError(f"Prometheus alerts query failed: {payload}")
return payload.get("data", {}).get("alerts") or []
def _alertmanager_alerts(base_url: str, timeout: int) -> list[dict[str, Any]]:
url = f"{base_url.rstrip('/')}/api/v2/alerts"
payload = _json_get(url, timeout)
if not isinstance(payload, list):
raise VisibilityError(f"Alertmanager alerts query returned unexpected payload: {payload}")
return payload
def _float_value(row: dict[str, Any], expr: str) -> float:
value = row.get("value")
if not isinstance(value, list) or len(value) < 2:
raise VisibilityError(f"Prometheus query returned unexpected value for {expr}: {row}")
try:
return float(value[1])
except (TypeError, ValueError) as exc:
raise VisibilityError(f"Prometheus query returned non-numeric value for {expr}: {row}") from exc
def _metric_labels(row: dict[str, Any]) -> dict[str, str]:
metric = row.get("metric") or {}
return {str(key): str(value) for key, value in metric.items()}
def _labels_match(actual: dict[str, str], expected: dict[str, str]) -> bool:
return all(actual.get(key) == value for key, value in expected.items())
def _find_prom_alert(alerts: list[dict[str, Any]], required: RequiredAlert) -> dict[str, Any] | None:
expected = {"alertname": required.name, **required.labels}
for alert in alerts:
if str(alert.get("state", "")) != "firing":
continue
labels = {str(key): str(value) for key, value in (alert.get("labels") or {}).items()}
if _labels_match(labels, expected):
return alert
return None
def _find_alertmanager_alert(alerts: list[dict[str, Any]], required: RequiredAlert) -> dict[str, Any] | None:
expected = {"alertname": required.name, **required.labels}
for alert in alerts:
status = alert.get("status") or {}
if str(status.get("state", "")) != "active":
continue
labels = {str(key): str(value) for key, value in (alert.get("labels") or {}).items()}
if _labels_match(labels, expected):
return alert
return None
def _require_prom_alert(alerts: list[dict[str, Any]], required: RequiredAlert) -> None:
if _find_prom_alert(alerts, required) is None:
raise VisibilityError(
f"missing Prometheus firing alert {required.name} with labels {required.labels}"
)
def _require_alertmanager_alert(alerts: list[dict[str, Any]], required: RequiredAlert) -> None:
if _find_alertmanager_alert(alerts, required) is None:
raise VisibilityError(
f"missing Alertmanager active alert {required.name} with labels {required.labels}"
)
def _sum_query_values(prometheus_url: str, expr: str, timeout: int) -> float:
return sum(_float_value(row, expr) for row in _prom_query(prometheus_url, expr, timeout))
def _max_query_value(prometheus_url: str, expr: str, timeout: int) -> float:
rows = _prom_query(prometheus_url, expr, timeout)
if not rows:
return 0
return max(_float_value(row, expr) for row in rows)
def _offsite_required_alerts(prometheus_url: str, host: str, timeout: int) -> tuple[list[RequiredAlert], str]:
expr = f'awoooi_backup_offsite_configured{{host="{host}"}}'
rows = _prom_query(prometheus_url, expr, timeout)
if not rows:
raise VisibilityError(f"Prometheus query returned no offsite configured series: {expr}")
configured_total = sum(_float_value(row, expr) for row in rows)
if configured_total == 0:
return (
[RequiredAlert("BackupOffsiteCopyNotConfigured", {**COMMON_LABELS, "host": host})],
"OK offsite gap metric requires BackupOffsiteCopyNotConfigured visibility",
)
fresh_expr = f'awoooi_backup_offsite_fresh{{host="{host}"}}'
if _sum_query_values(prometheus_url, fresh_expr, timeout) > 0:
return [], "OK offsite full marker is fresh; no offsite gap alert required"
enabled_expr = f'awoooi_backup_offsite_full_sync_enabled{{host="{host}"}}'
enabled_total = _sum_query_values(prometheus_url, enabled_expr, timeout)
if enabled_total > 0:
timestamp_expr = f'awoooi_backup_offsite_full_sync_enabled_timestamp{{host="{host}"}}'
enabled_timestamp = _max_query_value(prometheus_url, timestamp_expr, timeout)
enabled_age = int(time.time() - enabled_timestamp) if enabled_timestamp else 0
if enabled_timestamp and enabled_age <= 30 * 3600:
return (
[],
f"OK offsite full sync enabled within grace window; BackupOffsiteCopyStale not required yet age_seconds={enabled_age}",
)
return (
[RequiredAlert("BackupOffsiteCopyStale", {**COMMON_LABELS, "host": host})],
"OK offsite full marker gap requires BackupOffsiteCopyStale visibility",
)
def _escrow_required_alerts(prometheus_url: str, host: str, timeout: int) -> list[RequiredAlert]:
expr = f'awoooi_backup_credential_escrow_fresh{{host="{host}"}} == 0'
rows = _prom_query(prometheus_url, expr, timeout)
required: list[RequiredAlert] = []
for row in rows:
labels = _metric_labels(row)
item = labels.get("item")
if not item:
raise VisibilityError(f"Credential escrow gap metric missing item label: {row}")
required.append(
RequiredAlert(
"BackupCredentialEscrowEvidenceMissing",
{**COMMON_LABELS, "host": host, "item": item},
)
)
return sorted(required, key=lambda alert: alert.labels["item"])
def _receipt_stage_required_alerts(prometheus_url: str, host: str, timeout: int) -> list[RequiredAlert]:
# 直接從已 firing 的 meta alert 讀取 required_alert再對 Prometheus
# 與 Alertmanager 做同一 identity 核對。不得從缺 marker 推導假 receipt。
expr = (
'ALERTS{alertname="BackupAlertReceiptStageMissing",alertstate="firing",'
f'host="{host}"}}'
)
rows = _prom_query(prometheus_url, expr, timeout)
required: list[RequiredAlert] = []
for row in rows:
labels = _metric_labels(row)
required_alert = labels.get("required_alert")
missing = [
label
for label, value in {
"required_alert": required_alert,
}.items()
if not value
]
if missing:
raise VisibilityError(f"Backup alert receipt meta alert missing labels {missing}: {row}")
required.append(
RequiredAlert(
"BackupAlertReceiptStageMissing",
{
**COMMON_LABELS,
"host": host,
"required_alert": str(required_alert),
},
)
)
return sorted(required, key=lambda alert: alert.labels["required_alert"])
def live_check(prometheus_url: str, alertmanager_url: str, host: str, timeout: int) -> list[str]:
required_alerts: list[RequiredAlert] = []
lines: list[str] = []
offsite_alerts, offsite_line = _offsite_required_alerts(prometheus_url, host, timeout)
required_alerts.extend(offsite_alerts)
lines.append(offsite_line)
escrow_alerts = _escrow_required_alerts(prometheus_url, host, timeout)
required_alerts.extend(escrow_alerts)
if escrow_alerts:
escrow_items = ", ".join(alert.labels["item"] for alert in escrow_alerts)
lines.append(
f"OK credential escrow gap metrics require {len(escrow_alerts)} alert(s): {escrow_items}"
)
else:
lines.append("OK credential escrow markers are fresh; no escrow gap alert required")
receipt_stage_alerts = _receipt_stage_required_alerts(prometheus_url, host, timeout)
required_alerts.extend(receipt_stage_alerts)
if receipt_stage_alerts:
lines.append(
"OK firing backup alert receipt gaps preserve "
f"{len(receipt_stage_alerts)} required alert identity label(s): "
+ ", ".join(alert.labels["required_alert"] for alert in receipt_stage_alerts)
)
else:
lines.append("OK no BackupAlertReceiptStageMissing alert is currently firing")
prom_alerts = _prom_alerts(prometheus_url, timeout)
for required in required_alerts:
_require_prom_alert(prom_alerts, required)
lines.append(f"OK Prometheus exposes {len(required_alerts)} required backup gap firing alert(s)")
if alertmanager_url:
am_alerts = _alertmanager_alerts(alertmanager_url, timeout)
for required in required_alerts:
_require_alertmanager_alert(am_alerts, required)
lines.append(f"OK Alertmanager exposes {len(required_alerts)} required backup gap active alert(s)")
return lines
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--prometheus-url", required=True)
parser.add_argument("--alertmanager-url", default="")
parser.add_argument("--host", default="110")
parser.add_argument("--timeout", type=int, default=8)
args = parser.parse_args()
try:
for line in live_check(args.prometheus_url, args.alertmanager_url, args.host, args.timeout):
print(line)
except (VisibilityError, OSError, json.JSONDecodeError) as exc:
print(f"BACKUP_ALERT_LIVE_VISIBILITY_FAILED {exc}", file=sys.stderr)
return 1
print("BACKUP_ALERT_LIVE_VISIBILITY_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())