fc3h: retire backup + rollback from migrate API/task — moved to /api/system/backup/* per FC-3h

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 22:59:11 -04:00
parent 7d42cddb11
commit d3d4320ed5
2 changed files with 11 additions and 71 deletions
+5 -39
View File
@@ -1,14 +1,11 @@
"""FC-5: /api/migrate — trigger and poll migration runs.
Ingest kinds (gs_ingest, ir_ingest) accept multipart/form-data with an
`export_file` field. All other kinds accept JSON. Apply-without-backup
guard rejects non-dry-run ingests unless a pre_migration-tagged backup
exists in the last 24h (override with body.force=true).
`export_file` field. All other kinds accept JSON. Backup + rollback
were retired in FC-3h (2026-05-24); use /api/system/backup/* instead.
"""
import json
from datetime import UTC, datetime, timedelta
from pathlib import Path
from quart import Blueprint, jsonify, request
from sqlalchemy import select
@@ -19,17 +16,12 @@ from ..tasks.migration import run_migration
migrate_bp = Blueprint("migrate", __name__, url_prefix="/api/migrate")
# 'backup' + 'rollback' retired 2026-05-24 (FC-3h); see /api/system/backup/*.
_VALID_KINDS = frozenset({
"backup", "gs_ingest", "ir_ingest", "tag_apply",
"ml_queue", "verify", "rollback", "cleanup",
"gs_ingest", "ir_ingest", "tag_apply",
"ml_queue", "verify", "cleanup",
})
_INGEST_KINDS = frozenset({"gs_ingest", "ir_ingest"})
# Backup gate retired 2026-05-24 — operator-flagged the speculative-safety
# requirement was actively blocking the UI ingest path (backup itself is
# unreliable on large NFS-backed image libraries) and FC-3h will rewrite
# the backup surface as a first-class feature with its own scheduling
# + recovery. Leaving the constant for historical grep.
_APPLY_KINDS: frozenset[str] = frozenset()
def _bad(error: str, *, status: int = 400, **extra):
@@ -38,19 +30,6 @@ def _bad(error: str, *, status: int = 400, **extra):
return jsonify(body), status
def _has_recent_pre_migration_backup() -> bool:
from ..services.migrators import backup as backup_mod
images_root = Path("/images")
manifest = backup_mod.find_latest_backup(images_root, tag="pre_migration")
if manifest is None:
return False
created_at_str = manifest.get("created_at")
if not created_at_str:
return False
created_at = datetime.fromisoformat(created_at_str)
return (datetime.now(UTC) - created_at) < timedelta(hours=24)
def _run_to_dict(run: MigrationRun) -> dict:
return {
"id": run.id,
@@ -83,7 +62,6 @@ async def create_run(kind: str):
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
return _bad("invalid_export_file", detail=str(exc))
dry_run = str(form.get("dry_run", "false")).lower() in ("true", "1", "yes")
force = str(form.get("force", "false")).lower() in ("true", "1", "yes")
params: dict = {"data": data, "dry_run": dry_run}
else:
body = await request.get_json()
@@ -92,20 +70,8 @@ async def create_run(kind: str):
if not isinstance(body, dict):
return _bad("invalid_body")
dry_run = bool(body.get("dry_run", False))
force = bool(body.get("force", False))
params = dict(body)
is_apply = (kind in _APPLY_KINDS) and not dry_run
if is_apply and not force and not _has_recent_pre_migration_backup():
return _bad(
"no_backup",
detail="apply action requires a pre_migration-tagged backup "
"in the last 24h (or force=true).",
)
if kind == "backup":
params.setdefault("tag", "pre_migration")
async with get_session() as session:
run = MigrationRun(kind=kind, status="pending", dry_run=dry_run)
session.add(run)