Initial
This commit is contained in:
commit
a3cdb38239
|
@ -0,0 +1 @@
|
||||||
|
.venv/
|
|
@ -0,0 +1,2 @@
|
||||||
|
.venv
|
||||||
|
__pycache__
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"python.pythonPath": "${workspaceFolder}/.venv/bin/python"
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
FROM restic/restic
|
||||||
|
|
||||||
|
ADD requirements.txt /
|
||||||
|
RUN apk update && apk add python3 dcron
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
ADD restic-backup /restic-backup
|
||||||
|
|
||||||
|
WORKDIR /restic-backup
|
||||||
|
|
||||||
|
ENTRYPOINT []
|
||||||
|
CMD ["/restic-backup/entrypoint.sh"]
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
# restic-volume-backup
|
||||||
|
|
||||||
|
*WORK IN PROGRESS*
|
||||||
|
|
||||||
|
Backup using using https://restic.net/ for a docker-compose setup.
|
||||||
|
|
||||||
|
Backs up all docker volumes. This includes both host mapped volumes and actual docker volumes.
|
||||||
|
|
||||||
|
* Cron triggers backup
|
||||||
|
* Volumes for all running containers are backed up
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
TODO
|
|
@ -0,0 +1 @@
|
||||||
|
docker-py==1.10.6
|
|
@ -0,0 +1,54 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from containers import RunningContainers
|
||||||
|
import restic
|
||||||
|
|
||||||
|
cmds = ['volumes', 'backup', 'snapshots', 'check']
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
container_name = os.environ['CONTAINER_NAME']
|
||||||
|
password = os.environ['RESTIC_PASSWORD']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls):
|
||||||
|
if not cls.container_name:
|
||||||
|
raise ValueError("CONTAINER env var not set")
|
||||||
|
|
||||||
|
if not cls.password:
|
||||||
|
raise ValueError("PASSWORD env var not set")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
raise ValueError("Missing argument: {}".format(cmds))
|
||||||
|
|
||||||
|
mode = sys.argv[1]
|
||||||
|
if mode not in cmds:
|
||||||
|
raise ValueError("Valid arguments: {}".format(cmds))
|
||||||
|
|
||||||
|
Config.check()
|
||||||
|
|
||||||
|
containers = RunningContainers()
|
||||||
|
|
||||||
|
if mode == 'volumes':
|
||||||
|
volumes = containers.volume_mounts()
|
||||||
|
for vol in volumes:
|
||||||
|
print(vol)
|
||||||
|
print(vol.mount_string())
|
||||||
|
|
||||||
|
binds = containers.bind_mounts()
|
||||||
|
for vol in binds:
|
||||||
|
print(vol.mount_string())
|
||||||
|
|
||||||
|
if mode == 'backup':
|
||||||
|
restic.init_repo(Config.container_name)
|
||||||
|
|
||||||
|
for vol in containers.backup_volumes():
|
||||||
|
restic.backup_volume(Config.container_name, vol)
|
||||||
|
|
||||||
|
if mode == 'snapshots':
|
||||||
|
restic.snapshots(Config.container_name)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,128 @@
|
||||||
|
import os
|
||||||
|
import docker
|
||||||
|
import json
|
||||||
|
|
||||||
|
DOCKER_BASE_URL = os.environ.get('DOCKER_BASE_URL') or "unix://var/run/docker.sock"
|
||||||
|
VOLUME_TYPE_BIND = "bind"
|
||||||
|
VOLUME_TYPE_VOLUME = "volume"
|
||||||
|
|
||||||
|
class Container:
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self.id = data.get('Id')
|
||||||
|
self.names = data.get('Names')
|
||||||
|
self.mounts = [Mount(mnt, container=self) for mnt in data.get('Mounts')]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"Id": self.id,
|
||||||
|
"Mounts": [mnt.data for mnt in self.mounts]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Mount:
|
||||||
|
"""Mount wrapper"""
|
||||||
|
def __init__(self, data, container=None):
|
||||||
|
self.data = data
|
||||||
|
self._container = container
|
||||||
|
|
||||||
|
@property
|
||||||
|
def container(self):
|
||||||
|
return self._container
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return self.data.get('Type')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.data.get('Name')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self):
|
||||||
|
return self.data.get('Source')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination(self):
|
||||||
|
return self.data.get('Destination')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def driver(self):
|
||||||
|
return self.data.get('Driver')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self):
|
||||||
|
return self.data.get('Mode')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rw(self):
|
||||||
|
return self.data.get('RW')
|
||||||
|
|
||||||
|
def mount_string(self):
|
||||||
|
if self.type == VOLUME_TYPE_VOLUME:
|
||||||
|
return "- {}:{}:ro".format(self.name.split('_')[-1], self.destination)
|
||||||
|
elif self.type == VOLUME_TYPE_BIND:
|
||||||
|
return "- {}:{}:ro".format(self.source, self.destination)
|
||||||
|
else:
|
||||||
|
raise ValueError("Uknown volume type: {}".format(self.type))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.data)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""Uniquness for a volume"""
|
||||||
|
if self.type == VOLUME_TYPE_VOLUME:
|
||||||
|
return hash(self.name)
|
||||||
|
elif self.type == VOLUME_TYPE_BIND:
|
||||||
|
return hash(self.source)
|
||||||
|
else:
|
||||||
|
raise ValueError("Uknown volume type: {}".format(self.type))
|
||||||
|
|
||||||
|
|
||||||
|
class RunningContainers:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
client = docker.Client(base_url=DOCKER_BASE_URL)
|
||||||
|
all_containers = client.containers()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
self.containers = []
|
||||||
|
self.backup_container = None
|
||||||
|
|
||||||
|
for entry in all_containers:
|
||||||
|
if entry['Id'].startswith(os.environ['HOSTNAME']):
|
||||||
|
self.backup_container = Container(entry)
|
||||||
|
else:
|
||||||
|
if entry['State'] == "running":
|
||||||
|
self.containers.append(Container(entry))
|
||||||
|
|
||||||
|
if not self.backup_container:
|
||||||
|
raise ValueError("Cannot find metadata for backup container")
|
||||||
|
|
||||||
|
def backup_volumes(self):
|
||||||
|
return self.backup_container.mounts
|
||||||
|
|
||||||
|
def gen_volumes(self, volume_type):
|
||||||
|
"""Generator yielding volumes of a specific type"""
|
||||||
|
for cont in self.containers:
|
||||||
|
for mnt in cont.mounts:
|
||||||
|
if mnt.type == volume_type:
|
||||||
|
yield mnt
|
||||||
|
|
||||||
|
def volume_mounts(self):
|
||||||
|
"""Docker volumes"""
|
||||||
|
return set(mnt for mnt in self.gen_volumes(VOLUME_TYPE_VOLUME))
|
||||||
|
|
||||||
|
def bind_mounts(self):
|
||||||
|
"""Host mapped volumes"""
|
||||||
|
return set(mnt for mnt in self.gen_volumes(VOLUME_TYPE_BIND))
|
||||||
|
|
||||||
|
def print_all(self):
|
||||||
|
print("Backup container:")
|
||||||
|
print(json.dumps(self.backup_container.to_dict(), indent=2))
|
||||||
|
|
||||||
|
print("All containers:")
|
||||||
|
print(json.dumps([cnt.to_dict() for cnt in self.containers], indent=2))
|
|
@ -0,0 +1 @@
|
||||||
|
30 * * * * source /env.sh && python3 restic-backup/backup.py backup >> /backup.log 2>&1
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Dump all env vars so we can source them in cron jobs
|
||||||
|
printenv | sed 's/^\(.*\)$/export \1/g' > /root/env.sh
|
||||||
|
|
||||||
|
# start cron in the foreground
|
||||||
|
crontab crontab
|
||||||
|
crond -f
|
|
@ -0,0 +1,55 @@
|
||||||
|
import os
|
||||||
|
from subprocess import Popen, PIPE, check_call
|
||||||
|
|
||||||
|
|
||||||
|
def repo_path(container_name):
|
||||||
|
return "swift:{}:/".format(container_name)
|
||||||
|
|
||||||
|
|
||||||
|
def init_repo(container_name):
|
||||||
|
run_command([
|
||||||
|
"restic",
|
||||||
|
"-r",
|
||||||
|
repo_path(container_name),
|
||||||
|
"init",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def backup_volume(container_name, volume):
|
||||||
|
run_command([
|
||||||
|
"restic",
|
||||||
|
"-r",
|
||||||
|
repo_path(container_name),
|
||||||
|
"--verbose",
|
||||||
|
"backup",
|
||||||
|
volume.destination,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def snapshots(container_name):
|
||||||
|
run_command([
|
||||||
|
"restic",
|
||||||
|
"-r",
|
||||||
|
repo_path(container_name),
|
||||||
|
"snapshots",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def check(container_name):
|
||||||
|
run_command([
|
||||||
|
"restic",
|
||||||
|
"-r",
|
||||||
|
repo_path(container_name),
|
||||||
|
"check",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd):
|
||||||
|
child = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
|
stdoutdata, stderrdata = child.communicate()
|
||||||
|
|
||||||
|
if stdoutdata:
|
||||||
|
print(stdoutdata.decode())
|
||||||
|
|
||||||
|
if stderrdata:
|
||||||
|
print(stderrdata.decode())
|
Loading…
Reference in New Issue