Files
FabledSteward/tests/integration/test_backup_restore_fixes.py
T
bvandeusenandClaude Opus 5.5 b875160c40
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m14s
CI / unit (push) Failing after 1m28s
CI / publish (push) Skipped
feat(backup): steward backup and steward restore; CI restores a dump and boots on it (#5672, #5674)
The backup file (#5672 "Backup file: pg_dump of config + audit + alert
history, the secret key and a manifest"): steward-<UTC>.tar holding a
pg_dump --format=custom with metrics', check results' and Docker/Traefik/
UniFi rows left out, the secret key, and a manifest. Every table is named
KEPT or EXCLUDED in steward/backup/contents.py, and a guard test fails on a
new one. The image gains postgresql-client-17 (approved, System 180).

The restore (#5674 "Restore: `steward restore` and a CI test that a dump
brings Steward back"): pg_restore in its three sections. Between data and
constraints, a kept row pointing at a left-out row gets what its foreign
key's ON DELETE says (alert_events.ansible_run_id is cleared). It refuses
a non-empty database unless --wipe, and puts the backup's key in
/data/secret.key. Migrations run on Steward's next start.

`steward` stays the server command: it is now a click group that serves
when given no subcommand.

The publish smoke step backs up the booted image's database, restores it
into a second one, checks a marker setting came back and plugin_metrics
is empty, and starts Steward on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:14:04 -04:00

63 lines
2.6 KiB
Python

"""Integration: between pg_restore's data and its constraints, an alert event
naming an Ansible run the backup left out keeps its row and loses the link
(steward/backup/restore.py, #5674). Requires STEWARD_DATABASE_URL."""
from __future__ import annotations
import asyncio
import os
import uuid
import pytest
pytestmark = pytest.mark.integration
@pytest.fixture
def app():
if not os.environ.get("STEWARD_DATABASE_URL"):
pytest.skip("needs Postgres")
from steward.app import create_app
return create_app(testing=False)
def test_a_dangling_ansible_run_link_is_cleared_and_the_event_kept(app):
from sqlalchemy import select, text
from steward.backup.restore import _execute, all_metadata, dangling_fixes
from steward.models.alerts import AlertEvent, AlertOperator, AlertRule
from steward.models.users import User, UserRole
uid, rule_id = str(uuid.uuid4()), str(uuid.uuid4())
gone_run, dangling, plain = str(uuid.uuid4()), str(uuid.uuid4()), str(uuid.uuid4())
async def _go():
async with app.db_sessionmaker() as s:
async with s.begin():
s.add(User(id=uid, username=f"u{uid[:8]}", email=f"{uid[:8]}@e.test",
password_hash="x", role=UserRole.admin, is_active=True))
async with s.begin():
s.add(AlertRule(id=rule_id, name="backup fixes", source_module="host_agent",
resource_name="srv1", metric_name="cpu_pct",
operator=AlertOperator.gt, threshold=90.0,
consecutive_failures_required=1, enabled=True, created_by=uid))
# The state pg_restore's data section leaves: the run's row was
# never restored. Replica mode lets the row in without its parent.
async with s.begin():
await s.execute(text("SET LOCAL session_replication_role = replica"))
s.add(AlertEvent(id=dangling, rule_id=rule_id, from_state="pending",
to_state="firing", ansible_run_id=gone_run))
s.add(AlertEvent(id=plain, rule_id=rule_id, from_state="firing",
to_state="inactive"))
fixed = await _execute(os.environ["STEWARD_DATABASE_URL"], dangling_fixes(all_metadata()))
async with app.db_sessionmaker() as s:
rows = dict((await s.execute(
select(AlertEvent.id, AlertEvent.ansible_run_id)
.where(AlertEvent.rule_id == rule_id))).all())
return fixed, rows
fixed, rows = asyncio.run(_go())
assert fixed >= 1
assert rows == {dangling: None, plain: None}