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>
63 lines
2.6 KiB
Python
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}
|