feat: with an off-site destination set up, database backups stream to it
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 1s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 41s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 2s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m50s
CI and images / smoke-web (push) Successful in 49s
CI and images / promote (push) Successful in 2s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 1s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 41s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 2s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m50s
CI and images / smoke-web (push) Successful in 49s
CI and images / promote (push) Successful in 2s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped
The nightly database backup (and "Run DB backup now") now sends pg_dump's output straight to the off-site bucket as a multipart upload, with no copy staged on this server. The object is named database/fabledcurator-<UTC time>.dump and appears only once pg_dump has exited cleanly and every part has landed; a failed dump aborts the upload and records pg_dump's last line. Without a destination, dumps are written to /images/_backups as before. - backup_s3: Upload (start / add part / complete / abort), with a busy or silent bucket rested and asked again (15 s, 60 s, Retry-After obeyed), and a Pacer that holds uploads to the new bandwidth limit. - Settings: "Upload limit (Mbit/s)" on the destination card, 200 by default, 0 for none (migration 0120). - pg_dump runs under nice (and ionice where installed), locally too. - Backups card says where dumps go; the runs table gains "Saved to" and shows a failed run's error. Off-site rows have no restore/tag/delete here: restoring from the bucket is a later step (#6880). Task #6878, milestone 586. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,27 @@ async def test_restore_rejects_non_ok_status(client, db):
|
||||
assert body["error"] == "not_restorable"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_off_site_dump_is_not_restored_from_this_screen(client, db):
|
||||
db.add(BackupRun(
|
||||
kind="db", status="ok", tag=None, triggered_by="nightly",
|
||||
started_at=datetime.now(UTC), finished_at=datetime.now(UTC),
|
||||
manifest={"offsite": {"endpoint": "http://archives.example:8081",
|
||||
"bucket": "curator", "key": "database/x.dump"}},
|
||||
))
|
||||
await db.commit()
|
||||
run = (await (await client.get("/api/system/backup/runs?kind=db&limit=1")).get_json())[
|
||||
"runs"][0]
|
||||
assert run["offsite"]["bucket"] == "curator"
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/system/backup/runs/{run['id']}/restore",
|
||||
json={"confirm": f"restore-db-{run['id']}"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert (await resp.get_json())["error"] == "not_restorable"
|
||||
|
||||
|
||||
# --- DELETE /runs/<id> ----------------------------------------------
|
||||
|
||||
|
||||
@@ -352,7 +373,7 @@ async def test_destination_saves_and_never_returns_the_secret(client, db_sync, _
|
||||
assert body == {
|
||||
"endpoint": "http://archives.example:8081", "bucket": "curator",
|
||||
"access_key": "AKEXAMPLE", "region": "us-east-1",
|
||||
"secret_set": True, "configured": True,
|
||||
"secret_set": True, "configured": True, "upload_limit_mbps": 200,
|
||||
}
|
||||
assert "s3cr3t-example" not in str(await (await client.get(
|
||||
"/api/system/backup/destination")).get_json())
|
||||
@@ -398,7 +419,7 @@ async def test_destination_remove_forgets_everything(client, _sealing_key):
|
||||
resp = await client.delete("/api/system/backup/destination")
|
||||
assert await resp.get_json() == {
|
||||
"endpoint": "", "bucket": "", "access_key": "", "region": "us-east-1",
|
||||
"secret_set": False, "configured": False,
|
||||
"secret_set": False, "configured": False, "upload_limit_mbps": 200,
|
||||
}
|
||||
|
||||
|
||||
@@ -455,3 +476,23 @@ async def test_destination_test_reports_an_unreachable_bucket_as_a_result(
|
||||
"ok": False,
|
||||
"message": "Couldn't reach http://archives.example:8081: nothing is listening there.",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_upload_limit_saves_and_a_missing_one_keeps_it(client, _sealing_key):
|
||||
resp = await client.patch(
|
||||
"/api/system/backup/destination", json={**_DEST, "upload_limit_mbps": 50},
|
||||
)
|
||||
assert (await resp.get_json())["upload_limit_mbps"] == 50
|
||||
resp = await client.patch("/api/system/backup/destination", json=_DEST)
|
||||
assert (await resp.get_json())["upload_limit_mbps"] == 50
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("limit", [-1, 2.5, "fast", True])
|
||||
async def test_an_upload_limit_that_isnt_a_whole_number_is_refused(client, _sealing_key, limit):
|
||||
resp = await client.patch(
|
||||
"/api/system/backup/destination", json={**_DEST, "upload_limit_mbps": limit},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "upload_limit_mbps" in (await resp.get_json())["detail"]
|
||||
|
||||
Reference in New Issue
Block a user