restic-compose-backup/restic_compose_backup/cli.py

193 lines
6.0 KiB
Python
Raw Normal View History

2019-04-15 16:31:08 +00:00
import argparse
import pprint
2019-11-15 13:23:56 +00:00
import logging
2019-04-15 16:31:08 +00:00
2019-12-03 08:40:02 +00:00
from restic_compose_backup import (
2019-12-04 18:36:32 +00:00
alerts,
2019-12-03 08:40:02 +00:00
backup_runner,
log,
restic,
)
from restic_compose_backup.config import Config
from restic_compose_backup.containers import RunningContainers
2019-04-13 17:04:54 +00:00
2019-11-15 13:23:56 +00:00
logger = logging.getLogger(__name__)
2019-04-13 17:04:54 +00:00
def main():
2019-11-15 13:23:56 +00:00
"""CLI entrypoint"""
2019-04-15 16:31:08 +00:00
args = parse_args()
2019-12-03 23:31:13 +00:00
log.setup(level=args.log_level)
config = Config()
2019-04-13 17:04:54 +00:00
containers = RunningContainers()
2019-04-15 16:31:08 +00:00
if args.action == 'status':
2019-04-18 03:01:02 +00:00
status(config, containers)
2019-04-16 22:08:24 +00:00
2019-12-04 18:36:32 +00:00
elif args.action == 'snapshots':
2019-12-04 02:12:36 +00:00
snapshots(config, containers)
2019-04-18 03:01:02 +00:00
elif args.action == 'backup':
backup(config, containers)
2019-04-16 22:08:24 +00:00
2019-04-18 03:01:02 +00:00
elif args.action == 'start-backup-process':
start_backup_process(config, containers)
2019-04-13 17:04:54 +00:00
2019-12-04 21:03:49 +00:00
elif args.action == 'cleanup':
cleanup(config, containers)
2019-12-04 18:36:32 +00:00
elif args.action == 'alert':
alert(config, containers)
2019-04-17 02:38:15 +00:00
2019-04-18 03:01:02 +00:00
def status(config, containers):
2019-11-12 11:39:49 +00:00
"""Outputs the backup config for the compose setup"""
2019-12-04 18:36:32 +00:00
logger.info("Status for compose project '%s'", containers.project_name)
2019-12-04 00:12:26 +00:00
logger.info("Backup currently running?: %s", containers.backup_process_running)
logger.info("%s Detected Config %s", "-" * 25, "-" * 25)
backup_containers = containers.containers_for_backup()
for container in backup_containers:
2019-11-29 00:32:09 +00:00
logger.info('service: %s', container.service_name)
2019-12-03 02:04:49 +00:00
if container.volume_backup_enabled:
for mount in container.filter_mounts():
2019-11-29 00:32:09 +00:00
logger.info(' - volume: %s', mount.source)
2019-12-03 00:47:58 +00:00
if container.database_backup_enabled:
instance = container.instance
2019-12-03 02:04:49 +00:00
ping = instance.ping()
logger.info(' - %s (is_ready=%s)', instance.container_type, ping == 0)
2019-12-04 00:12:26 +00:00
if ping != 0:
logger.error("Database '%s' in service %s cannot be reached", instance.container_type, container.service_name)
2019-12-02 21:53:00 +00:00
if len(backup_containers) == 0:
2019-12-03 08:40:02 +00:00
logger.info("No containers in the project has 'restic-compose-backup.enabled' label")
2019-04-18 03:01:02 +00:00
2019-12-04 00:12:26 +00:00
logger.info("-" * 67)
2019-04-18 03:01:02 +00:00
def backup(config, containers):
2019-11-15 13:23:56 +00:00
"""Request a backup to start"""
2019-04-18 03:01:02 +00:00
# Make sure we don't spawn multiple backup processes
if containers.backup_process_running:
2019-11-12 11:39:49 +00:00
raise ValueError("Backup process already running")
2019-04-18 03:01:02 +00:00
2019-12-04 00:58:01 +00:00
logger.info("Initializing repository (may fail if already initalized)")
2019-04-18 03:01:02 +00:00
# TODO: Errors when repo already exists
restic.init_repo(config.repository)
# Map all volumes from the backup container into the backup process container
2019-11-15 13:23:56 +00:00
volumes = containers.this_container.volumes
# Map volumes from other containers we are backing up
mounts = containers.generate_backup_mounts('/backup')
volumes.update(mounts)
2019-12-03 06:36:48 +00:00
result = backup_runner.run(
2019-04-18 03:01:02 +00:00
image=containers.this_container.image,
2019-12-03 08:40:02 +00:00
command='restic-compose-backup start-backup-process',
volumes=volumes,
2019-11-12 11:39:49 +00:00
environment=containers.this_container.environment,
2019-12-03 03:22:24 +00:00
source_container_id=containers.this_container.id,
labels={
2019-12-03 08:40:02 +00:00
"restic-compose-backup.backup_process": 'True',
2019-12-04 18:36:32 +00:00
"com.docker.compose.project": containers.project_name,
},
2019-04-18 03:01:02 +00:00
)
2019-12-03 06:36:48 +00:00
logger.info('Backup container exit code: %s', result)
2019-04-13 23:35:14 +00:00
2019-12-04 20:24:10 +00:00
# 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',
)
2019-04-13 23:35:14 +00:00
def start_backup_process(config, containers):
2019-11-15 15:47:40 +00:00
"""The actual backup process running inside the spawned container"""
2019-11-12 11:39:49 +00:00
if (not containers.backup_process_container
or containers.this_container == containers.backup_process_container is False):
2019-11-15 15:47:40 +00:00
logger.error(
"Cannot run backup process in this container. Use backup command instead. "
"This will spawn a new container with the necessary mounts."
)
return
status(config, containers)
errors = False
2019-11-15 13:23:56 +00:00
2019-12-03 03:22:24 +00:00
# Back up volumes
2019-12-03 06:36:48 +00:00
try:
2019-12-04 00:58:01 +00:00
logger.info('Backing up volumes')
2019-12-03 06:36:48 +00:00
vol_result = restic.backup_files(config.repository, source='/backup')
2019-12-04 00:58:01 +00:00
logger.debug('Volume backup exit code: %s', vol_result)
if vol_result != 0:
logger.error('Backup command exited with non-zero code: %s', vol_result)
errors = True
2019-12-03 06:36:48 +00:00
except Exception as ex:
logger.error(ex)
errors = True
2019-11-12 11:39:49 +00:00
2019-12-03 03:22:24 +00:00
# back up databases
for container in containers.containers_for_backup():
if container.database_backup_enabled:
2019-12-03 06:36:48 +00:00
try:
instance = container.instance
logger.info('Backing up %s in service %s', instance.container_type, instance.service_name)
result = instance.backup()
2019-12-04 00:58:01 +00:00
logger.debug('Exit code: %s', result)
if result != 0:
logger.error('Backup command exited with non-zero code: %s', result)
errors = True
2019-12-03 06:36:48 +00:00
except Exception as ex:
logger.error(ex)
errors = True
if errors:
2019-12-04 20:24:10 +00:00
exit(1)
2019-12-03 03:22:24 +00:00
2019-12-04 21:03:49 +00:00
def cleanup(config, containers):
# restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 3
# restic snapshots 5fecf605
logger.info('Running forget/prune')
def snapshots(config, containers):
"""Display restic snapshots"""
stdout, stderr = restic.snapshots(config.repository, last=True)
for line in stdout.decode().split('\n'):
print(line)
2019-12-04 18:36:32 +00:00
def alert(config, containers):
"""Test alerts"""
logger.info("Testing alerts")
alerts.send(
subject="{}: Test Alert".format(containers.project_name),
body="Test message",
)
2019-12-04 18:36:32 +00:00
2019-04-15 16:31:08 +00:00
def parse_args():
2019-12-03 08:40:02 +00:00
parser = argparse.ArgumentParser(prog='restic_compose_backup')
2019-04-15 16:31:08 +00:00
parser.add_argument(
'action',
2019-12-04 21:03:49 +00:00
choices=['status', 'snapshots', 'backup', 'start-backup-process', 'alert', 'cleanup'],
2019-04-15 16:31:08 +00:00
)
2019-12-03 23:31:13 +00:00
parser.add_argument(
'--log-level',
default=None,
choices=list(log.LOG_LEVELS.keys()),
help="Log level"
)
2019-04-15 16:31:08 +00:00
return parser.parse_args()
2019-04-13 17:04:54 +00:00
if __name__ == '__main__':
main()