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>
121 lines
6.5 KiB
Python
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")),
|
|
)
|