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