Files
backandup/main.py
2023-11-12 15:19:41 +01:00

268 lines
9.8 KiB
Python

import os.path
import docker
import restic
import yaml
import configparser
import argparse
bindingIncludePattern = None
bindingExcludePattern = None
remoteHost = None
remoteUser = None
repository = None
passwordFile = None
def arguments_parser():
'''
récupère les arguments de la commande
:return:
'''
parser = argparse.ArgumentParser(
prog='BackandUp',
description='Parse your docker-compose.yml file to automatically shut the containers, backup their volumes '
'and restart them. Dependencies between containers are respected.',
epilog='BackandUp 2023')
parser.add_argument('-c', '--config', action='store', default='./configuration.ini',
help='configuration file for BackandUp. By default it looks for a configuration.ini '
'in its folder. It must contains the configuration for the backup service '
'and can also contains the BackandUp own configuration.')
parser.add_argument('--compose', action='store', default='./docker-compose.yml',
help='compose file to parse. By default it looks for a docker-compose.yml in its folder.')
parser.add_argument('--sshfolder', action='store', default='~/.ssh',
help='ssh folder for sftp connection. By default it uses ~/.ssh.')
parser.add_argument('-x', '--bindexclude', action='store', default=None,
help='Patterns of volume or bind to exclude from the backup.')
parser.add_argument('-i', '--bindinclude', action='store', default=None,
help='Patterns of volume or bind to include from the backup. Using this argument, '
'only matching volumes and binds will be backed up.')
args = parser.parse_args()
print(args)
print(vars(args))
bindcludepattern = None
bindexcludepattern = None
configfile = None
composefile = "/root/compose/core/docker-compose.yml"
sshfolder = "/root/.ssh"
remotehost = None
repository = None
passwordfile = None
def read_config_file(filepath: str = "configuration.ini"):
parser = configparser.ConfigParser()
parser.read(filepath)
global bindingIncludePattern
global bindingExcludePattern
global remoteHost
global repository
global passwordFile
bindingIncludePattern = parser["backandup"]["bindincludepattern"]
bindingExcludePattern = parser["backandup"]["bindexcludepattern"]
remoteHost = parser["restic"]["remotehost"]
remoteUser = parser["restic"]["remoteuser"]
repository = parser["restic"]["repository"]
passwordFile = parser["restic"]["passwordfile"]
def do_backup(dirpath: str, tags: str = None):
restic.repository = f"sftp:{remoteUser}@{remoteHost}:/{repository}"
restic.password_file = passwordFile
if tags is None:
restic.backup(paths=[dirpath])
else:
restic.backup(paths=[dirpath], tags=[])
def backup_ct_binds(ct: docker.models.containers.Container, includepattern: str | list = None,
excludepattern: str | list = None):
if ct.attrs['HostConfig']['Binds'] is None:
print(" Nothing to backup")
return 0
if type(includepattern) is str:
includepattern = [includepattern]
if type(excludepattern) is str:
excludepattern = [excludepattern]
if includepattern is None:
includepattern = ["NOPATTERNTOINCLUDEXXXXXX"]
if excludepattern is None:
excludepattern = ["NOPATTERNTOEXCLUDEXXXXXX"]
for BindMount in ct.attrs['HostConfig']['Binds']:
BindPath = BindMount.split(":")[0]
print(f" {BindPath}")
# Si on matche au moins 1 pattern d'inclusion ET aucun pattern d'exclusion
if any(x in BindPath for x in includepattern) and not any(x in BindPath for x in excludepattern):
do_backup(BindPath)
else:
print(" not a directory to backup")
return 0
def stop_backup_restart_container(ct: docker.models.containers.Container):
print(ct.name)
dorestart = False
if ct.attrs['State']['Status'] == 'running':
print(" stop the container")
dorestart = True
ct.stop()
backup_ct_binds(ct, bindingIncludePattern, bindingExcludePattern)
if dorestart:
print(" start the container")
ct.start()
return 0
def yaml_to_services_list(filepath: str) -> dict|None:
'''
Retourne la liste des services définis dans un fichier docker-compose.yaml sous forme de liste
:param filepath: str
:return: list|None
'''
print(f"sorting services from {filepath}")
serviceslist = dict()
if not os.path.isfile(filepath):
raise OSError(2, "No such file", filepath)
ycontent = None
with open(filepath, 'r') as yfile:
try:
ycontent = yaml.safe_load(yfile)
except yaml.YAMLError as yerror:
print(f"{filepath} doesn't seems to be a yaml file.")
raise yerror
if ycontent is None:
return None
if "services" not in ycontent:
raise Exception("The file doesn't seems to be a correct docker-compose yaml")
# on liste les services et ceux dont ils dépendent
for servicename in ycontent["services"]:
print(f"appending {servicename}")
mustbefore = []
if "depends_on" in ycontent["services"][servicename]:
for dependencie in ycontent["services"][servicename]["depends_on"]:
mustbefore.append(dependencie)
serviceslist[servicename] = {"must_before": mustbefore, "must_after": [], "backupdone": False}
# on reparcourt une seconde fois pour définir de quels services ilssont les dépendances
for service in serviceslist:
if len(serviceslist[service]["must_before"]):
for dependencie in serviceslist[service]["must_before"]:
if service not in serviceslist[dependencie]["must_after"]:
serviceslist[dependencie]["must_after"].append(service)
return serviceslist
def order_services_by_dependencies(servicesdict: dict) -> list:
'''
Génère une liste avec les noms de services pour ordonner leurs coupures par dépendances.
Les containers étant requis seront placés avant ceux les requérant.
:param servicesdict: dict
:return: list
'''
serviceslist = []
for service in servicesdict:
if len(servicesdict[service]["must_after"]) and len(serviceslist):
depindex = len(serviceslist) -1
for dependencieof in servicesdict[service]["must_after"]:
if dependencieof in serviceslist and serviceslist.index(dependencieof) < depindex:
depindex = serviceslist.index(dependencieof)
serviceslist.insert(depindex, service)
else:
serviceslist.append(service)
return serviceslist
def backup_services(servicename: str, servicesdict: dict, composefilepath: str) -> bool:
'''
Pause les containers, lance la backup des dépendances si il y en a, lance sa propre backup,
puis redémarre les containers.
:param servicename: str
:param servicesdict: dict
:param composefilepath: str
:return: bool
'''
if servicesdict[servicename]["backupdone"]:
print(f"backup is already done for {servicename}, ignoring")
return True
dockerclient = docker.from_env()
containersid = os.popen(f"docker-compose --file {composefilepath} ps --quiet {servicename}").read().splitlines()
containerrunning = []
for containerid in containersid:
container = dockerclient.containers.get(containerid)
containerrunning.append(container.status == "running")
print(f"stopping {container.name}")
container.stop()
if len(servicesdict[servicename]["must_before"]):
for dependencie in servicesdict[servicename]["must_before"]:
backup_service(dependencie, servicesdict, composefilepath)
# TODO: faire la sauvegarde
print(f"doing backup of {servicename}")
for containerindex in range(0, len(containersid)):
if containerrunning[containerindex]:
container = dockerclient.containers.get(containersid[containerindex])
print(f"restarting {container.name}")
container.start()
servicesdict[servicename]["backupdone"] = True
return True
def volume_backup(itemtobackup: str) -> bool:
'''
Exécute la sauvegarde du dossier/fichier bind ou volume du container.
:param itemtobackup: str
:return: bool
'''
# il faut lancer l'équivalent de ceci :
# docker run --rm -v $(itemtobackup):/$(itemtobackup) -v $passwordfile:/$passwordfile -v $sshfolder:/root/.ssh \
# --host $host restic/restic restic --password-file /$passwordfile -r $remotehost backup /$(itemtobackup)
volumes = {sshfolder: {'bind': '/root/.ssh', 'mode': 'ro'},
passwordfile: {'bind': '/.resticpass', 'mode': 'ro'}}
ctpath = itemtobackup
if itemtobackup.find("/") == -1 :
ctpath = f"/{itemtobackup}"
dockerclient = docker.from_env()
backuplog = dockerclient.containers.run("restic/restic", command="Lol")
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
dockerhost = docker.from_env()
bindcludepattern = None
bindexcludepattern = None
configfile = None
composefile = "/root/compose/core/docker-compose.yml"
sshfolder = "/root/.ssh"
remotehost = None
repository = None
passwordfile = None
arguments_parser()
# servicesDict = yaml_to_services_list(composefile)
# servicesList = order_services_by_dependencies(servicesDict)
# print("Backup process is starting")
# for servicetobackup in reversed(servicesList):
# print(f" - {servicetobackup}")
# backup_services(servicetobackup, servicesDict, composefile)