diff --git a/src/restic_compose_backup/cli.py b/src/restic_compose_backup/cli.py index bd8a763..4db35b5 100644 --- a/src/restic_compose_backup/cli.py +++ b/src/restic_compose_backup/cli.py @@ -1,20 +1,8 @@ import argparse -import os import logging from typing import List -from restic_compose_backup import ( - alerts, - backup_runner, - log, - restic, -) -from restic_compose_backup.config import Config -from restic_compose_backup.containers import RunningContainers -from restic_compose_backup import utils -from restic_compose_backup import commands - -logger = logging.getLogger(__name__) +from restic_compose_backup import commands, log def main(): @@ -22,187 +10,6 @@ def main(): args = parse_args(sorted(commands.COMMANDS.keys())) command = commands.COMMANDS[args.action](args) command.run() - return - - # Ensure log level is propagated to parent container if overridden - # if args.log_level: - # containers.this_container.set_config_env('LOG_LEVEL', args.log_level) - - if args.action == 'status': - status(config, containers) - - elif args.action == 'snapshots': - snapshots(config, containers) - - elif args.action == 'backup': - backup(config, containers) - - elif args.action == 'start-backup-process': - start_backup_process(config, containers) - - elif args.action == 'cleanup': - cleanup(config, containers) - - elif args.action == 'alert': - alert(config, containers) - - -def backup(config, containers): - """Request a backup to start""" - # Make sure we don't spawn multiple backup processes - if containers.backup_process_running: - alerts.send( - subject="Backup process container already running", - body=( - "A backup process container is already running. \n" - f"Id: {containers.backup_process_container.id}\n" - f"Name: {containers.backup_process_container.name}\n" - ), - alert_type='ERROR', - ) - raise RuntimeError("Backup process already running") - - # Map all volumes from the backup container into the backup process container - volumes = containers.this_container.volumes - - # Map volumes from other containers we are backing up - mounts = containers.generate_backup_mounts('/volumes') - volumes.update(mounts) - - logger.debug('Starting backup container with image %s', containers.this_container.image) - try: - result = backup_runner.run( - image=containers.this_container.image, - command='restic-compose-backup start-backup-process', - volumes=volumes, - environment=containers.this_container.environment, - source_container_id=containers.this_container.id, - labels={ - containers.backup_process_label: 'True', - "com.docker.compose.project": containers.project_name, - }, - ) - except Exception as ex: - logger.exception(ex) - alerts.send( - subject="Exception during backup", - body=str(ex), - alert_type='ERROR', - ) - return - - logger.info('Backup container exit code: %s', result) - - # Alert the user if something went wrong - if result != 0: - alerts.send( - subject="Backup process exited with non-zero code", - body=open('backup.log').read(), - alert_type='ERROR', - ) - - -def start_backup_process(config, containers): - """The actual backup process running inside the spawned container""" - if not utils.is_true(os.environ.get('BACKUP_PROCESS_CONTAINER')): - logger.error( - "Cannot run backup process in this container. Use backup command instead. " - "This will spawn a new container with the necessary mounts." - ) - alerts.send( - subject="Cannot run backup process in this container", - body=( - "Cannot run backup process in this container. Use backup command instead. " - "This will spawn a new container with the necessary mounts." - ) - ) - exit(1) - - status(config, containers) - errors = False - - # Did we actually get any volumes mounted? - try: - has_volumes = os.stat('/volumes') is not None - except FileNotFoundError: - logger.warning("Found no volumes to back up") - has_volumes = False - - # Warn if there is nothing to do - if len(containers.containers_for_backup()) == 0 and not has_volumes: - logger.error("No containers for backup found") - exit(1) - - if has_volumes: - try: - logger.info('Backing up volumes') - vol_result = restic.backup_files(config.repository, source='/volumes') - logger.debug('Volume backup exit code: %s', vol_result) - if vol_result != 0: - logger.error('Volume backup exited with non-zero code: %s', vol_result) - errors = True - except Exception as ex: - logger.error('Exception raised during volume backup') - logger.exception(ex) - errors = True - - # back up databases - logger.info('Backing up databases') - for container in containers.containers_for_backup(): - if container.database_backup_enabled: - try: - instance = container.instance - logger.info('Backing up %s in service %s', instance.container_type, instance.service_name) - result = instance.backup() - logger.debug('Exit code: %s', result) - if result != 0: - logger.error('Backup command exited with non-zero code: %s', result) - errors = True - except Exception as ex: - logger.exception(ex) - errors = True - - if errors: - logger.error('Exit code: %s', errors) - exit(1) - - # Only run cleanup if backup was successful - result = cleanup(config, container) - logger.debug('cleanup exit code: %s', result) - if result != 0: - logger.error('cleanup exit code: %s', result) - exit(1) - - # Test the repository for errors - logger.info("Checking the repository for errors") - result = restic.check(config.repository) - if result != 0: - logger.error('Check exit code: %s', result) - exit(1) - - logger.info('Backup completed') - - -def cleanup(config, containers): - """Run forget / prune to minimize storage space""" - logger.info('Forget outdated snapshots') - forget_result = restic.forget( - config.repository, - config.keep_daily, - config.keep_weekly, - config.keep_monthly, - config.keep_yearly, - ) - logger.info('Prune stale data freeing storage space') - prune_result = restic.prune(config.repository) - return forget_result and prune_result - - -def snapshots(config, containers): - """Display restic snapshots""" - stdout, stderr = restic.snapshots(config.repository, last=True) - for line in stdout.decode().split('\n'): - print(line) def parse_args(choices: List[str]): diff --git a/src/restic_compose_backup/commands/backup.py b/src/restic_compose_backup/commands/backup.py index 4fcde82..9d24aa6 100644 --- a/src/restic_compose_backup/commands/backup.py +++ b/src/restic_compose_backup/commands/backup.py @@ -1,4 +1,5 @@ from .base import BaseCommand +from restic_compose_backup import backup_runner, alerts class Command(BaseCommand): @@ -6,4 +7,56 @@ class Command(BaseCommand): name = "backup" def run(self): - print("Backup!") + """Run the backup command""" + containers = self.get_containers() + + if containers.backup_process_running: + alerts.send( + subject="Backup process container already running", + body=( + "A backup process container is already running. \n" + f"Id: {containers.backup_process_container.id}\n" + f"Name: {containers.backup_process_container.name}\n" + ), + alert_type='ERROR', + ) + raise RuntimeError("Backup process already running") + + # Map all volumes from the backup container into the backup process container + volumes = containers.this_container.volumes + + # Map volumes from other containers we are backing up + mounts = containers.generate_backup_mounts('/volumes') + volumes.update(mounts) + + self.logger.debug('Starting backup container with image %s', containers.this_container.image) + try: + result = backup_runner.run( + image=containers.this_container.image, + command='restic-compose-backup start_backup_process', + volumes=volumes, + environment=containers.this_container.environment, + source_container_id=containers.this_container.id, + labels={ + containers.backup_process_label: 'True', + "com.docker.compose.project": containers.project_name, + }, + ) + except Exception as ex: + self.logger.exception(ex) + alerts.send( + subject="Exception during backup", + body=str(ex), + alert_type='ERROR', + ) + return + + self.logger.info('Backup container exit code: %s', result) + + # Alert the user if something went wrong + if result != 0: + alerts.send( + subject="Backup process exited with non-zero code", + body=open('backup.log').read(), + alert_type='ERROR', + ) diff --git a/src/restic_compose_backup/commands/base.py b/src/restic_compose_backup/commands/base.py index 22422cf..31ff572 100644 --- a/src/restic_compose_backup/commands/base.py +++ b/src/restic_compose_backup/commands/base.py @@ -17,7 +17,9 @@ class BaseCommand: def get_containers(self): """Get running containers""" - return RunningContainers() + containers = RunningContainers() + containers.this_container.set_config_env('LOG_LEVEL', self.log_level) + return containers def run(self): """Run the command""" diff --git a/src/restic_compose_backup/commands/cleanup.py b/src/restic_compose_backup/commands/cleanup.py index e5416d0..e0fb505 100644 --- a/src/restic_compose_backup/commands/cleanup.py +++ b/src/restic_compose_backup/commands/cleanup.py @@ -1,4 +1,5 @@ from .base import BaseCommand +from restic_compose_backup import restic class Command(BaseCommand): @@ -6,4 +7,15 @@ class Command(BaseCommand): name = "cleanup" def run(self): - print("Cleanup!") + """Run forget / prune to minimize storage space""" + self.logger.info('Forget outdated snapshots') + forget_result = restic.forget( + self.config.repository, + self.config.keep_daily, + self.config.keep_weekly, + self.config.keep_monthly, + self.config.keep_yearly, + ) + self.logger.info('Prune stale data freeing storage space') + prune_result = restic.prune(self.config.repository) + return forget_result and prune_result diff --git a/src/restic_compose_backup/commands/snapshots.py b/src/restic_compose_backup/commands/snapshots.py index f53adda..008edf9 100644 --- a/src/restic_compose_backup/commands/snapshots.py +++ b/src/restic_compose_backup/commands/snapshots.py @@ -1,4 +1,5 @@ from .base import BaseCommand +from restic_compose_backup import restic class Command(BaseCommand): @@ -6,4 +7,7 @@ class Command(BaseCommand): name = "snapshots" def run(self): - print("Snapshots!") + """Display restic snapshots""" + stdout, stderr = restic.snapshots(self.config.repository, last=True) + for line in stdout.decode().split('\n'): + print(line) diff --git a/src/restic_compose_backup/commands/start_backup_process.py b/src/restic_compose_backup/commands/start_backup_process.py new file mode 100644 index 0000000..76bca77 --- /dev/null +++ b/src/restic_compose_backup/commands/start_backup_process.py @@ -0,0 +1,89 @@ +import os +from . import BaseCommand +from restic_compose_backup import restic, alerts, utils + +class Command(BaseCommand): + name = "start_backup_process" + + def run(self): + """The actual backup process running inside the spawned container""" + if not utils.is_true(os.environ.get('BACKUP_PROCESS_CONTAINER')): + self.logger.error( + "Cannot run backup process in this container. Use backup command instead. " + "This will spawn a new container with the necessary mounts." + ) + alerts.send( + subject="Cannot run backup process in this container", + body=( + "Cannot run backup process in this container. Use backup command instead. " + "This will spawn a new container with the necessary mounts." + ) + ) + exit(1) + + self.run_command("status") # status(config, containers) + errors = False + containers = self.get_containers() + + # Did we actually get any volumes mounted? + try: + has_volumes = os.stat('/volumes') is not None + except FileNotFoundError: + self.logger.warning("Found no volumes to back up") + has_volumes = False + + # Warn if there is nothing to do + if len(containers.containers_for_backup()) == 0 and not has_volumes: + self.logger.error("No containers for backup found") + exit(1) + + if has_volumes: + try: + self.logger.info('Backing up volumes') + vol_result = restic.backup_files(self.config.repository, source='/volumes') + self.logger.debug('Volume backup exit code: %s', vol_result) + if vol_result != 0: + self.logger.error('Volume backup exited with non-zero code: %s', vol_result) + errors = True + except Exception as ex: + self.logger.error('Exception raised during volume backup') + self.logger.exception(ex) + errors = True + + # back up databases + self.logger.info('Backing up databases') + for container in containers.containers_for_backup(): + if container.database_backup_enabled: + try: + instance = container.instance + self.logger.info('Backing up %s in service %s', instance.container_type, instance.service_name) + result = instance.backup() + self.logger.debug('Exit code: %s', result) + if result != 0: + self.logger.error('Backup command exited with non-zero code: %s', result) + errors = True + except Exception as ex: + self.logger.exception(ex) + errors = True + + if errors: + self.logger.error('Exit code: %s', errors) + exit(1) + + # Only run cleanup if backup was successful + #result = cleanup(config, container) + self.run_command("cleanup") + + self.logger.debug('cleanup exit code: %s', result) + if result != 0: + self.logger.error('cleanup exit code: %s', result) + exit(1) + + # Test the repository for errors + self.logger.info("Checking the repository for errors") + result = restic.check(self.config.repository) + if result != 0: + self.logger.error('Check exit code: %s', result) + exit(1) + + self.logger.info('Backup completed')