This commit is contained in:
Einar Forselv 2019-11-15 14:23:56 +01:00
parent f8d32af313
commit f1738147d6
11 changed files with 60 additions and 33 deletions

View File

@ -5,3 +5,5 @@ Dockerfile
tests/
docker-compose.yaml
*.env
*.egg-info
__pycache__

3
.gitignore vendored
View File

@ -14,3 +14,6 @@ __pycache__
env
.venv
venv
# misc
/private/

View File

@ -4,7 +4,8 @@ RUN apk update && apk add python3 dcron mariadb-client postgresql-client
ADD . /restic-volume-backup
WORKDIR /restic-volume-backup
RUN pip3 install -U pip setuptools && python3 setup.py develop && pip3 install .
RUN pip3 install -U pip setuptools
RUN python3 setup.py develop && pip3 install .
ENTRYPOINT []
CMD ["./entrypoint.sh"]

View File

@ -1 +1 @@
30 * * * * source /env.sh && python3 restic-backup/backup.py backup >> /backup.log 2>&1
1 * * * * source /env.sh && rvb backup > /proc/1/fd/1

View File

@ -1,7 +1,7 @@
#!/bin/sh
# Dump all env vars so we can source them in cron jobs
printenv | sed 's/^\(.*\)$/export \1/g' > /root/env.sh
printenv | sed 's/^\(.*\)$/export \1/g' > /env.sh
# start cron in the foreground
crontab crontab

View File

@ -1,13 +1,28 @@
import argparse
import pprint
import logging
from restic_volume_backup.config import Config
from restic_volume_backup.containers import RunningContainers
from restic_volume_backup import backup_runner
from restic_volume_backup import restic
logger = logging.getLogger(__name__)
def setup_logger(level=logging.INFO):
logger.setLevel(level)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(ch)
setup_logger()
def main():
"""CLI entrypoint"""
args = parse_args()
config = Config()
containers = RunningContainers()
@ -18,50 +33,45 @@ def main():
elif args.action == 'backup':
backup(config, containers)
# Separate command to avoid spawning infinite containers :)
elif args.action == 'start-backup-process':
start_backup_process(config, containers)
def status(config, containers):
"""Outputs the backup config for the compose setup"""
print()
print("Backup config for compose project '{}'".format(containers.this_container.project_name))
print("Current service:", containers.this_container.name)
print("Backup process :", containers.backup_process_container.name
if containers.backup_process_container else 'Not Running')
print("Backup running :", containers.backup_process_running)
print()
logger.info("Backup config for compose project '%s'", containers.this_container.project_name)
logger.info("Current service: %s", containers.this_container.name)
logger.info("Backup process: %s", containers.backup_process_container.name
if containers.backup_process_container else 'Not Running')
logger.info("Backup running: %s", containers.backup_process_running)
backup_containers = containers.containers_for_backup()
for container in backup_containers:
if container.backup_enabled:
print('service: {}'.format(container.service_name))
logger.info('service: %s', container.service_name)
for mount in container.filter_mounts():
print(' - {}'.format(mount.source))
logger.info(' - %s', mount.source)
if len(backup_containers) == 0:
print("No containers in the project has 'restic-volume-backup.enabled' label")
print()
logger.info("No containers in the project has 'restic-volume-backup.enabled' label")
def backup(config, containers):
"""Start backup"""
"""Request a backup to start"""
# Make sure we don't spawn multiple backup processes
if containers.backup_process_running:
raise ValueError("Backup process already running")
print("Initializing repository")
logger.info("Initializing repository")
# TODO: Errors when repo already exists
restic.init_repo(config.repository)
print("Starting backup container..")
logger.info("Starting backup container..")
# Map all volumes from the backup container into the backup process container
volumes = containers.this_container.volumes()
volumes = containers.this_container.volumes
# Map volumes from other containers we are backing up
mounts = containers.generate_backup_mounts('/backup')
@ -90,14 +100,17 @@ def start_backup_process(config, containers):
)
return
print("start-backup-process")
logger.info("start-backup-process")
status(config, containers)
# Waste a few seconds faking a backup
print("Fake backup running")
import time
for i in range(5):
time.sleep(1)
print(i)
exit(1)
exit(0)
def parse_args():

View File

@ -2,6 +2,7 @@ import os
class Config:
"""Bag for config values"""
def __init__(self, check=True):
self.repository = os.environ['RESTIC_REPOSITORY']
self.password = os.environ['RESTIC_PASSWORD']

View File

@ -9,7 +9,7 @@ VOLUME_TYPE_VOLUME = "volume"
class Container:
"""Represents a docker container"""
def __init__(self, data):
def __init__(self, data: dict):
self._data = data
self.id = data['Id']
self.name = data['Name'].replace('/', '')
@ -31,17 +31,17 @@ class Container:
self._exclude = self._parse_pattern(self.get_label('restic-volume-backup.exclude'))
@property
def image(self):
def image(self) -> str:
"""Image name"""
return self.get_config('Image')
@property
def environment(self):
def environment(self) -> dict:
"""All configured env vars for the container"""
return self.get_config('Env', default=[])
@property
def volumes(self):
def volumes(self) -> dict:
"""
Return volumes for the container in the following format:
{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},}
@ -65,15 +65,15 @@ class Container:
])
@property
def volume_backup_enabled(self):
def volume_backup_enabled(self) -> bool:
return utils.is_true(self.get_label('restic-volume-backup.volumes'))
@property
def mysql_backup_enabled(self):
def mysql_backup_enabled(self) -> bool:
return utils.is_true(self.get_label('restic-volume-backup.mysql'))
@property
def postgresql_backup_enabled(self):
def postgresql_backup_enabled(self) -> bool:
return utils.is_true(self.get_label('restic-volume-backup.postgresql'))
@property
@ -135,6 +135,7 @@ class Container:
return filtered
def volumes_for_backup(self, source_prefix='/backup', mode='ro'):
"""Get volumes configured for backup"""
mounts = self.filter_mounts()
volumes = {}
for mount in mounts:
@ -255,7 +256,7 @@ class RunningContainers:
"""Obtain all containers with backup enabled"""
return [container for container in self.containers if container.backup_enabled]
def generate_backup_mounts(self, dest_prefix):
def generate_backup_mounts(self, dest_prefix) -> dict:
mounts = {}
for container in self.containers_for_backup():
mounts.update(container.volumes_for_backup(source_prefix=dest_prefix, mode='ro'))

View File

@ -1,7 +1,14 @@
import logging
from subprocess import Popen, PIPE
logger = logging.getLogger(__name__)
def init_repo(repository):
"""
Attempt to initialize the repository.
Doing this after the repository is initialized
"""
run_command([
"restic",
"-r",
@ -40,6 +47,7 @@ def check(repository):
def run_command(cmd):
logger.info('cmd: %s', ' '.join(cmd))
child = Popen(cmd, stdout=PIPE, stderr=PIPE)
stdoutdata, stderrdata = child.communicate()

View File

@ -19,7 +19,7 @@ def list_containers():
return [c.attrs for c in all_containers]
def is_true(self, value):
def is_true(value):
"""
Evaluates the truthfullness of a bool value in container labels
"""

View File

@ -1,2 +0,0 @@
#!/bin/sh
for i in 1 2 3 4 5; do echo 'moo'; sleep 2; done