Several stores¶
'STORE': [
{
'NAME': 'offsite',
'BACKEND': 'offsite_backup.stores.s3.S3Store',
'ENDPOINT_URL': os.environ.get('BACKUP_S3_ENDPOINT', ''),
'BUCKET': os.environ.get('BACKUP_S3_BUCKET', ''),
'ACCESS_KEY': os.environ.get('BACKUP_S3_ACCESS_KEY', ''),
'SECRET_KEY': os.environ.get('BACKUP_S3_SECRET_KEY', ''),
'LOCK_DAYS': 13,
},
{
'NAME': 'local',
'BACKEND': 'offsite_backup.stores.filesystem.DirectoryStore',
'ROOT': '/backups',
'KEEP_NIGHTS': 3,
},
],
STORE takes a list. The usual pair: a local volume for a fast restore of last night, and
an S3 bucket with Object Lock for the day the machine is gone — each kept as long as it is
worth keeping. A single block (no list) works exactly as before and needs no NAME.
One pass, every store¶
Every object a pass writes goes to each store in turn, under the same key: the
databases, every media volume and its manifest, the configuration archive. Each store
reads its own lock back — the S3 copy is COMPLIANCE-locked and checked, the directory copy
has none — and each is rotated on its own, by its own KEEP_NIGHTS
(RETENTION['KEEP_NIGHTS'] when it sets none). E007 compares each store's window with
its own LOCK_DAYS.
The message and backup_offsite count each object once and name the stores:
📦 Objects: 6
⚖️ Size: 412.7 MB
🗃 Stores: offsite, local
backup_offsite: 6 objects, 432758912 bytes up to each of 2 stores
A store that fails¶
No silent partial. A store that refuses an object — a full disk, an expired key, a bucket out of reach — is recorded, written to no more in this pass, and not rotated: its night is lost, and rotating it would make room for a backup it does not hold. The other stores carry the pass to the end, and then the pass fails by the store's name:
backup_offsite: 1 store(s) not written, the others hold the pass — local: OSError: [Errno 28] No space left on device
The FAILED message says the same, the last-success file is not written, and the nightly task's retry runs the whole pass again. With every store refusing there is nothing to carry on with, and the pass stops at the first object. With one store, a refusal is the pass's own error, as it always was.
The morning check asks every store and names the folder with its store
(local: config/ (none)): the copy that stopped arriving is the one nobody looks at until
the day it is needed. The database guard counts a night as done only when every store
holds it.
Names¶
With a list, every store needs a NAME, and no two the same — E001 otherwise, and a
pass refuses to start. The name is how the message, the morning check and --store call
it:
python manage.py backup_list --store local
python manage.py backup_restore --stamp latest --store local --apply --database default
Reads — backup_list, backup_restore, restore_database(..., store=), fetch(...,
store=), the latest stamp — go to the first store unless one is named. Put the store
you restore from most often first; name the other when the first is out of reach.
back_up_database() writes its one-off dump to every store and raises if any refuses it —
a removal that cannot be backed up everywhere does not happen; keep_apart() copies in
every store.