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/ tests/
docker-compose.yaml docker-compose.yaml
*.env *.env
*.egg-info
__pycache__

3
.gitignore vendored
View File

@ -14,3 +14,6 @@ __pycache__
env env
.venv .venv
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 ADD . /restic-volume-backup
WORKDIR /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 [] ENTRYPOINT []
CMD ["./entrypoint.sh"] 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 #!/bin/sh
# Dump all env vars so we can source them in cron jobs # 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 # start cron in the foreground
crontab crontab crontab crontab

View File

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

View File

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

View File

@ -9,7 +9,7 @@ VOLUME_TYPE_VOLUME = "volume"
class Container: class Container:
"""Represents a docker container""" """Represents a docker container"""
def __init__(self, data): def __init__(self, data: dict):
self._data = data self._data = data
self.id = data['Id'] self.id = data['Id']
self.name = data['Name'].replace('/', '') self.name = data['Name'].replace('/', '')
@ -31,17 +31,17 @@ class Container:
self._exclude = self._parse_pattern(self.get_label('restic-volume-backup.exclude')) self._exclude = self._parse_pattern(self.get_label('restic-volume-backup.exclude'))
@property @property
def image(self): def image(self) -> str:
"""Image name""" """Image name"""
return self.get_config('Image') return self.get_config('Image')
@property @property
def environment(self): def environment(self) -> dict:
"""All configured env vars for the container""" """All configured env vars for the container"""
return self.get_config('Env', default=[]) return self.get_config('Env', default=[])
@property @property
def volumes(self): def volumes(self) -> dict:
""" """
Return volumes for the container in the following format: Return volumes for the container in the following format:
{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},} {'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},}
@ -65,15 +65,15 @@ class Container:
]) ])
@property @property
def volume_backup_enabled(self): def volume_backup_enabled(self) -> bool:
return utils.is_true(self.get_label('restic-volume-backup.volumes')) return utils.is_true(self.get_label('restic-volume-backup.volumes'))
@property @property
def mysql_backup_enabled(self): def mysql_backup_enabled(self) -> bool:
return utils.is_true(self.get_label('restic-volume-backup.mysql')) return utils.is_true(self.get_label('restic-volume-backup.mysql'))
@property @property
def postgresql_backup_enabled(self): def postgresql_backup_enabled(self) -> bool:
return utils.is_true(self.get_label('restic-volume-backup.postgresql')) return utils.is_true(self.get_label('restic-volume-backup.postgresql'))
@property @property
@ -135,6 +135,7 @@ class Container:
return filtered return filtered
def volumes_for_backup(self, source_prefix='/backup', mode='ro'): def volumes_for_backup(self, source_prefix='/backup', mode='ro'):
"""Get volumes configured for backup"""
mounts = self.filter_mounts() mounts = self.filter_mounts()
volumes = {} volumes = {}
for mount in mounts: for mount in mounts:
@ -255,7 +256,7 @@ class RunningContainers:
"""Obtain all containers with backup enabled""" """Obtain all containers with backup enabled"""
return [container for container in self.containers if container.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 = {} mounts = {}
for container in self.containers_for_backup(): for container in self.containers_for_backup():
mounts.update(container.volumes_for_backup(source_prefix=dest_prefix, mode='ro')) 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 from subprocess import Popen, PIPE
logger = logging.getLogger(__name__)
def init_repo(repository): def init_repo(repository):
"""
Attempt to initialize the repository.
Doing this after the repository is initialized
"""
run_command([ run_command([
"restic", "restic",
"-r", "-r",
@ -40,6 +47,7 @@ def check(repository):
def run_command(cmd): def run_command(cmd):
logger.info('cmd: %s', ' '.join(cmd))
child = Popen(cmd, stdout=PIPE, stderr=PIPE) child = Popen(cmd, stdout=PIPE, stderr=PIPE)
stdoutdata, stderrdata = child.communicate() stdoutdata, stderrdata = child.communicate()

View File

@ -19,7 +19,7 @@ def list_containers():
return [c.attrs for c in all_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 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