fix: Implement proper block-level Borg backups

BREAKING CHANGE: Borg backups now store raw block devices instead of files

Changes:
- LV→Borg: Stores snapshot as raw block device (.img file) in Borg
- VG→Borg: Stores each LV as separate .img files in Borg repository
- No more file-level mounting - preserves exact block-level state
- Uses dd | borg create --stdin-name for LV backups
- Creates temporary .img files for VG backups
- Maintains all filesystem metadata, boot sectors, etc.
- Better deduplication for similar block patterns

Benefits:
- Exact block-level restoration possible
- Preserves all filesystem metadata
- Better suited for system/boot volume backups
- Still gets Borg's compression, deduplication, encryption
- Clear difference between LV and VG modes

Now LV→Borg and VG→Borg have distinct, useful purposes:
- LV→Borg: Single logical volume as one block device
- VG→Borg: All logical volumes as separate block devices
This commit is contained in:
root
2025-10-09 00:45:22 +02:00
parent 179a84e442
commit aee3d5019c
4 changed files with 113 additions and 137 deletions

View File

@@ -490,8 +490,8 @@ class SimpleBackupGUI:
self.log("VG copy completed - target device now contains complete LVM structure")
def backup_lv_to_borg(self, source_lv, repo_path):
"""Backup LV to Borg repository"""
self.log("Mode: LV to Borg Repository")
"""Backup LV to Borg repository (block-level)"""
self.log("Mode: LV to Borg Repository (block-level backup)")
# Set up environment for Borg
borg_env = os.environ.copy()
@@ -523,57 +523,44 @@ class SimpleBackupGUI:
if not success:
raise Exception(f"Failed to create snapshot: {output}")
# Create a temporary mount point
import tempfile
temp_mount = tempfile.mkdtemp(prefix="borg_backup_")
# Create Borg backup of the raw block device
archive_name = f"lv_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
self.log(f"Creating Borg archive (block-level): {archive_name}")
self.log("Backing up raw snapshot block device to Borg...")
self.log("This preserves exact block-level state including filesystem metadata")
try:
# Mount the snapshot
self.log(f"Mounting snapshot to {temp_mount}")
success, output = self.run_command(f"mount {self.current_snapshot} {temp_mount}")
if not success:
raise Exception(f"Failed to mount snapshot: {output}")
# Create Borg backup
archive_name = f"lv_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
self.log(f"Creating Borg archive: {archive_name}")
self.log("This may take a long time depending on data size...")
borg_cmd = f"borg create --progress --stats {repo_path}::{archive_name} {temp_mount}"
# Run borg with environment
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=borg_env)
# Stream output
for line in process.stdout:
# Use stdin mode to pipe the block device into borg
borg_cmd = f"dd if={self.current_snapshot} bs=4M | borg create --stdin-name '{lv_name}.img' --progress --stats {repo_path}::{archive_name} -"
# Run command with Borg environment
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=borg_env)
# Stream output
for line in process.stdout:
if line.strip():
self.log(line.strip())
process.wait()
if process.returncode != 0:
raise Exception("Borg backup failed")
self.log("Borg backup completed successfully")
finally:
# Cleanup: unmount and remove temp directory
self.log("Cleaning up mount point")
self.run_command(f"umount {temp_mount}", show_output=False)
os.rmdir(temp_mount)
# Remove snapshot
self.log("Cleaning up snapshot")
success, output = self.run_command(f"lvremove -f {self.current_snapshot}")
if not success:
self.log(f"Warning: Failed to remove snapshot: {output}")
else:
self.log("Snapshot cleaned up")
self.current_snapshot = None
process.wait()
if process.returncode != 0:
raise Exception("Borg block-level backup failed")
self.log("Block-level Borg backup completed successfully")
# Cleanup snapshot
self.log("Cleaning up snapshot")
success, output = self.run_command(f"lvremove -f {self.current_snapshot}")
if not success:
self.log(f"Warning: Failed to remove snapshot: {output}")
else:
self.log("Snapshot cleaned up")
self.current_snapshot = None
def backup_vg_to_borg(self, source_vg, repo_path):
"""Backup entire VG to Borg repository"""
self.log("Mode: Entire VG to Borg Repository")
"""Backup entire VG to Borg repository (block-level)"""
self.log("Mode: Entire VG to Borg Repository (block-level backup)")
self.log("This will store all LV snapshots as raw block devices in Borg")
# Set up environment for Borg
borg_env = os.environ.copy()
@@ -606,7 +593,6 @@ class SimpleBackupGUI:
self.log(f"Found {len(lv_names)} logical volumes: {', '.join(lv_names)}")
snapshots_created = []
mount_points = []
try:
# Create snapshots for all LVs
@@ -620,62 +606,59 @@ class SimpleBackupGUI:
if not success:
raise Exception(f"Failed to create snapshot for {lv_name}: {output}")
snapshots_created.append(snapshot_path)
snapshots_created.append((snapshot_path, lv_name))
# Mount all snapshots
import tempfile
base_temp_dir = tempfile.mkdtemp(prefix="borg_vg_backup_")
for i, snapshot_path in enumerate(snapshots_created):
lv_name = lv_names[i]
mount_point = os.path.join(base_temp_dir, lv_name)
os.makedirs(mount_point)
mount_points.append(mount_point)
self.log(f"Mounting {snapshot_path} to {mount_point}")
success, output = self.run_command(f"mount {snapshot_path} {mount_point}")
if not success:
self.log(f"Warning: Failed to mount {snapshot_path}: {output}")
# Continue with other LVs
# Create Borg backup of the entire VG structure
# Create Borg archive with all block devices
archive_name = f"vg_{source_vg}_{time.strftime('%Y%m%d_%H%M%S')}"
self.log(f"Creating Borg archive: {archive_name}")
self.log("This may take a long time depending on data size...")
self.log(f"Creating Borg archive (block-level): {archive_name}")
self.log("Backing up all LV snapshots as raw block devices...")
borg_cmd = f"borg create --progress --stats {repo_path}::{archive_name} {base_temp_dir}"
# Create temporary directory structure for the archive
import tempfile
temp_dir = tempfile.mkdtemp(prefix="borg_vg_backup_")
# Run borg with environment
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=borg_env)
# Stream output
for line in process.stdout:
self.log(line.strip())
process.wait()
if process.returncode != 0:
raise Exception("Borg backup failed")
self.log("Borg backup of entire VG completed successfully")
finally:
# Cleanup: unmount all mount points
for mount_point in mount_points:
if os.path.ismount(mount_point):
self.log(f"Unmounting {mount_point}")
self.run_command(f"umount {mount_point}", show_output=False)
# Remove temp directory
if 'base_temp_dir' in locals():
try:
# Copy all snapshots to temporary files that Borg can backup
for snapshot_path, lv_name in snapshots_created:
temp_file = os.path.join(temp_dir, f"{lv_name}.img")
self.log(f"Creating temporary image file for {lv_name}")
# Use dd to copy snapshot to temporary file
copy_cmd = f"dd if={snapshot_path} of={temp_file} bs=4M"
success, output = self.run_command(copy_cmd)
if not success:
raise Exception(f"Failed to create temp file for {lv_name}: {output}")
# Create Borg backup of all the block device images
borg_cmd = f"borg create --progress --stats {repo_path}::{archive_name} {temp_dir}"
# Run borg with environment
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=borg_env)
# Stream output
for line in process.stdout:
if line.strip():
self.log(line.strip())
process.wait()
if process.returncode != 0:
raise Exception("Borg VG backup failed")
self.log("Block-level VG Borg backup completed successfully")
finally:
# Remove temporary directory
import shutil
try:
shutil.rmtree(base_temp_dir)
shutil.rmtree(temp_dir)
self.log("Temporary files cleaned up")
except:
self.log(f"Warning: Could not remove temp directory {base_temp_dir}")
self.log(f"Warning: Could not remove temp directory {temp_dir}")
finally:
# Remove all snapshots
for snapshot_path in snapshots_created:
for snapshot_path, lv_name in snapshots_created:
self.log(f"Removing snapshot {snapshot_path}")
success, output = self.run_command(f"lvremove -f {snapshot_path}")
if not success: