Protect dedicated staging disk and document verified migration

This commit is contained in:
admin committed 2026-10-27 19:38:37 +01:00
1 parent 5ab3d078fb
commit 46644e5041
7 files changed
+158 -4

No files matched your search

+2
View File
@@ -175,6 +175,8 @@ cmd_runner() {
# New settings are optional in existing configs, and inherited by the runner.
export REMOTE="${REMOTE:-Nextcloud:}"
export STAGING_DIR="${STAGING_DIR:-${HOME}/.local/share/nextcloud-borg-backup/staging}"
export EXPECTED_STAGE_MOUNT="${EXPECTED_STAGE_MOUNT:-}"
export EXPECTED_STAGE_UUID="${EXPECTED_STAGE_UUID:-}"
export EXPECTED_NAS_SOURCE="${EXPECTED_NAS_SOURCE:-thor:/volume1/NetBackup}"
export EXPECTED_NAS_FSTYPE="${EXPECTED_NAS_FSTYPE:-nfs4}"
export BACKUP_INTERVAL_HOURS="${BACKUP_INTERVAL_HOURS:-24}"
+18 -2
View File
@@ -186,6 +186,22 @@ class Runner:
return count, size
def staging_preflight(self, manifest):
# Check a dedicated staging disk before mkdir or any sync. Without this
# guard a missing mount could silently put the staging tree on root.
stage_mount = os.environ.get('EXPECTED_STAGE_MOUNT', '')
stage_uuid = os.environ.get('EXPECTED_STAGE_UUID', '')
if bool(stage_mount) != bool(stage_uuid):
raise BackupError('EXPECTED_STAGE_MOUNT and EXPECTED_STAGE_UUID must be configured together')
if stage_mount:
mount_path = Path(stage_mount).absolute()
if mount_path not in self.stage.parents or mount_path.resolve() != mount_path:
raise BackupError('Staging must be below the configured staging mount')
mounts = json.loads(self.run(['findmnt', '--json', '--target', mount_path,
'--output', 'TARGET,UUID,FSTYPE'], capture=True))['filesystems']
if (len(mounts) != 1 or mounts[0].get('target') != str(mount_path)
or mounts[0].get('uuid') != stage_uuid
or mounts[0].get('fstype') not in ('ext4', 'btrfs', 'xfs', 'f2fs', 'zfs')):
raise BackupError('Dedicated staging disk is missing or has the wrong identity')
# Refuse symlinked, overlapping, mounted or foreign staging paths before rclone sync.
for other in (self.repo, self.state, Path.home() / 'Nextcloud'):
if self.stage == other or self.stage in other.parents or other in self.stage.parents:
@@ -339,7 +355,7 @@ class Runner:
self.run([self.borg, 'check', '--repository-only', self.repo])
# Success means a complete verified archive. Retention failure must not lose this fact.
self.success = {'completed_at': time.time(), 'archive': archive, 'files': count,
'bytes': size, 'repo': str(self.repo), 'remote': self.remote,
'bytes': size, 'repo': str(self.repo), 'remote': self.remote, 'stage': str(self.stage),
'archive_id': data['archives'][0]['id'], 'maintenance_pending': True}
atomic_json(self.success_file, self.success)
log(f'BACKUP VERIFIED: {archive} ({count} files, {size} bytes)')
@@ -400,7 +416,7 @@ def main():
sys.stdout = Tee()
# Child output goes to journal; lifecycle/phase/status additionally go to the run log.
try:
log('Verified cloud-to-NAS runner 2.0.0, revision 3')
log('Verified cloud-to-NAS runner 2.0.0, revision 4')
runner.backup('--if-due' in sys.argv)
finally:
sys.stdout = sys.__stdout__