Files
backandup/main.py
2023-11-11 23:29:41 +01:00

249 lines
8.5 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"]:
return True
dockerclient = docker.from_env()
containersid = os.popen(f"docker-compose --file {composefilepath} ps --quiet {servicename}").readlines()
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"starting backup")
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)