Files
FabledSteward/steward/backup/contents.py
T
bvandeusenandClaude Opus 5.5 8eae3d3825
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m31s
CI / integration (push) Failing after 1m40s
CI / publish (push) Skipped
feat(syslog): pattern alerts: telling phrases counted per source each minute for alert rules, storage-trouble presets (#6001)
A pattern (phrases, a severity, a source, a window) is counted every minute
into plugin_metrics as lines_matching_<key> per added source, zero included,
so an ordinary alert rule fires on a match and clears once the window passes.
Each count reads only the lines stored since the last, by line id; a new or
changed pattern reads its window once.

Patterns page with ready-made storage-trouble presets and "Alert on it",
which opens the rule form aimed at the count. Source names are kept unique,
and a rename moves its counts and the rules on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 06:22:54 -04:00

121 lines
6.5 KiB
Python

"""What a backup carries. Every table Steward defines is named here as kept
or left out, and tests/core/test_backup_contents.py fails on one that is
neither, so a new table is a decision someone makes in this file.
The operator's ruling (System 180 "Backup & Restore", 2026-10-15): a backup
is for system recovery. It carries settings and keys, what would otherwise
have to be set up again, and no logs or history: not the audit log, not
alert history, not metrics. Left-out tables come back with their structure
and no rows, and fill again as Steward runs.
A table that is in neither list (an external plugin's) is backed up whole:
a table nobody thought about is kept, never lost.
"""
from __future__ import annotations
# What would otherwise have to be set up again.
KEPT: frozenset[str] = frozenset({
"app_settings", "users", "api_tokens",
"hosts", "host_agent_registrations",
# A group's maintenance cadence (#5768): configuration.
"host_maintenance_plans",
"monitors",
"alert_rules", "maintenance_windows",
"ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules",
"dashboards", "dashboard_widgets", "dashboard_share_tokens",
"snmp_devices", "prometheus_targets", "prometheus_widget_templates",
# Names given to network clients by hand (UniFi's alias column).
"unifi_known_clients",
# Syslog senders: which were added, under what name, and which ignored.
"syslog_sources",
"syslog_patterns",
})
# Rows that are measurements, logs, history or in-flight work: left out.
EXCLUDED: frozenset[str] = frozenset({
# Logs and history (the 2026-10-15 ruling): a recovery doesn't need them.
"audit_events", "alert_events",
# Live state: each rule starts inactive, and alert_reconcile gives a
# restored rule its state row within a minute (ensure_rule_states).
"alert_states",
# Metric anomalies and their reviews (#5740): history of the metrics.
"metric_anomalies", "metric_anomaly_spans",
# A pending invite is in flight, not setup; it can be sent again.
"user_invites",
"plugin_metrics", "plugin_metric_latest", "plugin_metrics_hourly",
# Learned from the metrics above, so relearned once they fill again.
"metric_norms",
"monitor_results",
# A host's downtime as its agent reported it: history, like the metrics.
"host_downtime",
# Maintenance jobs (#5767): run history, like ansible_runs.
"host_maintenance_jobs", "host_maintenance_runs",
"email_outbox", "email_outbox_events", "password_resets",
# Run history; its logs live on disk, not in the database.
"ansible_runs",
"docker_containers", "docker_disk_usage", "docker_events", "docker_host_status",
"docker_images", "docker_logs",
"docker_swarm_nodes", "docker_swarm_services",
"traefik_access_summaries", "traefik_certs", "traefik_global_stats", "traefik_metrics",
"traefik_status_codes",
"unifi_alarms", "unifi_client_snapshots", "unifi_devices", "unifi_dpi_snapshots",
"unifi_events", "unifi_fw_rules", "unifi_networks", "unifi_port_forwards",
"unifi_speedtest_results", "unifi_wan_stats", "unifi_wlan_stats",
"syslog_lines",
})
# ── What a check compares (#5823) ────────────────────────────────────────────
# Checking a backup compares its kept rows with the ones in place. Some columns
# of kept tables are Steward's own record of what it last saw, not something
# anyone set up: they change by the minute, so comparing them would call every
# backup out of date. They are left out of the comparison (a restore still
# carries them). `updated_at` is bookkeeping wherever it appears.
ACTIVITY: dict[str, frozenset[str]] = {
"api_tokens": frozenset({"last_used_at"}),
# What the agent reports about its host; the enrolment (host_id,
# token_hash, token_created_at) is what is compared.
"host_agent_registrations": frozenset({
"agent_version", "kernel", "distro", "arch", "host_ip", "last_seen_at",
"clock_offset_s", "clock_checked_at", "updates_manager", "updates_pending",
"updates_security", "updates_checked_at", "updates_error", "reboot_required",
"reboot_required_since", "reboot_packages", "maint_helper", "drive_health",
"drive_health_at"}),
"host_maintenance_plans": frozenset({"last_slot_at"}),
"monitors": frozenset({"last_checked_at"}),
"ansible_schedules": frozenset({"last_run_at", "last_run_id", "last_error"}),
"snmp_devices": frozenset({"last_polled_at", "last_ok_at", "last_error", "missing"}),
"prometheus_targets": frozenset({"last_scraped_at", "last_ok_at", "last_error", "last_kept",
"last_dropped", "last_capped"}),
# What the sender last sent; its address, name, state and host are setup.
"syslog_sources": frozenset({"hostname_seen", "first_seen_at", "last_seen_at", "lines_total",
"last_severity", "last_line"}),
# Scraped from the controller; the name and note are what someone set.
"unifi_known_clients": frozenset({"hostname", "ip", "mac_vendor", "is_blocked", "first_seen",
"last_seen", "scraped_at"}),
}
ACTIVITY_EVERYWHERE: frozenset[str] = frozenset({"updated_at"})
# Settings Steward writes as it runs: a record of its own work, not setup.
STATE_SETTINGS: frozenset[str] = frozenset({
"backup.history", "backup.last_attempt_at", "backup.last_success_at",
"reports.last_sent_at", "cleanup.last_run",
})
# A network client only counts once someone has named it or noted it; the
# rest are the controller's, and come and go as devices do.
HAND_SET_ONLY: dict[str, tuple[str, ...]] = {"unifi_known_clients": ("name", "note")}
# How a check groups what it found, in the order it shows them. A table
# not named here is shown under "Other".
AREAS: tuple[tuple[str, tuple[str, ...]], ...] = (
("Settings", ("app_settings",)),
("Users and API tokens", ("users", "api_tokens")),
("Hosts and agents", ("hosts", "host_agent_registrations", "host_maintenance_plans")),
("Monitors", ("monitors",)),
("Alert rules and maintenance windows", ("alert_rules", "maintenance_windows")),
("Ansible", ("ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules")),
("Dashboards", ("dashboards", "dashboard_widgets", "dashboard_share_tokens")),
("Plugins", ("snmp_devices", "prometheus_targets", "prometheus_widget_templates",
"unifi_known_clients", "syslog_sources", "syslog_patterns")),
)