add host while preserving filechange detection #53
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Moritz 2024-09-17 19:37:22 +02:00
parent ac7c5fb50d
commit 3aefae61c0

View File

@ -18,7 +18,7 @@ from shutil import copyfile, rmtree
VOLUME_PATH = "/var/lib/docker/volumes/" VOLUME_PATH = "/var/lib/docker/volumes/"
SECRET_PATH = '/secrets/' SECRET_PATH = '/secrets/'
SERVICE = None SERVICE = 'ALL'
logger = logging.getLogger("backupbot") logger = logging.getLogger("backupbot")
logging.addLevelName(55, 'SUMMARY') logging.addLevelName(55, 'SUMMARY')
@ -100,7 +100,7 @@ def create(retries):
app_settings = parse_backup_labels() app_settings = parse_backup_labels()
pre_commands, post_commands, backup_paths, apps = get_backup_details(app_settings) pre_commands, post_commands, backup_paths, apps = get_backup_details(app_settings)
copy_secrets(apps) copy_secrets(apps)
backup_paths.append(SECRET_PATH) backup_paths.append(Path(SECRET_PATH))
run_commands(pre_commands) run_commands(pre_commands)
backup_volumes(backup_paths, apps, int(retries)) backup_volumes(backup_paths, apps, int(retries))
run_commands(post_commands) run_commands(post_commands)
@ -114,10 +114,10 @@ def create(retries):
@click.option('container', '--container', '-c', envvar='CONTAINER', multiple=True) @click.option('container', '--container', '-c', envvar='CONTAINER', multiple=True)
def restore(snapshot, target, noninteractive, volumes, container): def restore(snapshot, target, noninteractive, volumes, container):
app_settings = parse_backup_labels('restore', container) app_settings = parse_backup_labels('restore', container)
if SERVICE: if SERVICE != 'ALL':
app_settings = {SERVICE: app_settings[SERVICE]} app_settings = {SERVICE: app_settings[SERVICE]}
pre_commands, post_commands, backup_paths, apps = get_backup_details(app_settings, volumes) pre_commands, post_commands, backup_paths, apps = get_backup_details(app_settings, volumes)
snapshots = get_snapshots(snapshot_id=snapshot, app=SERVICE) snapshots = get_snapshots(snapshot_id=snapshot)
if not snapshot: if not snapshot:
logger.error("No Snapshots with ID {snapshots} for {apps} found.") logger.error("No Snapshots with ID {snapshots} for {apps} found.")
exit(1) exit(1)
@ -136,7 +136,6 @@ def restore(snapshot, target, noninteractive, volumes, container):
logger.error("Restore aborted") logger.error("Restore aborted")
exit(1) exit(1)
print(f"Restoring Snapshot {snapshot} at {target}") print(f"Restoring Snapshot {snapshot} at {target}")
# TODO: use tags if no snapshot is selected, to use a snapshot including SERVICE
run_commands(pre_commands) run_commands(pre_commands)
result = restic_restore(snapshot_id=snapshot, include=backup_paths, target_dir=target) result = restic_restore(snapshot_id=snapshot, include=backup_paths, target_dir=target)
run_commands(post_commands) run_commands(post_commands)
@ -152,16 +151,15 @@ def restic_restore(snapshot_id='latest', include=[], target_dir=None):
return restic.internal.command_executor.execute(cmd) return restic.internal.command_executor.execute(cmd)
def get_snapshots(snapshot_id=None, app=None): def get_snapshots(snapshot_id=None):
if snapshot_id and snapshot_id != 'latest': if snapshot_id and snapshot_id != 'latest':
snapshots = restic.snapshots(snapshot_id=snapshot_id) snapshots = restic.snapshots(snapshot_id=snapshot_id)
if app and app not in snapshots[0]['tags']: if SERVICE not in snapshots[0]['tags']:
logger.error(f'Snapshot with ID {snapshot_id} does not contain {app}') logger.error(f'Snapshot with ID {snapshot_id} does not contain {SERVICE}')
exit(1) exit(1)
else: else:
snapshots = restic.snapshots() snapshots = restic.snapshots()
if app: snapshots = list(filter(lambda x: x.get('tags') and SERVICE in x.get('tags'), snapshots))
snapshots = list(filter(lambda x: app in x.get('tags'), snapshots))
if snapshot_id == 'latest': if snapshot_id == 'latest':
return snapshots[-1:] return snapshots[-1:]
else: else:
@ -214,10 +212,8 @@ def get_backup_details(app_settings, volumes=[]):
post_hooks = {} post_hooks = {}
for app, settings in app_settings.items(): for app, settings in app_settings.items():
if settings.get('enabled'): if settings.get('enabled'):
# Uncomment these lines to backup only a specific service if SERVICE != 'ALL' and SERVICE != app:
# This will unfortenately decrease restice performance continue
# if SERVICE and SERVICE != app:
# continue
backup_apps.append(app) backup_apps.append(app)
add_backup_paths(backup_paths, settings, app, volumes) add_backup_paths(backup_paths, settings, app, volumes)
if hooks:= settings.get('pre_hooks'): if hooks:= settings.get('pre_hooks'):
@ -309,7 +305,7 @@ def copy_secrets(apps):
f"For the secret {sec['SecretName']} the file {src} does not exist for {s.name}") f"For the secret {sec['SecretName']} the file {src} does not exist for {s.name}")
continue continue
dst = SECRET_PATH + sec['SecretName'] dst = SECRET_PATH + sec['SecretName']
logger.debug("Copy Secret {sec['SecretName']}") logger.debug(f"Copy Secret {sec['SecretName']}")
copyfile(src, dst) copyfile(src, dst)
@ -337,10 +333,17 @@ def run_commands(commands):
def backup_volumes(backup_paths, apps, retries, dry_run=False): def backup_volumes(backup_paths, apps, retries, dry_run=False):
while True: while True:
try: try:
logger.info("Start volume backup") logger.info("Backup these paths:")
logger.debug(backup_paths) logger.debug("\n".join(map(str, backup_paths)))
backup_paths = list(filter(path_exists, backup_paths)) backup_paths = list(filter(path_exists, backup_paths))
result = restic.backup(backup_paths, dry_run=dry_run, tags=apps) cmd = restic.cat.base_command()
parent = get_snapshots('latest')
if parent:
# https://restic.readthedocs.io/en/stable/040_backup.html#file-change-detection
cmd.extend(['--parent', parent[0]['short_id']])
tags = set(apps + [SERVICE])
logger.info("Start volume backup")
result = restic.internal.backup.run(cmd, backup_paths, dry_run=dry_run, tags=tags)
logger.summary("backup finished", extra=result) logger.summary("backup finished", extra=result)
return return
except ResticFailedError as error: except ResticFailedError as error:
@ -361,12 +364,12 @@ def path_exists(path):
@cli.command() @cli.command()
def snapshots(): def snapshots():
snapshots = get_snapshots(app=SERVICE) snapshots = get_snapshots()
for snap in snapshots: for snap in snapshots:
print(snap['time'], snap['id']) print(snap['time'], snap['id'])
if not snapshots: if not snapshots:
err_msg = "No Snapshots found" err_msg = "No Snapshots found"
if SERVICE: if SERVICE != 'ALL':
service_name = SERVICE.replace('_', '.') service_name = SERVICE.replace('_', '.')
err_msg += f' for app {service_name}' err_msg += f' for app {service_name}'
logger.warning(err_msg) logger.warning(err_msg)
@ -384,8 +387,7 @@ def ls(snapshot, path):
def list_files(snapshot, path): def list_files(snapshot, path):
cmd = restic.cat.base_command() + ['ls'] cmd = restic.cat.base_command() + ['ls']
if SERVICE: cmd = cmd + ['--tag', SERVICE]
cmd = cmd + ['--tag', SERVICE]
cmd.append(snapshot) cmd.append(snapshot)
if path: if path:
cmd.append(path) cmd.append(path)
@ -394,7 +396,7 @@ def list_files(snapshot, path):
except ResticFailedError as error: except ResticFailedError as error:
if 'no snapshot found' in str(error): if 'no snapshot found' in str(error):
err_msg = f'There is no snapshot "{snapshot}"' err_msg = f'There is no snapshot "{snapshot}"'
if SERVICE: if SERVICE != 'ALL':
err_msg += f' for the app "{SERVICE}"' err_msg += f' for the app "{SERVICE}"'
logger.error(err_msg) logger.error(err_msg)
exit(1) exit(1)
@ -426,7 +428,7 @@ def download(snapshot, path, volumes, secrets):
tarinfo.size = len(binary_output) tarinfo.size = len(binary_output)
file_dumps.append((binary_output, tarinfo)) file_dumps.append((binary_output, tarinfo))
if volumes: if volumes:
if not SERVICE: if SERVICE == 'ALL':
logger.error("Please specify '--host' when using '--volumes'") logger.error("Please specify '--host' when using '--volumes'")
exit(1) exit(1)
files = list_files(snapshot, VOLUME_PATH) files = list_files(snapshot, VOLUME_PATH)
@ -439,7 +441,7 @@ def download(snapshot, path, volumes, secrets):
tarinfo.size = len(binary_output) tarinfo.size = len(binary_output)
file_dumps.append((binary_output, tarinfo)) file_dumps.append((binary_output, tarinfo))
if secrets: if secrets:
if not SERVICE: if SERVICE == 'ALL':
logger.error("Please specify '--host' when using '--secrets'") logger.error("Please specify '--host' when using '--secrets'")
exit(1) exit(1)
filename = f"{SERVICE}.json" filename = f"{SERVICE}.json"
@ -476,8 +478,7 @@ def get_formatted_size(file_path):
def dump(snapshot, path): def dump(snapshot, path):
cmd = restic.cat.base_command() + ['dump'] cmd = restic.cat.base_command() + ['dump']
if SERVICE: cmd = cmd + ['--tag', SERVICE]
cmd = cmd + ['--tag', SERVICE]
cmd = cmd + [snapshot, path] cmd = cmd + [snapshot, path]
print(f"Dumping {path} from snapshot '{snapshot}'") print(f"Dumping {path} from snapshot '{snapshot}'")
output = subprocess.run(cmd, capture_output=True) output = subprocess.run(cmd, capture_output=True)