250 lines
8.6 KiB
Python
250 lines
8.6 KiB
Python
import os.path
|
|
|
|
import docker
|
|
import restic
|
|
import yaml
|
|
import json
|
|
import configparser
|
|
|
|
|
|
bindingIncludePattern = None
|
|
bindingExcludePattern = None
|
|
remoteHost = None
|
|
remoteUser = 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 remoteUser
|
|
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_service(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
|
|
:return: bool
|
|
'''
|
|
|
|
if servicesdict[servicename]["backupdone"]:
|
|
print("backup is already done, 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 yaml_file_to_container_list(filepath: str) -> list|None:
|
|
'''
|
|
Retourne le contenu d'un fichier yaml sous forme de dictionnaire
|
|
:param filepath: str
|
|
:return: dict|None
|
|
'''
|
|
pass
|
|
|
|
# dependances = []
|
|
# if "depends_on" in ycontent["services"][servicename]:
|
|
# dependances = ycontent["services"][servicename]["depends_on"]
|
|
#
|
|
# bindslist = []
|
|
# volumeslist = []
|
|
# for mount in ycontent["services"][servicename]["volumes"]:
|
|
# localpart = mount.split(":")
|
|
# # TODO: adapter avec pathlib pour gérer Windows
|
|
# # si un /, signifie qu'il s'agit d'un chemin et donc d'un bind mount, pas volume.
|
|
# if localpart.find("/") != -1:
|
|
# bindslist.append(localpart)
|
|
# else:
|
|
# volumeslist.append(localpart)
|
|
#
|
|
# containersid = os.popen(f"docker-compose --file {filepath} ps --quiet {servicename}").readlines()
|
|
# containersnames = os.popen(f"docker-compose --file {filepath} ps {servicename} | tail -n +3").readlines()
|
|
#
|
|
# containerslist = []
|
|
# for i in range(0, len(containersid)):
|
|
# containerslist.append((containersnames[i], containersid[i]))
|
|
#
|
|
# for containerfound in containerslist:
|
|
# container = {"service": servicename,
|
|
# "name": containerfound[0],
|
|
# "id": containerfound[1],
|
|
# "binds": bindslist,
|
|
# "depends_on": dependances}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Press the green button in the gutter to run the script.
|
|
if __name__ == '__main__':
|
|
dockerhost = docker.from_env()
|
|
|
|
bindcludepattern = None
|
|
bindexcludepattern = None
|
|
composefile = "/root/compose/core/docker-compose.yml"
|
|
remotehost = None
|
|
remoteuser = None
|
|
repository = None
|
|
passwordfile = None
|
|
|
|
#read_config_file()
|
|
|
|
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_service(servicetobackup, servicesDict, composefile)
|