feat: add borg-wrapper
This commit is contained in:
2 files changed
+208
No files matched your search
@@ -0,0 +1,189 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
"""
|
||||
Very primitive and unpolished borg wrapper for certain actions.
|
||||
|
||||
It relies on a JSON config from which it reads borg vault locations and passwords.
|
||||
"""
|
||||
|
||||
|
||||
def get_destination_host(destination_hosts, backup_destination):
|
||||
for entry in destination_hosts:
|
||||
if entry['alias'] == backup_destination:
|
||||
return entry['host']
|
||||
raise Exception(f"Destination host '{backup_destination}' is missing its configuration.")
|
||||
|
||||
|
||||
def sync_backup(args):
|
||||
operation = args.operation
|
||||
|
||||
print("Opening password file ...")
|
||||
# Read Borg archive passwords from JSON file using sudo
|
||||
password_file_path = '/root/borg_passwords.json'
|
||||
password_command = f'sudo cat {password_file_path}'
|
||||
password_data = os.popen(password_command).read().strip()
|
||||
|
||||
# Parse JSON data
|
||||
passwords = json.loads(password_data)
|
||||
|
||||
if operation == 'create_all':
|
||||
for entry in passwords['archives']:
|
||||
password = entry['password']
|
||||
destination_host_alias = entry['destination_host_alias']
|
||||
destination_host = get_destination_host(passwords['destination_hosts'], destination_host_alias)
|
||||
local_archive_root_path = entry['local_archive_root_path']
|
||||
remote_archive_path = entry['remote_archive_path']
|
||||
local_backup_source_dir = entry['local_backup_source_dir']
|
||||
|
||||
if args.backup_destination is not None and args.backup_destination != destination_host_alias:
|
||||
print(f'skipping archive {entry['archive_name']} of destination {destination_host_alias}')
|
||||
continue
|
||||
if not args.skip_missing_source and not os.path.exists(local_archive_root_path):
|
||||
raise RuntimeError(f"local archive root path '{local_archive_root_path}' doesn't exist")
|
||||
elif args.skip_missing_source:
|
||||
print(f"local archive root path is missing, skipping")
|
||||
continue
|
||||
|
||||
# Set up Borg command with operation as the first argument
|
||||
borg_env_var = f'export BORG_PASSPHRASE="{password}"'
|
||||
borg_command = f'{borg_env_var}; cd {local_archive_root_path} && borg create --rsh \'ssh -i ~/.ssh/id_ed25519\''
|
||||
|
||||
if args.dry_run is True:
|
||||
print("dry run enabled")
|
||||
dry_run_or_stats = '--dry-run' if args.dry_run is True else '--stats'
|
||||
if args.ignore_inode:
|
||||
borg_command += ' --files-cache ctime,size' # ignore inode (useful for external disks)
|
||||
borg_command += f' --iec --progress {dry_run_or_stats} -v --compression none --checkpoint-interval=900'
|
||||
borg_command += f' {destination_host}{remote_archive_path}::' + '{utcnow}Z ' + f'{local_backup_source_dir}'
|
||||
|
||||
# Execute Borg command
|
||||
print(f'[CMD] {";".join(borg_command.split(";")[1:])}')
|
||||
retcode = subprocess.call(borg_command, shell=True)
|
||||
if retcode != 0:
|
||||
raise RuntimeError("exited with non-zero return code")
|
||||
elif operation == 'prune_all':
|
||||
for entry in passwords['archives']:
|
||||
password = entry['password']
|
||||
destination_host = get_destination_host(passwords['destination_hosts'], entry['destination_host_alias'])
|
||||
local_archive_root_path = entry['local_archive_root_path']
|
||||
remote_archive_path = entry['remote_archive_path']
|
||||
#local_backup_source_dir = entry['local_backup_source_dir']
|
||||
|
||||
if not args.skip_missing_source and not os.path.exists(local_archive_root_path):
|
||||
raise RuntimeError(f"local archive root path '{local_archive_root_path}' doesn't exist")
|
||||
elif args.skip_missing_source:
|
||||
print(f"local archive root path is missing, skipping")
|
||||
continue
|
||||
|
||||
# Set up Borg command with operation as the first argument
|
||||
borg_env_var = f'export BORG_PASSPHRASE="{password}"'
|
||||
borg_command = f'{borg_env_var}; cd {local_archive_root_path} && borg prune --rsh \'ssh -i ~/.ssh/id_ed25519\''
|
||||
|
||||
if args.dry_run is True:
|
||||
print("dry run enabled")
|
||||
dry_run_or_stats = '--dry-run' if args.dry_run is True else '--stats'
|
||||
borg_command += f' --iec --progress {dry_run_or_stats} -v'
|
||||
borg_command += f' --list --keep-last 1 {destination_host}{remote_archive_path}'
|
||||
|
||||
# Execute Borg command
|
||||
print(f'[CMD] {";".join(borg_command.split(";")[1:])}')
|
||||
retcode = subprocess.call(borg_command, shell=True)
|
||||
if retcode != 0:
|
||||
raise RuntimeError("exited with non-zero return code")
|
||||
elif operation == 'compact_all':
|
||||
for entry in passwords['archives']:
|
||||
password = entry['password']
|
||||
destination_host = get_destination_host(passwords['destination_hosts'], entry['destination_host_alias'])
|
||||
local_archive_root_path = entry['local_archive_root_path']
|
||||
remote_archive_path = entry['remote_archive_path']
|
||||
#local_backup_source_dir = entry['local_backup_source_dir']
|
||||
|
||||
if not args.skip_missing_source and not os.path.exists(local_archive_root_path):
|
||||
raise RuntimeError(f"local archive root path '{local_archive_root_path}' doesn't exist")
|
||||
elif args.skip_missing_source:
|
||||
print(f"local archive root path is missing, skipping")
|
||||
continue
|
||||
|
||||
# Set up Borg command with operation as the first argument
|
||||
borg_env_var = f'export BORG_PASSPHRASE="{password}"'
|
||||
borg_command = f'{borg_env_var}; cd {local_archive_root_path} && borg compact --rsh \'ssh -i ~/.ssh/id_ed25519\''
|
||||
|
||||
borg_command += f' --iec --progress -v {destination_host}{remote_archive_path}'
|
||||
|
||||
# Execute Borg command
|
||||
print(f'[CMD] {";".join(borg_command.split(";")[1:])}')
|
||||
retcode = subprocess.call(borg_command, shell=True)
|
||||
if retcode != 0:
|
||||
raise RuntimeError("exited with non-zero return code")
|
||||
else:
|
||||
found = False
|
||||
for entry in passwords['archives']:
|
||||
if entry['archive_name'] == args.archive_name and entry['destination_host_alias'] == args.backup_destination:
|
||||
found = True
|
||||
password = entry['password']
|
||||
destination_host = get_destination_host(passwords['destination_hosts'], args.backup_destination)
|
||||
local_archive_root_path = entry['local_archive_root_path']
|
||||
remote_archive_path = entry['remote_archive_path']
|
||||
local_backup_source_dir = entry['local_backup_source_dir']
|
||||
|
||||
if not args.skip_missing_source and not os.path.exists(local_archive_root_path):
|
||||
raise RuntimeError(f"local archive root path '{local_archive_root_path}' doesn't exist")
|
||||
elif args.skip_missing_source:
|
||||
print(f"local archive root path is missing, skipping")
|
||||
continue
|
||||
|
||||
# Set up Borg command with operation as the first argument
|
||||
borg_env_var = f'export BORG_PASSPHRASE="{password}"'
|
||||
borg_command = f'{borg_env_var}; cd {local_archive_root_path} && borg {operation} --rsh \'ssh -i ~/.ssh/id_ed25519\''
|
||||
|
||||
if operation == 'create':
|
||||
dry_run_or_stats = '--dry-run' if args.dry_run is True else '--stats'
|
||||
if args.ignore_inode:
|
||||
borg_command += ' --files-cache ctime,size' # ignore inode (useful for external disks)
|
||||
if not args.skip_list:
|
||||
borg_command += ' --list'
|
||||
borg_command += f' --iec --progress {dry_run_or_stats} -v --compression none --checkpoint-interval=900'
|
||||
borg_command += f' {destination_host}{remote_archive_path}::' + '{utcnow}Z ' + f'{local_backup_source_dir}'
|
||||
elif operation == 'list':
|
||||
borg_command += f' {destination_host}{remote_archive_path}'
|
||||
elif operation == 'info':
|
||||
borg_command += f' --iec {destination_host}{remote_archive_path}'
|
||||
elif operation == 'prune':
|
||||
borg_command += f' --list --keep-last 1 {destination_host}{remote_archive_path}'
|
||||
elif operation == 'compact':
|
||||
borg_command += f' --iec --progress {destination_host}{remote_archive_path}'
|
||||
# elif operation == 'delete':
|
||||
else:
|
||||
print(f"Invalid operation '{operation}'")
|
||||
return
|
||||
|
||||
# Execute Borg command
|
||||
print(f'[CMD] {";".join(borg_command.split(";")[1:])}')
|
||||
os.system(borg_command)
|
||||
break
|
||||
if not found:
|
||||
print(f"Couldn't find a pair of archive '{args.archive_name}' and destination '{args.backup_destination}'.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Sync backups using Borg via CLI")
|
||||
parser.add_argument("operation", type=str, help="Borg operation (create, list, prune, info, compact, create_all, prune_all, compact_all)")
|
||||
parser.add_argument("-a", "--archive-name", type=str, help="Name of the Borg archive", required=False)
|
||||
parser.add_argument("-d", "--backup-destination", type=str, help="Destination for the backup", required=False)
|
||||
parser.add_argument("--dry-run", type=bool, help="If specified, shows only what would happen", required=False)
|
||||
parser.add_argument("--skip-missing-source", action=argparse.BooleanOptionalAction, help="Skips syncing a missing archive instead of crashing", required=False)
|
||||
parser.add_argument("--ignore-inode", action=argparse.BooleanOptionalAction, help="Ignores inodes when creating a backup", required=False)
|
||||
parser.add_argument("--skip-list", action=argparse.BooleanOptionalAction, help="Skips printing list of synced files", required=False)
|
||||
arguments = parser.parse_args()
|
||||
print(f'debug: {arguments.operation}')
|
||||
if arguments.operation in ["create_all", "prune_all", "compact_all"]:
|
||||
if arguments.archive_name is not None:
|
||||
raise RuntimeError(f"Argument -a cannot be used with operation '{arguments.operation}'.")
|
||||
else:
|
||||
if arguments.archive_name is None or arguments.backup_destination is None:
|
||||
raise RuntimeError(f"Arguments -a and -d must be used with operation '{arguments.operation}'.")
|
||||
sync_backup(arguments)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"destination_hosts": [
|
||||
{
|
||||
"alias": "your-host",
|
||||
"host": "ssh://your-server.example.com:22"
|
||||
}
|
||||
],
|
||||
"archives": [
|
||||
{
|
||||
"archive_name": "arc01",
|
||||
"destination_host_alias": "your-host",
|
||||
"remote_archive_path": "/home/remote-user/archive01",
|
||||
"password": "placeholder",
|
||||
"local_archive_root_path": "/home/local-user/archive01/",
|
||||
"local_backup_source_dir": "./"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user