Cleanup
This commit is contained in:
parent
f8d32af313
commit
f1738147d6
|
@ -5,3 +5,5 @@ Dockerfile
|
||||||
tests/
|
tests/
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
*.env
|
*.env
|
||||||
|
*.egg-info
|
||||||
|
__pycache__
|
||||||
|
|
|
@ -14,3 +14,6 @@ __pycache__
|
||||||
env
|
env
|
||||||
.venv
|
.venv
|
||||||
venv
|
venv
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/private/
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
2
crontab
2
crontab
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue