From 75f44db867689a622115377ad16ea08987ee677f Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 23 Sep 2024 14:05:26 +0200 Subject: [PATCH 01/13] [update] creation du plugin backup restaure. Ce plugin est en charge de recevoir la liste des fichiers que la machine devra tirer en rsync sur le serveur backup. (cherry picked from commit 2598a433e8a29716dda30fc7a762986785917b4f) --- .../plugins_common/plugin_backup_restore.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 pulse_xmpp_agent/plugins_common/plugin_backup_restore.py diff --git a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py new file mode 100644 index 000000000..963ee6375 --- /dev/null +++ b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2016-2023 Siveo +# SPDX-License-Identifier: GPL-3.0-or-later +# file: pulse_xmpp_agent/pluginsrelay/plugin_backup_restore.py +# file: pulse_xmpp_agent/pluginsmachine/plugin_backup_restore.py +import logging +import traceback +import time +from threading import Lock +from threading import Condition +import os +import hashlib + +logger = logging.getLogger() + +plugin = {"VERSION": "1.0", "NAME": "backup_restore", "TYPE": "all"} # fmt: skip + +# Verrou pour la synchronisation de la configuration +# config_lock = Lock() + +config_condition = Condition() + +def action(objectxmpp, action, sessionid, data, message, dataerreur): + """ + Fonction principale du plugin "backup_restore" qui gère l'action de restauration. + + Cette fonction est appelée par le système XMPP avec les paramètres appropriés pour effectuer des + actions spécifiques de restauration. Elle vérifie si la configuration du plugin + a déjà été effectuée et attend que cette configuration soit terminée avant d'exécuter ce a quoi le plugin est cree. + + Args: + objectxmpp (object): L'objet XMPP représentant l'agent en cours d'exécution. + action (str): L'action à réaliser, ici 'plugin_backup_restore'. + sessionid (str): Identifiant unique de la session en cours. + data (dict): Données fournies pour l'exécution du plugin, telles que la liste des répertoires et fichiers. + message (dict): Message contenant les informations sur l'émetteur. + dataerreur (dict): dict message pour repondre aa l'appelant d'une des erreurs lors de l'exécution. + + Returns: + None + """ + logger.debug("###################################################") + logger.debug("call %s from %s" % (plugin, message["from"])) + logger.debug("###################################################") + compteurcallplugin = getattr(objectxmpp, f"num_call{action}", None) + if compteurcallplugin is None: + logger.error(f"num_call attribute for action {action} not found on objectxmpp") + return + + with config_condition: + # Si c'est le premier appel du plugin, on configure + if compteurcallplugin == 0: + if not hasattr(objectxmpp, 'configuration_done') or not objectxmpp.configuration_done: + logger.debug("Starting initial configuration") + read_conf_plugin_backup_restore(objectxmpp) + objectxmpp.configuration_done = True + config_condition.notify_all() + logger.debug("Configuration done") + else: + # Si la configuration n'est pas encore faite, attendre sa fin + while not hasattr(objectxmpp, 'configuration_done') or not objectxmpp.configuration_done: + config_condition.wait() + + # Exécution du corps du plugin une fois la configuration terminée + if 'directorylist' in data and data['directorylist']: + for directory in data['directorylist']: + logger.debug(f"Processing directory: {directory}") + +def read_conf_plugin_backup_restore(objectxmpp): + """ + Fonction pour lire et charger la configuration spécifique au plugin "backup_restore". + + Gère les exceptions liées à l'accès aux fichiers de configuration et au parsing. + + Args: + objectxmpp (object): L'objet XMPP représentant l'agent en cours d'exécution. + + Returns: + None + """ + try: + if objectxmpp.config.agenttype in ["machine"]: + configfilename = os.path.join(directoryconffile(), "backup_restore.ini") + elif objectxmpp.config.agenttype in ["relayserver"]: + configfilename = os.path.join(directoryconffile(), "backup_restore.ini") + else: + logger.error( + "The %s agenttype is not supported in this function, it must be machine or relayserver." + % objectxmpp.config.agenttype + ) + return + + if os.path.isfile(configfilename): + Config = configparser.ConfigParser() + Config.read(configfilename) + if os.path.isfile(configfilename + ".local"): + Config.read(configfilename + ".local") + logger.debug(f"Loaded configuration from {configfilename}") + else: + logger.warning(f"Configuration file {configfilename} not found.") + except Exception as e: + logger.error(f"Error reading configuration: {str(e)}") + traceback.print_exc() + +class FileHasher: + def __init__(self, file_path): + self.file_path = file_path + + def calculate_hash(self): + hash_md5 = hashlib.md5() + with open(self.file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + From 9d82d60e8420cdd88fd7e0e46194ed61241e7241 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 23 Sep 2024 14:05:26 +0200 Subject: [PATCH 02/13] [agent machine] plugin qui permet de recuperer des fichiers demander au serveur de backup. la machine devra tirer en scp sur le serveur backup. - restriction : seulement pour les machines dans le lan du serveur urbackup - si machine n'est pas dans le lan, alors le plugin quitte sans rien faire. il met juste 1 erreur (cherry picked from commit eb9b3739d45e2542f640c9ce144141c960932ff8) --- pulse_xmpp_agent/config/backup_restore.ini | 20 + .../plugins_common/plugin_backup_restore.py | 481 ++++++++++++++++-- 2 files changed, 465 insertions(+), 36 deletions(-) create mode 100644 pulse_xmpp_agent/config/backup_restore.ini diff --git a/pulse_xmpp_agent/config/backup_restore.ini b/pulse_xmpp_agent/config/backup_restore.ini new file mode 100644 index 000000000..1aed78fb1 --- /dev/null +++ b/pulse_xmpp_agent/config/backup_restore.ini @@ -0,0 +1,20 @@ +[backup_restore] +remote_user = urbackup + +# La clé privée est normalement celle de reverse_ssh. +# La clé publique id_rsa.pub de reverse_ssh doit être inscrite dans /var/urbackup/authorized_keys afin de permettre l'authentification de la machine locale vers le serveur de sauvegarde. +# Sur la machine locale, +# on donne un autre nom à la même clé que id_rsa de reverse_ssh, par exemple pulseuser_backup_id_rsa. +# pulseuser_backup_id_rsa est réinstallée à chaque demande de transfert de sauvegarde en locale. +# Elle est en fait une copie de la clé privée id_rsa de reverse_ssh. +# Cela permet de diminuer le risque de corruption de la copie de la clé en cas d'erreur de plugin. +private_name_key = pulseuser_backup_id_rsa + + +username = pulseuser +#on utilise le profil de pulseuser + +# restore_to_backup_location 'True' signifie que les fichiers récupérés seront placés dans le répertoire de sauvegarde : +# C:\Program Files\Medulla\var\backup_files pour Windows +# /var/lib/pulse2/backup_files pour Darwin ou Linux +restore_to_backup_location = True diff --git a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py index 963ee6375..6d4fc4f02 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py +++ b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py @@ -1,18 +1,40 @@ # -*- coding: utf-8 -*- # SPDX-FileCopyrightText: 2016-2023 Siveo # SPDX-License-Identifier: GPL-3.0-or-later -# file: pulse_xmpp_agent/pluginsrelay/plugin_backup_restore.py + +# file: pulse_xmpp_agent/[pluginsrelay | pluginsmachine]/plugin_backup_restore.py # file: pulse_xmpp_agent/pluginsmachine/plugin_backup_restore.py + import logging import traceback -import time -from threading import Lock from threading import Condition import os +import sys + +import psutil +import socket +import ipaddress + import hashlib +from lib.agentconffile import ( + directoryconffile, +) +# from lib.networkinfo import find_common_addresses, get_CIDR_ipv4_addresses +import subprocess +from lib.utils import pulseuser_useraccount_mustexist, pulseuser_profile_mustexist, create_idrsa_on_client, getHomedrive, simplecommand +import platform +import configparser +import json +from pathlib import PurePosixPath, Path + +if sys.platform.startswith("linux") or sys.platform.startswith("darwin"): + import pwd + logger = logging.getLogger() +remote_host = None + plugin = {"VERSION": "1.0", "NAME": "backup_restore", "TYPE": "all"} # fmt: skip # Verrou pour la synchronisation de la configuration @@ -28,20 +50,21 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): actions spécifiques de restauration. Elle vérifie si la configuration du plugin a déjà été effectuée et attend que cette configuration soit terminée avant d'exécuter ce a quoi le plugin est cree. - Args: - objectxmpp (object): L'objet XMPP représentant l'agent en cours d'exécution. - action (str): L'action à réaliser, ici 'plugin_backup_restore'. - sessionid (str): Identifiant unique de la session en cours. - data (dict): Données fournies pour l'exécution du plugin, telles que la liste des répertoires et fichiers. - message (dict): Message contenant les informations sur l'émetteur. - dataerreur (dict): dict message pour repondre aa l'appelant d'une des erreurs lors de l'exécution. - - Returns: - None + :param objectxmpp: The XMPP object. + :param action: The action to be performed. + :param sessionid: The session ID. + :param data: The data containing IP list, file list, directory list, etc. + :param message: The message object. + :param dataerreur: Error data. """ logger.debug("###################################################") logger.debug("call %s from %s" % (plugin, message["from"])) logger.debug("###################################################") + # logger.debug("MESSAGE") + logger.debug("%s" % json.dumps(data, indent=4)) + # strjidagent = str(objectxmpp.boundjid.bare) + + compteurcallplugin = getattr(objectxmpp, f"num_call{action}", None) if compteurcallplugin is None: logger.error(f"num_call attribute for action {action} not found on objectxmpp") @@ -49,6 +72,8 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): with config_condition: # Si c'est le premier appel du plugin, on configure + # tant que pas configurer les concurant attentent pour profiter de la conf aussi. + # et pas provoquer des erreurs par manque de configuration if compteurcallplugin == 0: if not hasattr(objectxmpp, 'configuration_done') or not objectxmpp.configuration_done: logger.debug("Starting initial configuration") @@ -61,24 +86,69 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): while not hasattr(objectxmpp, 'configuration_done') or not objectxmpp.configuration_done: config_condition.wait() - # Exécution du corps du plugin une fois la configuration terminée - if 'directorylist' in data and data['directorylist']: - for directory in data['directorylist']: - logger.debug(f"Processing directory: {directory}") + # Execution du corps du plugin une fois la configuration terminee + # logger.debug("corp plugin") -def read_conf_plugin_backup_restore(objectxmpp): - """ - Fonction pour lire et charger la configuration spécifique au plugin "backup_restore". + try: + server_ip_networks = data["ip_list"] + client_ip_networks = get_ip_and_netmask_linux() + remote_host = find_best_server_address(server_ip_networks, client_ip_networks) + if not remote_host: + logger.error("le serveur urbackup n'est pas dans le reseau de la machine.") + logger.error("SERVEUR IP : %s", server_ip_networks) + logger.error("CLIENT IP : %s", client_ip_networks ) + return + except Exception as e: + logger.error("termine plugin %s" % (traceback.format_exc())) + return + logger.debug("IP serveur CONNECT. %s" % remote_host) + + try: + # Make sure user account and profile exists + result, message = pulseuser_useraccount_mustexist( + objectxmpp.username + ) + if result is False: + logger.error(f"{message}") + return + logger.debug(f"{message}") + result, message = pulseuser_profile_mustexist(objectxmpp.username) + if result is False: + logger.error(f"{message}") + return + logger.debug(f"{message}") + result, message = create_idrsa_on_client( + objectxmpp.username, + data['key_private'] + ) + if result is False: + logger.error(f"{message}") + return + logger.debug(f"{message}") + except Exception as e: + logger.error(f"{e}") + return - Gère les exceptions liées à l'accès aux fichiers de configuration et au parsing. + id_rsa_path = os.path.join(getHomedrive(), ".ssh", objectxmpp.private_name_key) - Args: - objectxmpp (object): L'objet XMPP représentant l'agent en cours d'exécution. + copy_files_and_directories(objectxmpp.remote_user, + remote_host, + data['filelist'], + data['directorylist'], + data['base_path'], + private_key_path=id_rsa_path, + restore_to_backup_location = objectxmpp.restore_to_backup_location) - Returns: - None +def read_conf_plugin_backup_restore(objectxmpp): + """ + Reads and loads the configuration for the backup_restore plugin. + If the configuration file `backup_restore.ini` does not exist, it is created with default values. + + :param objectxmpp: The XMPP object to which the configuration values will be assigned. + :type objectxmpp: object """ try: + # Vérification de l'agenttype pour déterminer le fichier de configuration if objectxmpp.config.agenttype in ["machine"]: configfilename = os.path.join(directoryconffile(), "backup_restore.ini") elif objectxmpp.config.agenttype in ["relayserver"]: @@ -90,26 +160,365 @@ def read_conf_plugin_backup_restore(objectxmpp): ) return - if os.path.isfile(configfilename): - Config = configparser.ConfigParser() - Config.read(configfilename) - if os.path.isfile(configfilename + ".local"): - Config.read(configfilename + ".local") - logger.debug(f"Loaded configuration from {configfilename}") - else: - logger.warning(f"Configuration file {configfilename} not found.") + Config = configparser.ConfigParser() + + # Si le fichier de configuration n'existe pas, le créer avec un contenu par défaut + if not os.path.isfile(configfilename): + logger.warning(f"Configuration file {configfilename} not found. Creating it...") + + config_content = """[backup_restore] +remote_user = urbackup + +# La clé privée est normalement celle de reverse_ssh. +# La clé publique id_rsa.pub de reverse_ssh doit être inscrite dans /var/urbackup/authorized_keys afin de permettre l'authentification de la machine locale vers le serveur de sauvegarde. +# Sur la machine locale, +# on donne un autre nom à la même clé que id_rsa de reverse_ssh, par exemple pulseuser_backup_id_rsa. +# pulseuser_backup_id_rsa est réinstallée à chaque demande de transfert de sauvegarde en locale. +# Elle est en fait une copie de la clé privée id_rsa de reverse_ssh. +# Cela permet de diminuer le risque de corruption de la copie de la clé en cas d'erreur de plugin. + +private_name_key = pulseuser_backup_id_rsa + +username = pulseuser +# on utilise le profil de pulseuser pour inscrire la clef privet + +# restore_to_backup_location 'True' signifie que les fichiers récupérés seront placés dans le répertoire de sauvegarde : +# C:\\Program Files\\Medulla\\var\\backup_files pour Windows +# /var/lib/pulse2/backup_files pour Darwin ou Linux +restore_to_backup_location = True +""" + # Écrire le fichier de configuration avec le contenu par défaut + with open(configfilename, 'w') as configfile: + configfile.write(config_content) + + logger.info(f"Configuration file {configfilename} has been created with default content.") + + # Charger les valeurs du fichier de configuration + Config.read(configfilename) + if os.path.isfile(configfilename + ".local"): + Config.read(configfilename + ".local") + logger.debug(f"Loaded configuration from {configfilename}") + + # Récupérer les valeurs de configuration ou les valeurs par défaut + objectxmpp.remote_user = Config.get('backup_restore', 'remote_user', fallback='urbackup') + objectxmpp.private_name_key = Config.get('backup_restore', 'private_name_key', fallback='pulseuser_backup_id_rsa') + objectxmpp.username = Config.get('backup_restore', 'username', fallback='pulseuser') + objectxmpp.restore_to_backup_location = Config.getboolean('backup_restore', 'restore_to_backup_location', fallback=True) + + logger.debug(f"Configuration values set: remote_user={objectxmpp.remote_user}, " + f"private_name_key={objectxmpp.private_name_key}, " + f"username={objectxmpp.username}, " + f"restore_to_backup_location={objectxmpp.restore_to_backup_location}") + except Exception as e: - logger.error(f"Error reading configuration: {str(e)}") - traceback.print_exc() + logger.error(f"Error reading or creating configuration: {str(e)}") + logger.error("\n%s" % (traceback.format_exc())) + + +def copy_files_and_directories(remote_user, + remote_host, + files, + directories, + base_path, + private_key_path=None, + restore_to_backup_location=True): + """ + Copies files and directories from a remote host to the local machine. + + :param remote_user: The remote user for SSH. + :param remote_host: The remote host. + :param files: List of files to copy. + :param directories: List of directories to copy. + :param base_path: The base path for the backup. + :param private_key_path: The path to the private key for SSH. + :param restore_to_backup_location: Whether to restore files to the backup location. + """ + if private_key_path is None: + private_key_path = os.path.join(getHomedrive(), ".ssh", "id_rsa") + + if restore_to_backup_location: + # on etablie la base ou vont etre mis les backups recuperer. + # rappel que ce plugin et pour recuperer des fichiers sur 1 autre machine. + # il y a trop de risque de placer les fichier directement a l'emplacement de depart. + # mais la hierachi est respecter depuis cette basse backup_path + backup_path = get_backup_path() + + # Determiner le systeme d'exploitation + system = platform.system() + # Chemins des executables + if system == 'Windows': + rsync_path = r'C:\Windows\SysWOW64\rsync.exe' + ssh_path = r'C:\Progra~1\OpenSSH\ssh.exe' + scp_path = r'C:\Progra~1\OpenSSH\scp.exe' + else: # Linux + rsync_path = 'rsync' + ssh_path = 'ssh' + scp_path = 'scp' + # Verifier si rsync est disponible que pour windows + + # cette ligne sera a decomenter + rsync_available = False + # rsync_available = os.path.exists(rsync_path) if system == 'Windows' else True + + + # quand on poura ectire la commande rsync en windows. avec clef ssh + # C:\Windows\SysWOW64\rsync.exe -L -z --rsync-path=rsync + # -e "C:/Program Files/OpenSSH/ssh.exe + # -o IdentityFile=c:/users/pulseuser/.ssh/pulseuser_backup_id_rsa + # -o UserKnownHostsFile=/dev/null + # -o StrictHostKeyChecking=no + # -o Batchmode=yes + # -o PasswordAuthentication=no + # -o ServerAliveInterval=10 + # -o CheckHostIP=no + # -o LogLevel=ERROR + # -o ConnectTimeout=10" + # -av --chmod=777 + # urbackup@10.10.0.100:/media/BACKUP/urbackup/amu-win-6/240916-1727/Users/desktop.ini + # "C:/Program Files/Medulla/var/backup_files/C_0/Users/desktop.ini + + cmd = ( + """%s -r -p -C "-o IdentityFile=%s" "-o UserKnownHostsFile=/dev/null" "-o StrictHostKeyChecking=no" "-o Batchmode=yes" "-o PasswordAuthentication=no" "-o ServerAliveInterval=10" "-o CheckHostIP=no" "-o LogLevel=ERROR" "-o ConnectTimeout=10" """ + % ( + scp_path, + private_key_path, + ) + ) + if rsync_available: + cmd = ( + """%s -L -z --rsync-path=rsync -e "%s -o IdentityFile=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o Batchmode=yes -o PasswordAuthentication=no -o ServerAliveInterval=10 -o CheckHostIP=no -o LogLevel=ERROR -o ConnectTimeout=10" -av --chmod=777 """ + % ( + rsync_path, + ssh_path, + private_key_path, + ) + ) + + # Copier les fichiers + for src, dest in files: + # le serveur urbackup est sur linux on determine depuis 1 machine windows le chemin dans linux + # Utiliser PurePosixPath pour construire un chemin specifique e Linux + logger.debug(f"base_path: {base_path}") + if restore_to_backup_location: + modified_path_dest = modify_backup_path(backup_path, dest) + else: + modified_path_dest = dest + linux_path = PurePosixPath(base_path) / src + # Convertir le chemin en une chaine de caracteres + srcfile = str(linux_path) + logger.debug(f"srcfile: {srcfile}") + remotesrc = """%s@%s:%s """ % (remote_user, remote_host, srcfile) + # remotesrc = "urbackup@10.10.0.100:/media/BACKUP/urbackup/amu-win-6/240916-1727/Users/desktop.ini" + create_directories(get_directory_path(modified_path_dest)) + # dest = ' C:/Users/blablabla.txt' + command = cmd + remotesrc + '"'+modified_path_dest+'"' + + # command = cmd + remotesrc + modified_path_dest + logger.debug(f"Command: {command}") + obj = simplecommand(command) + logger.warning(f"Transfer file : {obj['code']} {obj['result']}") + + if obj['code'] == 1: + logger.warning(f"Transfer mais link non creer : { obj['code']}") + logger.warning(f"nb link {len(obj['result'])}") + logger.warning("liste link") + for linelogresult in obj['result']: + logger.warning("{ linelogresult.strip()}") + elif obj['code'] != 0: + logger.error(f"Transfer error: { obj['code']}") + logger.error(f"Transfer error: sur nb file : {len(obj['result'])}") + + else: + if len(obj['result']) > 0: + for linelogresult in obj['result']: + logger.debug("Transfer successful") + logger.debug("{ linelogresult.strip()}") + else: + logger.warning("Transfer successful") + + # Copier les repertoires + for src, dest in directories: + # le serveur urbackup est sur linux on determine depuis 1 machine windows le chemin dans linux + # Utiliser PurePosixPath pour construire un chemin specifique e Linux + logger.debug(f"src dest: {src} {dest}") + logger.debug(f"base_path: {base_path}") + if restore_to_backup_location: + modified_path_dest = modify_backup_path(backup_path, dest) + else: + modified_path_dest = dest + logger.debug(f"dest: {modified_path_dest}") + linux_path = PurePosixPath(base_path) / src + # Convertir le chemin en une chaene de caracteres + srcdirectory = str(linux_path) + logger.debug(f"srcdirectory: {srcdirectory}") + remotesrc = """%s@%s:"%s" """ % (remote_user, remote_host, srcdirectory) + # create_directories(modified_path_dest) + create_directories(get_directory_path(modified_path_dest)) + command = cmd + remotesrc + '"'+modified_path_dest+'"' + logger.debug(f"Command: {command}") + obj = simplecommand(command) + if rsync_available: + if obj['code'] != 0: + logger.error(f"Transfer error code error: { obj['code']}") + logger.error(f"error message : {obj['result']}") + else: + logger.debug("Transfer successful") + else: + if obj['code'] == 1: + logger.warning(f"Transfer mais link non creer : { obj['code']}") + logger.warning(f"nb link {len(obj['result'])}") + logger.warning("liste link from serveur %s" % base_path) + if restore_to_backup_location: + logger.warning("to %s" % backup_path) + backup_path_slach_linux = str(backup_path).replace("\\", "/") + logger.warning("to %s" % backup_path_slach_linux) + stringmessage = scp_path + ": Download of file " + base_path + # Remplacement de stringmessage dans chaque élément de la liste + for linelogresult in obj['result']: + linestr = linelogresult.replace(stringmessage, "").strip() + # if restore_to_backup_location: + linestr1 = linestr.replace( backup_path_slach_linux, "") + linestr1 = linestr1.replace(str(scp_path), "") + logger.warning(f"{linestr1}") + elif obj['code'] != 0: + logger.error(f"Transfer error: { obj['code']}") + logger.error(f"Transfer error: sur nb file : {len(obj['result'])}") + else: + if len(obj['result']) > 0: + for linelogresult in obj['result']: + logger.debug("Transfer successful") + logger.debug(f"{linelogresult.strip()}") + else: + logger.warning("Transfer successful") + +def get_ip_and_netmask_linux(exclude_local=True): + """ + Retrieves the IP addresses and netmasks of the local machine. + + :param exclude_local: Whether to exclude local addresses. + :type exclude_local: bool + :return: List of tuples containing IP addresses and netmasks. + :rtype: list of tuples + """ + ip_netmask_list = [] + # Parcours de toutes les interfaces reseau + for interface, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == socket.AF_INET: # Ne prend que les adresses IPv4 + ip = addr.address + netmask = addr.netmask + + # Exclure les adresses locales si exclude_local est True + if exclude_local: + ip_obj = ipaddress.ip_address(ip) + if ip_obj.is_loopback or ip_obj.is_link_local: + continue # On passe e l'iteration suivante si c'est une adresse locale + ip_netmask_list.append((ip, netmask)) + return ip_netmask_list + +def find_best_server_address(server_addresses, client_addresses): + """ + Finds the best server address that is in the same network as the client. + + :param server_addresses: List of server IP addresses and netmasks. + :type server_addresses: list of tuples + :param client_addresses: List of client IP addresses and netmasks. + :type client_addresses: list of tuples + :return: The best server IP address or None if no common network is found. + :rtype: str or None + """ + for client_ip, client_netmask in client_addresses: + client_network = ipaddress.IPv4Network(f"{client_ip}/{client_netmask}", strict=False) + for server_ip, server_netmask in server_addresses: + server_network = ipaddress.IPv4Network(f"{server_ip}/{server_netmask}", strict=False) + # Verifie si le client et le serveur sont dans le meme reseau + if client_network.overlaps(server_network): + return server_ip # Retourne l'adresse du serveur dans le meme reseau que le client + return None # Aucun reseau commun trouve + +def get_backup_path(): + """ + Determines the backup path based on the operating system. + + :return: The backup path. + :rtype: Path + """ + # Déterminer le système d'exploitation + system = platform.system() + + # Définir le chemin du répertoire de sauvegarde en fonction du système d'exploitation + if system == "Windows": + backup_path = Path("C:/Program Files/Medulla/var/backup_files") + elif system in ["Linux", "Darwin"]: + backup_path = Path("/var/lib/pulse2/backup_files") + else: + raise OSError(f"Système d'exploitation non pris en charge: {system}") + + # Créer le répertoire s'il n'existe pas + backup_path.mkdir(parents=True, exist_ok=True) + + return backup_path + +def modify_backup_path(base_dir, restore_path): + """ + Modifies the backup path to include the drive letter. + + :param base_dir: The base directory for the backup. + :param restore_path: The restore path. + :return: The modified backup path. + :rtype: str + """ + # Convertir le chemin de restauration en chemin absolu + drive_letter = restore_path.replace(':', '_0') + modified_path = os.path.join(base_dir, drive_letter) + modified_path = modified_path.replace('\\', '/') + return modified_path + +def create_directories(path): + """ + Creates the necessary directories. + + :param path: The path to create directories for. + :type path: str + """ + # Créer les répertoires nécessaires + os.makedirs(path, exist_ok=True) + +def get_directory_path(file_path): + """ + Gets the directory path from a file path. + + :param file_path: The file path. + :type file_path: str + :return: The directory path. + :rtype: str + """ + # Récupérer le chemin du répertoire sans le nom du fichier + directory_path = os.path.dirname(file_path) + return directory_path class FileHasher: + """ + A class to calculate the MD5 hash of a file. + """ def __init__(self, file_path): + """ + Initializes the FileHasher with the file path. + + :param file_path: The path to the file. + :type file_path: str + """ self.file_path = file_path def calculate_hash(self): + """ + Calculates the MD5 hash of the file. + + :return: The MD5 hash of the file. + :rtype: str + """ hash_md5 = hashlib.md5() with open(self.file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() - From 9e586d1d05526e4b580952b63eea280816ad820b Mon Sep 17 00:00:00 2001 From: jfkneib Date: Thu, 10 Oct 2024 09:05:32 +0200 Subject: [PATCH 03/13] [agent] fonction pour retrouver les machine locals. (cherry picked from commit ed854209f4df11bce67b1cff4a247b8e9186a2d2) --- pulse_xmpp_agent/lib/networkinfo.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/pulse_xmpp_agent/lib/networkinfo.py b/pulse_xmpp_agent/lib/networkinfo.py index 4f3d5453a..2e9458b52 100644 --- a/pulse_xmpp_agent/lib/networkinfo.py +++ b/pulse_xmpp_agent/lib/networkinfo.py @@ -3,6 +3,8 @@ # SPDX-FileCopyrightText: 2016-2023 Siveo # SPDX-License-Identifier: GPL-3.0-or-later +# FILE pulse_xmpp_agent/lib/networkinfo.py + import netifaces import subprocess @@ -26,6 +28,93 @@ logger = logging.getLogger() +def find_common_addresses(list1, list2): + """ + Trouve les adresses IP communes entre deux listes de réseaux CIDR. + + Cette fonction prend deux listes de chaînes de caractères représentant des réseaux CIDR, + les convertit en objets IPv4Network, et recherche les adresses IP communes dans les deux listes. + + Args: + list1 (list of str): Une liste de chaînes de caractères représentant des réseaux CIDR. + list2 (list of str): Une autre liste de chaînes de caractères représentant des réseaux CIDR. + + Returns: + list of str: Une liste d'adresses IP communes entre les deux listes de réseaux CIDR. + """ + # Convertir les chaînes de CIDR en objets IPv4Network + cidr_list1 = [ipaddress.ip_network(cidr) for cidr in list1] + cidr_list2 = [ipaddress.ip_network(cidr) for cidr in list2] + + # Chercher des adresses IP communes dans les deux listes + common_addresses = [] + for net1 in cidr_list1: + for net2 in cidr_list2: + # Vérifier si les réseaux se chevauchent + if net1.overlaps(net2): + # Ajouter l'IP commune à la liste + common_addresses.append(str(net1.network_address)) + + return common_addresses + +def get_CIDR_ipv4_addresses(exclude_localhost=True): + """ + Récupère les adresses IPv4 au format CIDR pour chaque interface réseau de la machine. + + Cette fonction utilise des commandes spécifiques au système d'exploitation pour obtenir les informations + sur les interfaces réseau et les adresses IPv4. Elle retourne une liste d'adresses IPv4 au format CIDR. + + Args: + exclude_localhost (bool): Si True, exclut les interfaces locales (localhost ou 127.0.0.1). + + Returns: + list: Une liste d'adresses IPv4 au format CIDR (par exemple, '192.168.1.0/24'). + """ + ipv4_addresses = [] + # Vérifier le système d'exploitation + system = platform.system() + if system == "Windows": + # Commande pour obtenir les interfaces réseau sous Windows + output = subprocess.check_output("ipconfig", shell=True).decode() + ip, mask = None, None + for line in output.splitlines(): + if "IPv4 Address" in line or "IPv4" in line: + ip = line.split(":")[-1].strip() + if "Subnet Mask" in line: + mask = line.split(":")[-1].strip() + if ip and mask: + if exclude_localhost and ip == "127.0.0.1": + continue + cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + ipv4_addresses.append(cidr) + ip, mask = None, None + elif system == "Linux": + # Commande pour obtenir les interfaces réseau sous Linux + output = subprocess.check_output("ip addr show", shell=True).decode() + for line in output.splitlines(): + line = line.strip() + if line.startswith("inet "): + parts = line.split() + ip_mask = parts[1] # Exemple : 192.168.1.100/24 + if exclude_localhost and ip_mask.startswith("127.0.0.1/"): + continue + ipv4_addresses.append(ip_mask) + elif system == "Darwin": # macOS est identifié par 'Darwin' + # Commande pour obtenir les interfaces réseau sous macOS + output = subprocess.check_output("ifconfig", shell=True).decode() + current_ip = None + for line in output.splitlines(): + line = line.strip() + if line.startswith("inet ") and "127.0.0.1" not in line: # Éviter localhost + parts = line.split() + ip = parts[1] # L'adresse IP est le second champ + mask = parts[3] # Le masque est généralement le quatrième champ + if exclude_localhost and ip == "127.0.0.1": + continue + cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + ipv4_addresses.append(cidr) + return ipv4_addresses + class networkagentinfo: def __init__(self, sessionid, action="resultgetinfo", param=[]): self.sessionid = sessionid From c7ac80853106458e9ca442bfb8f7ab26ee77cd90 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Thu, 10 Oct 2024 09:10:08 +0200 Subject: [PATCH 04/13] [substitut] fonction pour permettre de trouver les machine dans le reseau local (cherry picked from commit c4c932c7c4e09a9991ff462d7f3cbf6f27debe2f) --- .../lib/networkinfo.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/pulse_xmpp_master_substitute/lib/networkinfo.py b/pulse_xmpp_master_substitute/lib/networkinfo.py index 7cffd15f6..7828d76ed 100644 --- a/pulse_xmpp_master_substitute/lib/networkinfo.py +++ b/pulse_xmpp_master_substitute/lib/networkinfo.py @@ -3,6 +3,8 @@ # SPDX-FileCopyrightText: 2016-2023 Siveo # SPDX-License-Identifier: GPL-3.0-or-later +# FILE : pulse_xmpp_master_substitute/lib/networkinfo.py + import netifaces import subprocess import sys @@ -17,11 +19,101 @@ from lib.utils import simplecommand, powerschellscriptps1 from . import utils +import ipaddress + if sys.platform.startswith("win"): import wmi import pythoncom + +def find_common_addresses(list1, list2): + """ + Trouve les adresses IP communes entre deux listes de réseaux CIDR. + + Cette fonction prend deux listes de chaînes de caractères représentant des réseaux CIDR, + les convertit en objets IPv4Network, et recherche les adresses IP communes dans les deux listes. + + Args: + list1 (list of str): Une liste de chaînes de caractères représentant des réseaux CIDR. + list2 (list of str): Une autre liste de chaînes de caractères représentant des réseaux CIDR. + + Returns: + list of str: Une liste d'adresses IP communes entre les deux listes de réseaux CIDR. + """ + # Convertir les chaînes de CIDR en objets IPv4Network + cidr_list1 = [ipaddress.ip_network(cidr) for cidr in list1] + cidr_list2 = [ipaddress.ip_network(cidr) for cidr in list2] + + # Chercher des adresses IP communes dans les deux listes + common_addresses = [] + for net1 in cidr_list1: + for net2 in cidr_list2: + # Vérifier si les réseaux se chevauchent + if net1.overlaps(net2): + # Ajouter l'IP commune à la liste + common_addresses.append(str(net1.network_address)) + + return common_addresses + +def get_CIDR_ipv4_addresses(exclude_localhost=True): + """ + Récupère les adresses IPv4 au format CIDR pour chaque interface réseau de la machine. + + Cette fonction utilise des commandes spécifiques au système d'exploitation pour obtenir les informations + sur les interfaces réseau et les adresses IPv4. Elle retourne une liste d'adresses IPv4 au format CIDR. + + Args: + exclude_localhost (bool): Si True, exclut les interfaces locales (localhost ou 127.0.0.1). + + Returns: + list: Une liste d'adresses IPv4 au format CIDR (par exemple, '192.168.1.0/24'). + """ + ipv4_addresses = [] + # Vérifier le système d'exploitation + system = platform.system() + if system == "Windows": + # Commande pour obtenir les interfaces réseau sous Windows + output = subprocess.check_output("ipconfig", shell=True).decode() + ip, mask = None, None + for line in output.splitlines(): + if "IPv4 Address" in line or "IPv4" in line: + ip = line.split(":")[-1].strip() + if "Subnet Mask" in line: + mask = line.split(":")[-1].strip() + if ip and mask: + if exclude_localhost and ip == "127.0.0.1": + continue + cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + ipv4_addresses.append(cidr) + ip, mask = None, None + elif system == "Linux": + # Commande pour obtenir les interfaces réseau sous Linux + output = subprocess.check_output("ip addr show", shell=True).decode() + for line in output.splitlines(): + line = line.strip() + if line.startswith("inet "): + parts = line.split() + ip_mask = parts[1] # Exemple : 192.168.1.100/24 + if exclude_localhost and ip_mask.startswith("127.0.0.1/"): + continue + ipv4_addresses.append(ip_mask) + elif system == "Darwin": # macOS est identifié par 'Darwin' + # Commande pour obtenir les interfaces réseau sous macOS + output = subprocess.check_output("ifconfig", shell=True).decode() + current_ip = None + for line in output.splitlines(): + line = line.strip() + if line.startswith("inet ") and "127.0.0.1" not in line: # Éviter localhost + parts = line.split() + ip = parts[1] # L'adresse IP est le second champ + mask = parts[3] # Le masque est généralement le quatrième champ + if exclude_localhost and ip == "127.0.0.1": + continue + cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + ipv4_addresses.append(cidr) + return ipv4_addresses + class networkagentinfo: def __init__(self, sessionid, action="resultgetinfo", param=[]): self.sessionid = sessionid From a28a09dc6f6c1117fa1adceb518d22cbd912db4c Mon Sep 17 00:00:00 2001 From: jfkneib Date: Thu, 10 Oct 2024 10:37:59 +0200 Subject: [PATCH 05/13] [plugin_backup_restore] le serveur ssh pour le transfert de fichiers peut etre configure avec 1 port autre que 22 on peut donc configurer ce port dans le plugin. ; SSH Server machine urbackup port number ssh_port = 22 dans le fichier de configuration. pulse_xmpp_agent/config/backup_restore.ini (cherry picked from commit c0340b28b470b4626cbe32a977ca00f8a7cc17ea) --- pulse_xmpp_agent/config/backup_restore.ini | 4 +++- .../plugins_common/plugin_backup_restore.py | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pulse_xmpp_agent/config/backup_restore.ini b/pulse_xmpp_agent/config/backup_restore.ini index 1aed78fb1..3520ea3c6 100644 --- a/pulse_xmpp_agent/config/backup_restore.ini +++ b/pulse_xmpp_agent/config/backup_restore.ini @@ -10,7 +10,6 @@ remote_user = urbackup # Cela permet de diminuer le risque de corruption de la copie de la clé en cas d'erreur de plugin. private_name_key = pulseuser_backup_id_rsa - username = pulseuser #on utilise le profil de pulseuser @@ -18,3 +17,6 @@ username = pulseuser # C:\Program Files\Medulla\var\backup_files pour Windows # /var/lib/pulse2/backup_files pour Darwin ou Linux restore_to_backup_location = True + +# SSH Server machine urbackup port number +ssh_port = 22 diff --git a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py index 6d4fc4f02..1814a7604 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py +++ b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py @@ -137,7 +137,8 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): data['directorylist'], data['base_path'], private_key_path=id_rsa_path, - restore_to_backup_location = objectxmpp.restore_to_backup_location) + restore_to_backup_location = objectxmpp.restore_to_backup_location, + ssh_port = objectxmpp.ssh_port) def read_conf_plugin_backup_restore(objectxmpp): """ @@ -186,6 +187,10 @@ def read_conf_plugin_backup_restore(objectxmpp): # C:\\Program Files\\Medulla\\var\\backup_files pour Windows # /var/lib/pulse2/backup_files pour Darwin ou Linux restore_to_backup_location = True + +# SSH Server machine urbackup port number +ssh_port = 22 + """ # Écrire le fichier de configuration avec le contenu par défaut with open(configfilename, 'w') as configfile: @@ -204,7 +209,7 @@ def read_conf_plugin_backup_restore(objectxmpp): objectxmpp.private_name_key = Config.get('backup_restore', 'private_name_key', fallback='pulseuser_backup_id_rsa') objectxmpp.username = Config.get('backup_restore', 'username', fallback='pulseuser') objectxmpp.restore_to_backup_location = Config.getboolean('backup_restore', 'restore_to_backup_location', fallback=True) - + objectxmpp.ssh_port = Config.getint('backup_restore', 'ssh_port', fallback=22) logger.debug(f"Configuration values set: remote_user={objectxmpp.remote_user}, " f"private_name_key={objectxmpp.private_name_key}, " f"username={objectxmpp.username}, " @@ -221,7 +226,8 @@ def copy_files_and_directories(remote_user, directories, base_path, private_key_path=None, - restore_to_backup_location=True): + restore_to_backup_location=True, + ssh_port=22): """ Copies files and directories from a remote host to the local machine. @@ -232,6 +238,7 @@ def copy_files_and_directories(remote_user, :param base_path: The base path for the backup. :param private_key_path: The path to the private key for SSH. :param restore_to_backup_location: Whether to restore files to the backup location. + :param ssh_port: port number ssh server. """ if private_key_path is None: private_key_path = os.path.join(getHomedrive(), ".ssh", "id_rsa") @@ -272,25 +279,28 @@ def copy_files_and_directories(remote_user, # -o ServerAliveInterval=10 # -o CheckHostIP=no # -o LogLevel=ERROR - # -o ConnectTimeout=10" + # -o ConnectTimeout=10 + # -p 2222" # -av --chmod=777 # urbackup@10.10.0.100:/media/BACKUP/urbackup/amu-win-6/240916-1727/Users/desktop.ini # "C:/Program Files/Medulla/var/backup_files/C_0/Users/desktop.ini cmd = ( - """%s -r -p -C "-o IdentityFile=%s" "-o UserKnownHostsFile=/dev/null" "-o StrictHostKeyChecking=no" "-o Batchmode=yes" "-o PasswordAuthentication=no" "-o ServerAliveInterval=10" "-o CheckHostIP=no" "-o LogLevel=ERROR" "-o ConnectTimeout=10" """ + """%s -r -p -C -P %s "-o IdentityFile=%s" "-o UserKnownHostsFile=/dev/null" "-o StrictHostKeyChecking=no" "-o Batchmode=yes" "-o PasswordAuthentication=no" "-o ServerAliveInterval=10" "-o CheckHostIP=no" "-o LogLevel=ERROR" "-o ConnectTimeout=10" """ % ( scp_path, + ssh_port, private_key_path, ) ) if rsync_available: cmd = ( - """%s -L -z --rsync-path=rsync -e "%s -o IdentityFile=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o Batchmode=yes -o PasswordAuthentication=no -o ServerAliveInterval=10 -o CheckHostIP=no -o LogLevel=ERROR -o ConnectTimeout=10" -av --chmod=777 """ + """%s -L -z --rsync-path=rsync -e "%s -o IdentityFile=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o Batchmode=yes -o PasswordAuthentication=no -o ServerAliveInterval=10 -o CheckHostIP=no -o LogLevel=ERROR -o ConnectTimeout=10 -p %s" -av --chmod=777 """ % ( rsync_path, ssh_path, private_key_path, + ssh_port ) ) From 2b5b3a61760e3e41f1a2e84760615e9fb64a03ea Mon Sep 17 00:00:00 2001 From: jfkneib Date: Thu, 10 Oct 2024 11:49:50 +0200 Subject: [PATCH 06/13] [plugin_backup_restore.py] change name parametre dans fichier initialisation du plugin (cherry picked from commit e69540b8754b181cbaec34aeefced2d7345a40a4) --- pulse_xmpp_agent/plugins_common/plugin_backup_restore.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py index 1814a7604..7aeb306f2 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py +++ b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py @@ -138,7 +138,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): data['base_path'], private_key_path=id_rsa_path, restore_to_backup_location = objectxmpp.restore_to_backup_location, - ssh_port = objectxmpp.ssh_port) + ssh_port = objectxmpp.reverseserver_ssh_port) def read_conf_plugin_backup_restore(objectxmpp): """ @@ -189,7 +189,8 @@ def read_conf_plugin_backup_restore(objectxmpp): restore_to_backup_location = True # SSH Server machine urbackup port number -ssh_port = 22 +# Reverse SSH Server port number +# reverseserver_ssh_port = 22 """ # Écrire le fichier de configuration avec le contenu par défaut @@ -209,7 +210,7 @@ def read_conf_plugin_backup_restore(objectxmpp): objectxmpp.private_name_key = Config.get('backup_restore', 'private_name_key', fallback='pulseuser_backup_id_rsa') objectxmpp.username = Config.get('backup_restore', 'username', fallback='pulseuser') objectxmpp.restore_to_backup_location = Config.getboolean('backup_restore', 'restore_to_backup_location', fallback=True) - objectxmpp.ssh_port = Config.getint('backup_restore', 'ssh_port', fallback=22) + objectxmpp.reverseserver_ssh_port = Config.getint('backup_restore', 'reverseserver_ssh_port', fallback=22) logger.debug(f"Configuration values set: remote_user={objectxmpp.remote_user}, " f"private_name_key={objectxmpp.private_name_key}, " f"username={objectxmpp.username}, " From 48b8bbc8d06c2e6f8c087055e55c1959dc4cd62f Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 14 Oct 2024 17:01:38 +0200 Subject: [PATCH 07/13] =?UTF-8?q?[plugin=20substitut]=20creation=20du=20pl?= =?UTF-8?q?ugin=20plugin=5Fload=5Fsend=5FSegment=5Ffile=20-=20Le=20plugin?= =?UTF-8?q?=20load=5Fsend=5FSegment=5Ffile=20installe=20une=20fonction=20a?= =?UTF-8?q?ppel=C3=A9e=20cycliquement=20pour=20envoyer=20des=20fichiers=20?= =?UTF-8?q?=C3=A0=20des=20machines.=20-=20Les=20fichiers=20sont=20envoy?= =?UTF-8?q?=C3=A9s=20par=20morceaux=20via=20des=20messages=20XMPP=20vers?= =?UTF-8?q?=20l'agent=20qui=20recombine=20les=20morceaux=20pour=20refaire?= =?UTF-8?q?=20le=20fichier=20a=20l'aide=20du=20plugin=5Frecombine=5Ffile?= =?UTF-8?q?=20-=20Chaque=20envoi=20inclut=20un=20fichier=20manifeste=20et?= =?UTF-8?q?=20des=20fichiers=20de=20contenu.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pour active cette fonction il faut declarer l'utilisation du plugin dans la rubrique [plugins] pluginliststart = ..., ..., loadlogsrotation, load_send_Segment_file exemple si on donne cette fonction au substitut reconf dans /etc/pulse-xmpp-agent-substitute/agent_master_substitute_reconf.ini on a la rubrique avec load_send_Segment_file dans la liste [plugins] pluginliststart = loadreconf, loadarscheck, loadlogsrotation, load_send_Segment_file (cherry picked from commit a91ceb9b151e24184e8f77bf255d141c559cd3d6) --- .../plugin_load_send_Segment_file.py | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py new file mode 100644 index 000000000..ffa632d10 --- /dev/null +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py @@ -0,0 +1,316 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2016-2023 Siveo +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +file : pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py + +Ce plugin installe une fonction appelée cycliquement. +Cette fonction a pour charge d'envoyer des fichiers à des machines. +""" + +import re +import json +import os +import logging +import configparser +import shutil +import types +from lib.configuration import confParameter +from datetime import datetime, timedelta +from lib.plugins.xmpp import XmppMasterDatabase +from lib.plugins.msc import MscDatabase +import traceback +from lib.utils import file_put_contents, simplecommandstr, simplecommand, getRandomName + +logger = logging.getLogger() + +DEBUGPULSEPLUGIN = 25 + +plugin = {"VERSION": "1.0", "NAME": "load_send_Segment_file", "TYPE": "substitute", "LOAD": "START"} # fmt: skip + +var_file_zipper = "/var/lib/pulse2/zip_transfert" + +def action(objectxmpp, action, sessionid, data, msg, dataerreur): + """ + Fonction principale du plugin, appelée lors de l'exécution du plugin. + + Args: + objectxmpp (object): L'objet XMPP. + action (str): L'action à exécuter. + sessionid (str): L'ID de la session. + data (dict): Les données associées à l'action. + msg (dict): Le message XMPP. + dataerreur (dict): Les données d'erreur. + + Returns: + None + """ + logger.debug("=====================================================") + logger.debug("call %s from %s" % (plugin, msg["from"])) + logger.debug("=====================================================") + logger.debug("data %s" % json.dumps(data, indent=4)) + + compteurcallplugin = getattr(objectxmpp, "num_call%s" % action) + if compteurcallplugin == 0: + read_conf_load_send_Segment_file(objectxmpp) + # Installer le code dynamique : fonction de transfert de fichier + objectxmpp.transfert_segment_file = types.MethodType(transfert_segment_file, objectxmpp) + # Planifier l'appel de cette fonction + objectxmpp.schedule( + "transfert_segment_file", + 15, + objectxmpp.transfert_segment_file, + repeat=True, + ) + +def transfert_segment_file(self): + """ + Fonction de transfert de fichiers vers les machines présentes. + + Args: + self (object): L'objet XMPP. + + Returns: + None + """ + type_transfert = "backup" + location = None + transfert_de_fichier_a_nb_machine_simultanement = 10 + repertoires_uuid = lister_repertoires_uuid(var_file_zipper)[:transfert_de_fichier_a_nb_machine_simultanement] + # Envoyer les fichiers tronqués. + for dir_uuid_machine in repertoires_uuid: + # dir est un répertoire mais le nom du répertoire est le UUID de la machine cible. + # Si la machine est présente. + + machine = XmppMasterDatabase().getMachinefromuuidsetup(str(dir_uuid_machine)) + + if machine: + if not machine['enabled']: + # La machine n'est pas présente, on ne peut pas lui envoyer des fichiers. + logger.debug("Machine %s eteinte : on ne peut pas transferer de fichiers a cette machine" % machine['hostname'] ) + logger.debug("tout les demande de transfert pour la machine %s sont annuler" % machine['hostname']) + supprimer_repertoire(os.path.join(var_file_zipper, dir_uuid_machine)) + continue + logger.debug("TRANSFERT SEGMENT FILE to machine %s " % (machine['hostname'] )) + # La machine est présente. On peut lui envoyer les fichiers. + list_repertoire_to_file_for_send = get_uuid_directories(os.path.join(var_file_zipper, dir_uuid_machine)) + for repertoire_file_seg in list_repertoire_to_file_for_send: + sessionid = getRandomName(5, "transfert_file") + file_list_to_send = lister_fichiers(repertoire_file_seg) + # logger.debug("REPERTOIRE DES FICHIERS %s \n file_list_to_send %s " % (repertoire_file_seg, file_list_to_send)) + + if len(file_list_to_send) < 2: + # Le fichier n'est pas encore segmenté ou il y a une erreur + continue + + # Le premier fichier de la liste doit être un fichier .manif pour manifeste. + if file_list_to_send[0].endswith(".manif"): + manifeste = lire_fichier_json(file_list_to_send[0]) + if len(file_list_to_send) != manifeste['nb_total'] + 1: + # Pas encore préparé + continue + + # logger.debug("contenue fichier manifeste : %s " % json.dumps(manifeste, indent=4)) + # logger.debug("repertoire_file_seg : %s " % repertoire_file_seg) + + if not manifeste: + # Le manifeste n'est pas un JSON correct + # Donc on considère que le transfert ne peut pas se faire. + supprimer_repertoire(repertoire_file_seg) + continue + + else: + continue + + a = 0 + for filesend in file_list_to_send: + # logger.debug("*********************************") + # logger.debug("filesend %s " % (filesend)) + # logger.debug("*********************************") + + if a == 0: + data = manifeste + # logger.debug("a=0 *********************************%s" % data) + else: + data = lire_fichier_json(filesend) + data['namefile'] = manifeste['namefile'] + # logger.debug("a=1 *********************************%s" % data) + + data['segment'] = a + data['dir_uuid_machine'] = dir_uuid_machine + data['dir_segment'] = os.path.basename(repertoire_file_seg) + msg_send = { + "sessionid": sessionid, + "data": data, + "action": "recombine_file", + "ret": 0 + } + # logger.debug("msg_send: %s " % json.dumps(msg_send, indent=4)) + a = a + 1 + # C'est la machine qui reçoit les segments qui doit recombiner le fichier. + self.send_message(mto=machine['jid'], mbody=json.dumps(msg_send), mtype="chat") + + supprimer_repertoire(repertoire_file_seg) + + supprimer_repertoire(os.path.join(var_file_zipper, dir_uuid_machine)) + +def supprimer_repertoire(chemin_repertoire): + """ + Supprime un répertoire et tout son contenu. + + Paramètres: + chemin_repertoire (str): Chemin du répertoire à supprimer. + + Retourne: + bool: True si le répertoire a été supprimé avec succès, False sinon. + """ + try: + shutil.rmtree(chemin_repertoire) + # logger.debug(f"Le répertoire {chemin_repertoire} a été supprimé avec succès.") + return True + except FileNotFoundError: + logger.debug(f"Le répertoire {chemin_repertoire} n'existe pas.") + return False + except PermissionError: + logger.debug(f"Vous n'avez pas les permissions nécessaires pour supprimer le répertoire {chemin_repertoire}.") + return False + except Exception as e: + logger.debug(f"Une erreur s'est produite lors de la suppression du répertoire {chemin_repertoire}: {e}") + return False + +def lire_fichier_json(chemin_fichier): + """ + Lit le contenu d'un fichier JSON et le stocke dans un dictionnaire. + + Paramètres: + chemin_fichier (str): Chemin du fichier JSON à lire. + + Retourne: + dict: Le contenu du fichier JSON sous forme de dictionnaire. + """ + try: + with open(chemin_fichier, 'r', encoding='utf-8') as fichier: + contenu = json.load(fichier) + return contenu + except FileNotFoundError: + logger.debug(f"Le fichier {chemin_fichier} n'existe pas.") + return None + except json.JSONDecodeError: + logger.debug(f"Erreur de décodage JSON dans le fichier {chemin_fichier}.") + return None + except Exception as e: + logger.debug(f"Erreur lors de la lecture du fichier {chemin_fichier}: {e}") + return None + +def get_uuid_directories(base_directory): + """ + Renvoie les répertoires dans un répertoire de base dont le nom commence par 10 chiffres. + + Args: + base_directory (str): Le chemin du répertoire de base. + + Returns: + list: Une liste des chemins des répertoires qui correspondent. + """ + matching_dirs = [] + + # Expression régulière pour 10 chiffres au début du nom + pattern = re.compile(r'^\d{10}_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') + + # Parcourt les fichiers et répertoires dans le répertoire de base + for dir_name in os.listdir(base_directory): + # Vérifie si c'est un répertoire et si son nom commence par 10 chiffres + dir_path = os.path.join(base_directory, dir_name) + if os.path.isdir(dir_path) and pattern.match(dir_name): + matching_dirs.append(dir_path) + + return matching_dirs + +def lister_fichiers(repertoire): + """ + Lit tous les fichiers dans un répertoire donné et renvoie une liste triée des fichiers. + + Paramètres: + repertoire (str): Chemin du répertoire. + + Retourne: + list: Une liste triée des noms de fichiers dans le répertoire. + """ + # logger.debug(f"cherche file repertoire {repertoire}") + try: + # Obtenir la liste des fichiers et des répertoires dans le répertoire donné + contenu = os.listdir(repertoire) + # logger.debug(f"CONTENU {contenu}") + # Filtrer pour ne conserver que les fichiers + fichiers = [os.path.join(repertoire, f) for f in contenu if os.path.isfile(os.path.join(repertoire, f))] + # logger.debug(f"fichiers {fichiers}") + # Trier la liste des fichiers + fichiers.sort() + + return fichiers + except Exception as e: + logger.debug(f"Erreur lors de la lecture du répertoire {repertoire}: {e}") + return [] + +def lire_contenu_fichier(chemin_fichier): + """ + Lit le contenu texte d'un fichier et le stocke dans une variable. + + Paramètres: + chemin_fichier (str): Chemin du fichier à lire. + + Retourne: + str: Le contenu texte du fichier. + """ + try: + with open(chemin_fichier, 'r', encoding='utf-8') as fichier: + contenu = fichier.read() + return contenu + except Exception as e: + logger.debug(f"Erreur lors de la lecture du fichier {chemin_fichier}: {e}") + return None + +def lister_repertoires_uuid(repertoire_principal): + """ + Renvoie une liste de tous les répertoires dans le répertoire principal qui ont un nom de la forme UUID. + + Paramètres: + repertoire_principal (str): Chemin du répertoire principal. + + Retourne: + list: Une liste des répertoires ayant un nom de la forme UUID. + """ + # Expression régulière pour vérifier le format UUID + uuid_pattern = re.compile(r'^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$') + # Liste pour stocker les répertoires UUID + repertoires_uuid = [] + # Parcourir les éléments dans le répertoire principal + for item in os.listdir(repertoire_principal): + item_path = os.path.join(repertoire_principal, item) + if os.path.isdir(item_path) and uuid_pattern.match(item): + repertoires_uuid.append(item) + return repertoires_uuid + +def read_conf_load_send_Segment_file(objectxmpp): + """ + Lire la configuration du plugin. + Le dossier contenant le fichier de configuration est dans la variable objectxmpp.config.pathdirconffile. + + Args: + objectxmpp (object): L'objet XMPP. + + Returns: + None + """ + nameconffile = plugin["NAME"] + ".ini" + pathconffile = os.path.join(objectxmpp.config.pathdirconffile, nameconffile) + if not os.path.isfile(pathconffile): + logger.info("pas de fichiers de configuration %s " % pathconffile) + else: + # Implémentez la configuration + # Config = configparser.ConfigParser() + # Config.read(pathconffile) + # if os.path.exists(pathconffile + ".local"): + # Config.read(pathconffile + ".local") + pass From e6b324b548e485491561b5834c4bdfefa89a2ad7 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 14 Oct 2024 17:46:34 +0200 Subject: [PATCH 08/13] [substitut master] Plugin master. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remarque importante. la signature de la fonction principal utilise est la suivante. def action(xmppobject, action, sessionid, data, message, ret, dataobj=None) dataobj=None permet a ce plugin d'etre aussi appele depuis 1 substitut. Ce plugin est appelé depuis mmc, il est utilisé par substitut master. Il a comme but de transférer des fichiers de backup via xmpp. Il crée une archive zip, celle de ce que l'on veut transférer via xmpp. L'archive est décomposée en morceaux dans un répertoire /var/lib/pulse2/zip_transfert. Le JSON reçu par ce plugin est de la forme suivante : { "action": "backup_restore_substitut", "sessionid": "backup_restoreq3d0m6vl", "data": { "key_private": "-----BEGIN OPENSSH PRIVATE KEY--------\n", "ip_list_reseau": [ "127.0.0.0/8", "10.10.0.0/16", "95.217.42.79/32" ], "ip_list": [ [ "10.10.0.30", "255.255.0.0" ], [ "95.217.42.79", "255.255.255.255" ] ], "machine_source_backup": { "hostname": "DESKTOP-Q7O57H5", "jid": "DESKTOP-Q7O57H5.68i@pulse", "uuid_inventorymachine": "UUID5", "uuid_serial_machine": "4C4C4544-0030-4710-8054-C2C04F544C33", "id": 501, "enabled": 0 }, "machine_dest_backup": { "hostname": "DESKTOP-Q7O57H5", "jid": "DESKTOP-Q7O57H5.68i@pulse", "uuid_inventorymachine": "UUID5", "uuid_serial_machine": "4C4C4544-0030-4710-8054-C2C04F544C33", "id": 501, "enabled": 0 }, "base_path": "/media/BACKUP/urbackup/amu-win-6/240916-1727", "directorylist": [ [ "C_0/Users/pulseuser", "C:/Users/pulseuser" ] ], "filelist": [ [ "Users/desktop.ini", "C:\\Users\\desktop.ini" ] ] } } (cherry picked from commit fbd87c18c2095bb8feb05469311c9f287dad044d) --- .../plugin_backup_restore_substitut.py | 576 ++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py new file mode 100644 index 000000000..93fb844bd --- /dev/null +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py @@ -0,0 +1,576 @@ +#!/usr/bin/python3 +# -*- coding: utf-8; -*- +# SPDX-FileCopyrightText: 2016-2023 Siveo +# SPDX-License-Identifier: GPL-3.0-or-later +# pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py + +# ce plugin est appeler depuis mmc +# il a comme but de transferer des fichier de backup + + +""" +This plugin can be called from quick action +""" +import traceback +import json +import logging +import os +import hashlib +from datetime import datetime +import zipfile +import uuid +import time +import re +import base64 + +var_file_zipzer = "/var/lib/pulse2/zip_transfert" + +logger = logging.getLogger() + +plugin = {"VERSION": "1.0", "NAME": "backup_restore_substitut", "TYPE": "mastersub"} + +def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): + logger.debug("###################################################") + logger.debug("call %s from %s" % (plugin, message["from"])) + logger.debug("###################################################") + logger.debug("datafffffffffffffff %s" % json.dumps(data['data'], indent=4)) + try: + datamsg=data['data'] + logger.debug("datamsg %s" % datamsg.keys()) + # Spécifier le chemin où créer le répertoire ZIP + var_file_zipzer = "/var/lib/pulse2/zip_transfert" + # Créer le répertoire s'il n'existe pas + os.makedirs(var_file_zipzer, exist_ok=True) + if 'directorylist' in datamsg and datamsg['directorylist']: + for directory in datamsg['directorylist']: + logger.debug("creation d'un fichier zip depuis 1 repertoire : %s" % directory) + if directory: + repertoire_zip = os.path.join(datamsg['base_path'], directory[0]) + logger.info("repertoire_zip %s" %repertoire_zip) + name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], + os.path.join(directory[1])) + archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + logger.info("creation d'un fichier zip %s" %path_archive_fichier_name_zip) + if not zipper_repertoire( repertoire_zip, path_archive_fichier_name_zip): + logger.info("demande de compression dun repertoire inexistant") + continue + # Exemple d'utilisation + manager = ZipFileManager(var_file_zipzer) + manager.analyze_and_cleanup() + output_dir_list = process_zip_files(var_file_zipzer, var_file_zipzer, datamsg['machine_dest_backup']['uuid_serial_machine'], segment_size=64000) + if 'filelist' in datamsg and datamsg['filelist']: + for files in datamsg['filelist']: + logger.debug("creation d'un fichier zip depuis 1 fichier : %s" % files) + if files: + input_file = os.path.join(datamsg['base_path'], files[0]) + logger.info("imput file a zipper %s" %input_file) + name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], + os.path.join(files[1])) + archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) + + path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + logger.info("creation d'un fichier zip %s" %path_archive_fichier_name_zip) + if not zipper_fichier(input_file, path_archive_fichier_name_zip, fichier_vide=True): + # if not zipper_repertoire( file_zip, path_archive_fichier_name_zip): + logger.info("demande de compression dun fichier inexistant") + continue + # Exemple d'utilisation + manager = ZipFileManager(var_file_zipzer) + manager.analyze_and_cleanup() + output_dir_list = process_zip_files(var_file_zipzer, + var_file_zipzer, + datamsg['machine_dest_backup']['uuid_serial_machine'], + segment_size=64000, + type_transfert = "backup", + location = None, + contenttype = "file" + ) + except Exception: + logger.error("%s" % (traceback.format_exc())) + +def process_zip_files(input_dir, + output_dir_base_trunck, + uuid_serial_machine, + segment_size=8000, + type_transfert = "backup", + location = None, + contenttype = "directory"): + """ + Lit tous les fichiers ZIP d'un répertoire, les découpe et enregistre les segments dans un répertoire de sortie basé sur le JID. + contenttype file ou directory + """ + output_dir_list=[] + output_dir_base = f"{output_dir_base_trunck}/{uuid_serial_machine}" + + # Vérifier si le répertoire de base existe, sinon le créer + if not os.path.exists(output_dir_base): + os.makedirs(output_dir_base) + + # Parcourir tous les fichiers dans le répertoire d'entrée + for file_name in os.listdir(input_dir): + if file_name.endswith(".zip"): # Vérifier si c'est un fichier ZIP + file_path = os.path.join(input_dir, file_name) + output_dir = os.path.join(output_dir_base, os.path.splitext(file_name)[0]) + + # Créer le répertoire de sortie pour chaque fichier ZIP + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Appeler la fonction split_file pour découper le fichier ZIP + split_file(file_path, + output_dir, + segment_size, + type_transfert, + location, + contenttype) + + logger.debug(f"Fichier {file_name} découpé et enregistré dans {output_dir}") + try: + os.remove(file_path) + logger.debug(f"Le fichier {file_name} a été effacé avec succès.") + return True + except FileNotFoundError: + logger.error(f"Le fichier {file_name} n'existe pas.") + return output_dir_list + except PermissionError: + logger.error(f"Vous n'avez pas les permissions nécessaires pour effacer le fichier {file_name}.") + return output_dir_list + except Exception as e: + logger.error(f"Une erreur s'est produite lors de l'effacement du fichier {file_name}: {e}") + return output_dir_list + output_dir_list.append(output_dir) + return output_dir_list + +def md5_hash(file_path): + """Calcule le hash MD5 d'un fichier.""" + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + +def split_file(file_path, + output_dir, + segment_size=8000, + type_transfert = "backup", + location = None , + contenttype = "directory"): + """Découpe un fichier en segments et enregistre chaque segment sous forme de fichier JSON en base64.""" + # Vérifier si le répertoire de sortie existe, sinon le créer + logger.error(f"split_file file_path {file_path}: ") + logger.error(f"split_file output_dir {output_dir}: ") + logger.error(f"split_file contenttype {contenttype}: ") + + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Obtenir les informations de base du fichier + file_name = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + total_parts = (file_size + segment_size - 1) // segment_size + file_uuid = str(uuid.uuid4()) + directory_or_file= file_name + if file_name.endswith(".zip"): + directory_or_file = file_name[:-4] + try: + parties = file_name.split('_') + # Prendre seulement la 3e partie + directory_or_file = parties[2] + except ValueError as ve: + logger.error("pas 1 fichier zip") + return None + + # Calculer le hash MD5 du fichier original + original_md5 = md5_hash(file_path) + + # Lecture et découpe du fichier en segments + with open(file_path, "rb") as f: + part_num = 0 + while chunk := f.read(segment_size): + # Encodage de la partie en base64 + encoded_content = base64.b64encode(chunk).decode('utf-8') + + # Création du fichier JSON pour cette partie + part_data = { + "namefile": file_name, + "directory":directory_or_file, + "nb": part_num + 1, + "nbtotal": total_parts, + "content": encoded_content, + "type": type_transfert, + "location": location, + "contenttype": contenttype + } + + part_file_name = f"{file_uuid}_{part_num+1}.json" + part_file_path = os.path.join(output_dir, part_file_name) + + with open(part_file_path, "w") as part_file: + json.dump(part_data, part_file) + + part_num += 1 + + # Création du fichier manifeste + manifest_data = { + "namefile": file_name, + "directory":directory_or_file, + "creation": str(datetime.now()), + "nb_total": total_parts, + "md5": original_md5, + "type_file": "zip" if file_name.endswith('.zip') else "unknown", + "size_trunck": segment_size, + "type": type_transfert, + "location": location, + "contenttype": contenttype + } + + manifest_file_name = f"{file_uuid}_0.manif" + manifest_file_path = os.path.join(output_dir, manifest_file_name) + + with open(manifest_file_path, "w") as manifest_file: + json.dump(manifest_data, manifest_file) + + logger.debug(f"Fichier découpé en {total_parts} parties et manifest généré.") + +def recombine_file(manifest_path, input_dir, output_file): + """Recompose le fichier original à partir des segments JSON et vérifie le hash MD5.""" + # Lire le fichier manifeste + with open(manifest_path, "r") as manifest_file: + manifest_data = json.load(manifest_file) + + original_file_name = manifest_data["le nom reelle du fichier"] + total_parts = manifest_data["nombre total de partie"] + original_md5 = manifest_data["md5"] + + file_uuid = os.path.basename(manifest_path).split("_")[0] + + with open(output_file, "wb") as output_f: + # Recomposer le fichier à partir des fichiers JSON + for part_num in range(1, total_parts + 1): + part_file_name = f"{file_uuid}_{part_num}.json" + part_file_path = os.path.join(input_dir, part_file_name) + + with open(part_file_path, "r") as part_file: + part_data = json.load(part_file) + # Décoder le contenu base64 et l'écrire dans le fichier de sortie + decoded_content = base64.b64decode(part_data["content"]) + output_f.write(decoded_content) + + # Vérification du hash MD5 du fichier recomposé + recombined_md5 = md5_hash(output_file) + if recombined_md5 == original_md5: + logger.debug(f"Fichier recomposé avec succès, MD5 vérifié : {recombined_md5}") + else: + logger.debug(f"Erreur : le MD5 recomposé {recombined_md5} ne correspond pas à l'original {original_md5}.") + + +def check_manifests(directory): + """Vérifie tous les manifestes dans un répertoire et retourne deux listes : + - Manifestes complets : Tous les fichiers nécessaires sont présents. + - Manifestes incomplets : Certains fichiers manquent. + """ + complete_manifests = [] + incomplete_manifests = [] + + # Parcourir tous les fichiers dans le répertoire + for file_name in os.listdir(directory): + # Chercher uniquement les fichiers manifeste (suffixe _0.manif) + if file_name.endswith("_0.manif"): + manifest_path = os.path.join(directory, file_name) + + # Lire le fichier manifeste + with open(manifest_path, "r") as manifest_file: + manifest_data = json.load(manifest_file) + + total_parts = manifest_data["nombre total de partie"] + file_uuid = file_name.split("_")[0] + + # Vérifier que toutes les parties de fichiers existent + all_parts_present = True + for part_num in range(1, total_parts + 1): + part_file_name = f"{file_uuid}_{part_num}.json" + part_file_path = os.path.join(directory, part_file_name) + if not os.path.exists(part_file_path): + all_parts_present = False + break + + if all_parts_present: + complete_manifests.append(file_name) + else: + incomplete_manifests.append(file_name) + + return complete_manifests, incomplete_manifests + + +def remplacer_caracteres(chaine): + # Remplacer @ par @@ + chaine = chaine.replace('@', '@64@') + # Remplacer les : par : + chaine = chaine.replace(':', '@58@') + # Remplacer les espaces par @nbsp@ + chaine = chaine.replace(' ', '@nbsp@') + # Remplacer les barres obliques / par / + chaine = chaine.replace('/', '@47@') + # Remplacer les barres obliques \ par \ + chaine = chaine.replace('\\', '@92@') + # Remplacer . par \ + chaine = chaine.replace('.', '@46@') + return chaine + +def restaurer_caracteres(chaine): + chaine = chaine.replace('@58@', ':') + # Remplacer @46@ par . + chaine = chaine.replace('@46@', '.') + # Remplacer @92@ par \ + chaine = chaine.replace('@92@', '\\') + # Remplacer @47@ par / + chaine = chaine.replace('@47@', '/') + # Remplacer @nbsp@ par espace + chaine = chaine.replace('@nbsp@', ' ') + # Remplacer @@ par @ + chaine = chaine.replace('@64@', '@') + + return chaine + +def time_since_timestamp(timestamp): + """ + Calcule le temps écoulé depuis un timestamp donné. + + Args: + timestamp (int): Le timestamp à partir duquel calculer le temps écoulé. + + Returns: + tuple: Un tuple contenant le nombre de jours, heures, minutes et secondes écoulés depuis le timestamp. + """ + # Obtenir le temps actuel + current_time = time.time() + + # Calculer la différence en secondes + time_difference = current_time - timestamp + + # Convertir la différence en jours, heures, minutes et secondes + days = time_difference // (24 * 3600) + time_difference %= (24 * 3600) + hours = time_difference // 3600 + time_difference %= 3600 + minutes = time_difference // 60 + seconds = time_difference % 60 + + return int(days), int(hours), int(minutes), int(seconds) + +def generer_name_avec_timestamp(jid_dest_backup, pathnamefile, millisecondes=False): + """ + Génère un UUID aléatoire et y ajoute un timestamp. + + :param millisecondes: Si True, utilise le timestamp en millisecondes. Par défaut, c'est False. + :return: Une chaîne contenant l'UUID et le timestamp. + """ + # emplacement = remplacer_caracteres(pathnamefile) + emplacement = pathnamefile + # Obtenir le timestamp + if millisecondes: + timestamp = int(time.time() * 1000) # Timestamp en millisecondes + else: + timestamp = int(time.time()) # Timestamp en secondes + # Combiner le UUID et le timestamp + nom_file_emplacement = f"{timestamp}_{jid_dest_backup}_{emplacement}" + return remplacer_caracteres(nom_file_emplacement) + +def generer_uuid_avec_timestamp(millisecondes=False): + """ + Génère un UUID aléatoire et y ajoute un timestamp. + + :param millisecondes: Si True, utilise le timestamp en millisecondes. Par défaut, c'est False. + :return: Une chaîne contenant l'UUID et le timestamp. + """ + # Générer un UUID aléatoire + uuid_aleatoire = uuid.uuid4() + + # Obtenir le timestamp + if millisecondes: + timestamp = int(time.time() * 1000) # Timestamp en millisecondes + else: + timestamp = int(time.time()) # Timestamp en secondes + + # Combiner le UUID et le timestamp + uuid_timestamp = f"{timestamp}-{uuid_aleatoire}" + + return uuid_timestamp + + +class ZipFileManager: + """ + Classe qui gère les fichiers ZIP dans un répertoire en conservant les fichiers les plus récents + basés sur un horodatage (timestamp) dans le nom du fichier, et en supprimant les fichiers plus anciens. + + Le format attendu des fichiers ZIP est : _.zip + """ + + def __init__(self, directory): + """ + Initialise un gestionnaire de fichiers ZIP pour le répertoire spécifié. + + :param directory: Le répertoire contenant les fichiers ZIP à gérer. + """ + self.directory = directory + self.files_to_keep = {} + + def analyze_and_cleanup(self): + """ + Analyse les fichiers ZIP dans le répertoire et supprime les fichiers obsolètes. + + Garde uniquement le fichier ZIP le plus récent pour chaque combinaison unique de nom de fichier (sans timestamp). + """ + # Lire tous les fichiers .zip dans le répertoire + zip_files = [f for f in os.listdir(self.directory) if f.endswith('.zip')] + + # Analyser chaque fichier + for file in zip_files: + # Vérifier que le fichier correspond au format attendu : _.zip + match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + if match: + timestamp, file_rest = match.groups() + key = file_rest + + # Garder le fichier avec le plus grand timestamp (le plus récent) + if key not in self.files_to_keep or int(timestamp) > int(self.files_to_keep[key][0]): + self.files_to_keep[key] = (timestamp, file) + + # Supprimer les fichiers obsolètes + for file in zip_files: + match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + if match: + timestamp, file_rest = match.groups() + key = file_rest + + # Si un fichier plus récent existe, supprimer l'ancien + if key in self.files_to_keep and self.files_to_keep[key][1] != file: + self.delete_file(file) + + def delete_file(self, file): + """ + Supprime un fichier ZIP du répertoire. + + :param file: Le nom du fichier à supprimer. + """ + file_path = os.path.join(self.directory, file) + os.remove(file_path) + logger.debug(f"Supprimé : {file_path}") + + + + +def zipper_fichier(fichier, fichier_zip, fichier_vide=True): + """ + Zipe un seul fichier. + + Args: + fichier (str): Le chemin du fichier à zipper. + fichier_zip (str): Le nom du fichier zip de sortie. + fichier_vide (bool, optional): Si True, inclut les fichiers vides dans le zip. (par défaut: True) + + Returns: + None: La fonction crée un fichier zip et n'a pas de valeur de retour. + """ + # Vérifier si le répertoire existe + if not os.path.exists(fichier): + return None + + with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + if os.path.islink(fichier): + # Ajouter le lien symbolique en tant que tel dans le zip sans compression + zipf.write(fichier, os.path.basename(fichier), compress_type=zipfile.ZIP_STORED) + else: + # Inclure les fichiers vides si demandé + if fichier_vide or os.path.getsize(fichier) > 0: + zipf.write(fichier, os.path.basename(fichier)) + return True + + +def zipper_repertoire(repertoire, fichier_zip, resoudre_liens=False, repertoire_vide=True, fichier_vide=True): + """ + Zipe le contenu d'un répertoire, y compris tous les fichiers, sous-répertoires, + répertoires vides et fichiers vides. + + Args: + repertoire (str): Le chemin du répertoire à zipper. + fichier_zip (str): Le nom du fichier zip de sortie. + resoudre_liens (bool, optional): Si True, résout les liens symboliques et ajoute leurs cibles au zip. + Si False, ajoute les liens symboliques tels quels. (par défaut: False) + repertoire_vide (bool, optional): Si True, inclut les répertoires vides dans le zip. (par défaut: False) + fichier_vide (bool, optional): Si True, inclut les fichiers vides dans le zip. (par défaut: False) + + Returns: + None: Si le répertoire n'existe pas ou est vide. + True: Si le répertoire n'est pas vide et le fichier zip a été créé. + """ + # Vérifier si le répertoire existe + if not os.path.exists(repertoire): + return None + + # Vérifier si le répertoire est vide + if not os.listdir(repertoire): + return None + + with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Parcourt tous les fichiers et sous-répertoires dans le répertoire spécifié + for root, dirs, files in os.walk(repertoire): + # Ajouter le répertoire courant (même s'il est vide) + chemin_rel_repertoire = os.path.relpath(root, repertoire) + if repertoire_vide or files: # Inclut les répertoires vides ou ceux avec des fichiers + zipf.write(root, chemin_rel_repertoire) # Ajoute le répertoire courant au zip + + for file in files: + chemin_complet = os.path.join(root, file) # Chemin complet du fichier + chemin_rel = os.path.relpath(chemin_complet, repertoire) # Chemin relatif pour le zip + + if os.path.islink(chemin_complet): + if resoudre_liens: + # Résoudre le lien symbolique et ajouter la cible au zip + chemin_cible = os.readlink(chemin_complet) + zipf.write(chemin_cible, chemin_rel) + else: + # Ajouter le lien symbolique en tant que tel dans le zip + zipf.write(chemin_complet, chemin_rel) + else: + # Inclure les fichiers vides si demandé + if fichier_vide or os.path.getsize(chemin_complet) > 0: + zipf.write(chemin_complet, chemin_rel) + + return True +# +# def zipper_repertoire( repertoire, fichier_zip, resoudre_liens = False): +# with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: +# for root, dirs, files in os.walk(repertoire): +# for file in files: +# chemin_complet = os.path.join(root, file) +# chemin_rel = os.path.relpath(chemin_complet, repertoire) +# +# if os.path.islink(chemin_complet): +# if resoudre_liens: +# # Résoudre le lien symbolique et ajouter la cible +# chemin_cible = os.readlink(chemin_complet) +# zipf.write(chemin_cible, chemin_rel) +# else: +# # Ajouter le lien symbolique en tant que tel +# zipf.write(chemin_complet, chemin_rel) +# else: +# # Ajouter le fichier normal +# zipf.write(chemin_complet, chemin_rel) + +def decompresser_archive(fichier_zip, repertoire_destination): + # Vérifier si le fichier ZIP existe + if not os.path.exists(fichier_zip): + raise FileNotFoundError(f"Le fichier ZIP {fichier_zip} n'existe pas.") + + # Créer le répertoire de destination s'il n'existe pas déjà + if not os.path.exists(repertoire_destination): + os.makedirs(repertoire_destination) + + # Ouvrir et extraire tout le contenu du fichier ZIP + with zipfile.ZipFile(fichier_zip, 'r') as zip_ref: + zip_ref.extractall(repertoire_destination) From 94ab8a384adfe987b719d3f3cf1a1f5fdf3bea39 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 14 Oct 2024 17:51:29 +0200 Subject: [PATCH 09/13] [plugin master] ce plugin est appeler depuis la MMC. remarque importante. on ajoute a la signature de la fonction action, "dataobj=None" cela permet d'avoir 1 plugin appelable aussi depuis 1 substitut. def action(xmppobject, action, sessionid, data, message, ret, dataobj=None) comme par exemple le plugin WOL appeler depuis MMC et depuis le substitut de deployement. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce plugin est appelé depuis mmc, il est utilisé par substitut master. Il a comme but de transférer des fichiers de backup via xmpp. Il crée une archive zip, celle de ce que l'on veut transférer via xmpp. L'archive est décomposée en morceaux dans un répertoire /var/lib/pulse2/zip_transfert. Le JSON reçu par ce plugin est de la forme suivante : { "action": "backup_restore_substitut", "sessionid": "backup_restoreq3d0m6vl", "data": { "key_private": "-----BEGIN OPENSSH PRIVATE KEY--------\n", "ip_list_reseau": [ "127.0.0.0/8", "10.10.0.0/16", "95.217.42.79/32" ], "ip_list": [ [ "10.10.0.30", "255.255.0.0" ], [ "95.217.42.79", "255.255.255.255" ] ], "machine_source_backup": { "hostname": "DESKTOP-Q7O57H5", "jid": "DESKTOP-Q7O57H5.68i@pulse", "uuid_inventorymachine": "UUID5", "uuid_serial_machine": "4C4C4544-0030-4710-8054-C2C04F544C33", "id": 501, "enabled": 0 }, "machine_dest_backup": { "hostname": "DESKTOP-Q7O57H5", "jid": "DESKTOP-Q7O57H5.68i@pulse", "uuid_inventorymachine": "UUID5", "uuid_serial_machine": "4C4C4544-0030-4710-8054-C2C04F544C33", "id": 501, "enabled": 0 }, "base_path": "/media/BACKUP/urbackup/amu-win-6/240916-1727", "directorylist": [ [ "C_0/Users/pulseuser", "C:/Users/pulseuser" ] ], "filelist": [ [ "Users/desktop.ini", "C:\\Users\\desktop.ini" ] ] } } (cherry picked from commit 12d6418494a9cb1f8df00ad71ddebc135979b05b) --- .../plugin_backup_restore_substitut.py | 100 +++++++----------- 1 file changed, 37 insertions(+), 63 deletions(-) diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py index 93fb844bd..075135623 100644 --- a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py @@ -4,9 +4,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py -# ce plugin est appeler depuis mmc -# il a comme but de transferer des fichier de backup - +# Ce plugin est appelé depuis mmc, il est utilisé par substitut master. +# Il a comme but de transférer des fichiers de backup via xmpp +# Il crée une archive zip, celle de ce que l'on veut transférer via xmpp. +# L'archive est décomposée en morceaux dans un répertoire /var/lib/pulse2/zip_transfert """ This plugin can be called from quick action @@ -23,6 +24,7 @@ import re import base64 +# Chemin où les fichiers ZIP seront créés var_file_zipzer = "/var/lib/pulse2/zip_transfert" logger = logging.getLogger() @@ -33,48 +35,52 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): logger.debug("###################################################") logger.debug("call %s from %s" % (plugin, message["from"])) logger.debug("###################################################") - logger.debug("datafffffffffffffff %s" % json.dumps(data['data'], indent=4)) + logger.debug("data %s" % json.dumps(data['data'], indent=4)) try: - datamsg=data['data'] + datamsg = data['data'] logger.debug("datamsg %s" % datamsg.keys()) + # Spécifier le chemin où créer le répertoire ZIP var_file_zipzer = "/var/lib/pulse2/zip_transfert" # Créer le répertoire s'il n'existe pas os.makedirs(var_file_zipzer, exist_ok=True) + + # Traitement des répertoires à zipper if 'directorylist' in datamsg and datamsg['directorylist']: for directory in datamsg['directorylist']: logger.debug("creation d'un fichier zip depuis 1 repertoire : %s" % directory) if directory: repertoire_zip = os.path.join(datamsg['base_path'], directory[0]) - logger.info("repertoire_zip %s" %repertoire_zip) + logger.info("repertoire_zip %s" % repertoire_zip) name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], - os.path.join(directory[1])) + os.path.join(directory[1])) archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' - logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) - path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) - logger.info("creation d'un fichier zip %s" %path_archive_fichier_name_zip) - if not zipper_repertoire( repertoire_zip, path_archive_fichier_name_zip): + logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) + if not zipper_repertoire(repertoire_zip, path_archive_fichier_name_zip): logger.info("demande de compression dun repertoire inexistant") continue # Exemple d'utilisation manager = ZipFileManager(var_file_zipzer) manager.analyze_and_cleanup() output_dir_list = process_zip_files(var_file_zipzer, var_file_zipzer, datamsg['machine_dest_backup']['uuid_serial_machine'], segment_size=64000) + + # Traitement des fichiers à zipper if 'filelist' in datamsg and datamsg['filelist']: for files in datamsg['filelist']: logger.debug("creation d'un fichier zip depuis 1 fichier : %s" % files) if files: input_file = os.path.join(datamsg['base_path'], files[0]) - logger.info("imput file a zipper %s" %input_file) + logger.info("imput file a zipper %s" % input_file) name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], - os.path.join(files[1])) + os.path.join(files[1])) archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' - logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) + logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) - path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) - logger.info("creation d'un fichier zip %s" %path_archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) if not zipper_fichier(input_file, path_archive_fichier_name_zip, fichier_vide=True): - # if not zipper_repertoire( file_zip, path_archive_fichier_name_zip): logger.info("demande de compression dun fichier inexistant") continue # Exemple d'utilisation @@ -84,9 +90,9 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): var_file_zipzer, datamsg['machine_dest_backup']['uuid_serial_machine'], segment_size=64000, - type_transfert = "backup", - location = None, - contenttype = "file" + type_transfert="backup", + location=None, + contenttype="file" ) except Exception: logger.error("%s" % (traceback.format_exc())) @@ -95,14 +101,14 @@ def process_zip_files(input_dir, output_dir_base_trunck, uuid_serial_machine, segment_size=8000, - type_transfert = "backup", - location = None, - contenttype = "directory"): + type_transfert="backup", + location=None, + contenttype="directory"): """ Lit tous les fichiers ZIP d'un répertoire, les découpe et enregistre les segments dans un répertoire de sortie basé sur le JID. contenttype file ou directory """ - output_dir_list=[] + output_dir_list = [] output_dir_base = f"{output_dir_base_trunck}/{uuid_serial_machine}" # Vérifier si le répertoire de base existe, sinon le créer @@ -155,16 +161,10 @@ def md5_hash(file_path): def split_file(file_path, output_dir, segment_size=8000, - type_transfert = "backup", - location = None , - contenttype = "directory"): + type_transfert="backup", + location=None, + contenttype="directory"): """Découpe un fichier en segments et enregistre chaque segment sous forme de fichier JSON en base64.""" - # Vérifier si le répertoire de sortie existe, sinon le créer - logger.error(f"split_file file_path {file_path}: ") - logger.error(f"split_file output_dir {output_dir}: ") - logger.error(f"split_file contenttype {contenttype}: ") - - if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -173,7 +173,7 @@ def split_file(file_path, file_size = os.path.getsize(file_path) total_parts = (file_size + segment_size - 1) // segment_size file_uuid = str(uuid.uuid4()) - directory_or_file= file_name + directory_or_file = file_name if file_name.endswith(".zip"): directory_or_file = file_name[:-4] try: @@ -197,7 +197,7 @@ def split_file(file_path, # Création du fichier JSON pour cette partie part_data = { "namefile": file_name, - "directory":directory_or_file, + "directory": directory_or_file, "nb": part_num + 1, "nbtotal": total_parts, "content": encoded_content, @@ -217,7 +217,7 @@ def split_file(file_path, # Création du fichier manifeste manifest_data = { "namefile": file_name, - "directory":directory_or_file, + "directory": directory_or_file, "creation": str(datetime.now()), "nb_total": total_parts, "md5": original_md5, @@ -267,7 +267,6 @@ def recombine_file(manifest_path, input_dir, output_file): else: logger.debug(f"Erreur : le MD5 recomposé {recombined_md5} ne correspond pas à l'original {original_md5}.") - def check_manifests(directory): """Vérifie tous les manifestes dans un répertoire et retourne deux listes : - Manifestes complets : Tous les fichiers nécessaires sont présents. @@ -305,7 +304,6 @@ def check_manifests(directory): return complete_manifests, incomplete_manifests - def remplacer_caracteres(chaine): # Remplacer @ par @@ chaine = chaine.replace('@', '@64@') @@ -401,7 +399,6 @@ def generer_uuid_avec_timestamp(millisecondes=False): return uuid_timestamp - class ZipFileManager: """ Classe qui gère les fichiers ZIP dans un répertoire en conservant les fichiers les plus récents @@ -461,9 +458,6 @@ def delete_file(self, file): os.remove(file_path) logger.debug(f"Supprimé : {file_path}") - - - def zipper_fichier(fichier, fichier_zip, fichier_vide=True): """ Zipe un seul fichier. @@ -476,7 +470,7 @@ def zipper_fichier(fichier, fichier_zip, fichier_vide=True): Returns: None: La fonction crée un fichier zip et n'a pas de valeur de retour. """ - # Vérifier si le répertoire existe + # Vérifier si le répertoire existe if not os.path.exists(fichier): return None @@ -490,7 +484,6 @@ def zipper_fichier(fichier, fichier_zip, fichier_vide=True): zipf.write(fichier, os.path.basename(fichier)) return True - def zipper_repertoire(repertoire, fichier_zip, resoudre_liens=False, repertoire_vide=True, fichier_vide=True): """ Zipe le contenu d'un répertoire, y compris tous les fichiers, sous-répertoires, @@ -542,25 +535,6 @@ def zipper_repertoire(repertoire, fichier_zip, resoudre_liens=False, repertoire_ zipf.write(chemin_complet, chemin_rel) return True -# -# def zipper_repertoire( repertoire, fichier_zip, resoudre_liens = False): -# with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: -# for root, dirs, files in os.walk(repertoire): -# for file in files: -# chemin_complet = os.path.join(root, file) -# chemin_rel = os.path.relpath(chemin_complet, repertoire) -# -# if os.path.islink(chemin_complet): -# if resoudre_liens: -# # Résoudre le lien symbolique et ajouter la cible -# chemin_cible = os.readlink(chemin_complet) -# zipf.write(chemin_cible, chemin_rel) -# else: -# # Ajouter le lien symbolique en tant que tel -# zipf.write(chemin_complet, chemin_rel) -# else: -# # Ajouter le fichier normal -# zipf.write(chemin_complet, chemin_rel) def decompresser_archive(fichier_zip, repertoire_destination): # Vérifier si le fichier ZIP existe From 3d501e74a4a56a75e70339638662a8a116c95078 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 14 Oct 2024 18:00:16 +0200 Subject: [PATCH 10/13] =?UTF-8?q?[agent=20machine/relay]=20Ajout=20du=20pl?= =?UTF-8?q?ugin=20plugin=5Frecombine=5Ffile.py=20pour=20reconstituer=20les?= =?UTF-8?q?=20segments=20de=20fichiers=20transf=C3=A9r=C3=A9s=20et=20d?= =?UTF-8?q?=C3=A9compresser=20l'archive=20=C3=A0=20l'endroit=20pr=C3=A9cis?= =?UTF-8?q?=C3=A9.=20Ce=20plugin=20re=C3=A7oit=20les=20segments=20de=20fic?= =?UTF-8?q?hiers=20transf=C3=A9r=C3=A9s=20via=20xmpp=20et=20les=20reconsti?= =?UTF-8?q?tue.=20Ensuite,=20il=20d=C3=A9compresse=20l'archive=20=C3=A0=20?= =?UTF-8?q?l'endroit=20pr=C3=A9cis=C3=A9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 6f28fbe4f957882653ebe6c677d505bb55879847) --- .../plugins_common/plugin_recombine_file.py | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 pulse_xmpp_agent/plugins_common/plugin_recombine_file.py diff --git a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py new file mode 100644 index 000000000..56e9a3e6f --- /dev/null +++ b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py @@ -0,0 +1,401 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2022-2023 Siveo +# SPDX-License-Identifier: GPL-3.0-or-later +# file plugin_recombine_file.py + +import logging +import json +import platform +import os +import traceback +import hashlib +from datetime import datetime +import zipfile +import uuid +import time +import re +import base64 +import shutil + +from lib.utils import set_logging_level + +plugin = {"VERSION": "1.0", "NAME": "recombine_file", "TYPE": "all"} # fmt: skip + +logger = logging.getLogger() +repertoire = "C:\Program Files\Pulse\var\zip_transfert" + +@set_logging_level +def action(objectxmpp, action, sessionid, data, message, dataerreur): + logger.debug("###################################################") + logger.debug( + "call %s from %s session id %s" % (plugin, message["from"], sessionid) + ) + logger.debug("###################################################") + + # verification des clefs + if not verifier_cles_non_vides(data, [ "namefile", + "directory", + "type", + "contenttype", + "segment", + "dir_uuid_machine", + "dir_segment"]): + logger.error("message error recombine_file" ) + return + # on veriy les types de transfert + if platform.system() in ['Linux', 'Darwin']: + if data['type'].lower().startswith("backup"): + directory_path = '/var/lib/pulse2/zip_transfert' + elif data['type'].lower().startswith("packages"): + directory_path = '/var/lib/pulse2/packages/' + elif data['type'].lower().startswith("location"): + if not verifier_cles_non_vides(data, ["location"]): + logger.error("type location. la location n'est pas transmise") + return + elif not chemin_valide( data["location"] ): + logger.error("location n'est pas 1 chemin correct %s" % data["location"] ) + return + directory_path = data['location'] + else: + logger.error("type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]") + tmp_dir = "/tmp" + elif platform.system() == 'Windows': + if data['type'].lower().startswith("backup"): + directory_path = r'C:/Program Files/Pulse/var/zip_transfert' + elif data['type'].lower().startswith("packages"): + directory_path = r'C:/Program Files/Pulse/var/tmp/packages' + elif data['type'].lower().startswith("location"): + if not verifier_cles_non_vides(data, ["location"]): + logger.error("type location. la location n'est pas transmise") + return + elif not chemin_valide( data["location"] ): + logger.error("location n'est pas 1 chemin correct %s" % data["location"] ) + return + directory_path = data['location'] + else: + logger.error("type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]") + return + tmp_dir = r"C:\Windows\Temp" + else: + logger.error("os inconue") + return + + repertoire_temporaire_reception = os.path.join(tmp_dir, data['dir_uuid_machine'], data['dir_segment'] ) + try: + if not os.path.exists(repertoire_temporaire_reception): + logger.debug("creation repertoire temporaire reception %s " % repertoire_temporaire_reception) + os.makedirs(repertoire_temporaire_reception, exist_ok=True) + + if not os.path.exists(directory_path): + logger.debug("creation %s " % directory_path) + os.makedirs(directory_path, exist_ok=True) + except Exception: + logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + + file_transfert = os.path.join( repertoire_temporaire_reception, data['namefile']) + if data['segment'] == 0: + logger.debug("DEBUT TRAITEMENT MANIFEST") + logger.debug("fichier %s" % json.dumps(data, indent=4 )) + # manifest + # creation des chemin + manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") + logger.debug("creation manifeste transfert %s" % manifest_file) + try: + with open(manifest_file, 'w') as json_file: + json.dump(data, json_file, indent=4) + except Exception as e: + logger.error(f"Erreur lors de l'écriture du manifest de transfert en JSON : {e}") + logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + # Création d'un fichier vide + try: + logger.debug("creation file transfer %s" % file_transfert) + with open( file_transfert, 'wb') as file: + pass # Ne fait rien, mais crée le fichier vide + except Exception as e: + logger.error(f"Erreur lors de l'écriture du fichier JSON : {e}") + logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + logger.debug("FIN TRAITEMENT MANIFEST") + else: + logger.debug("DEBUT TRAITEMENT SEGMENT") + if not verifier_cles_non_vides(data, ["nbtotal", + "content", + "nb", + "directory"]): + logger.error("message error TRAITEMENT SEGMENT" ) + return + try: + logger.debug("creation file transfer %s" % file_transfert) + + with open( file_transfert, 'ab') as file: + file.write(base64.b64decode(data['content']))# Décodage en Base64 + file_size_bytes = get_file_size(file_transfert) + if file_size_bytes is not None: + file_size_human = convert_size(file_size_bytes) + logger.debug(f"La taille du fichier est : {file_size_bytes} octets ({file_size_human})") + except Exception as e: + logger.error(f"Erreur lors de l'écriture du fichier JSON : {e}") + logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + + if data['nbtotal'] == data['segment']: + try: + md5 = md5_hash(file_transfert) + logger.error("md5 is %s" % md5 ) + # on charge le manifest pour controler le md5 + manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") + with open(manifest_file, 'r') as json_file: + data_manifest = json.load(json_file) + if data_manifest['md5'] == md5: + logger.debug("transfert archive reussi md5 correct") + else: + logger.error("le md5 n'est pas du transfert n'est pas correct") + supprimer_repertoire(repertoire_temporaire_reception) + return + except Exception: + logger.error("transfert terminer. 1 erreur dans le md5 %s" % traceback.format_exc()) + return + try: + # on recupere le repertoire ou decompresser l'archive + name_repertoire_reception = restaurer_caracteres(data['directory']) + name_repertoire_decompression = re.sub(r'^[A-Za-z]:', '', name_repertoire_reception) + name_repertoire_decompression1 = re.sub(r'\.zip$', '', name_repertoire_decompression) + if data['type'].lower() in ['backup','packages']: + name_repertoire_decomp = directory_path.replace("\\",'/') + name_repertoire_decompression1 + else: + name_repertoire_decomp = directory_path + logger.error("directory_path %s" % directory_path) + + if data['contenttype'].lower().startswith('file'): + name_repertoire_decomp = os.path.dirname(name_repertoire_decomp) + + logger.error("il faut faire le traitement de decompression to %s" % name_repertoire_decomp) + + # on cree le repertoire si non exist + if not os.path.exists(name_repertoire_decomp): + logger.debug("creation %s " % name_repertoire_decomp) + os.makedirs(name_repertoire_decomp, exist_ok=True) + logger.error("decompresser_archive %s"%(file_transfert) ) + decompresser_archive(file_transfert, name_repertoire_decomp) + logger.error("et on remet a zero le fichier" ) + # with open( file_transfert, 'wb') as file: + # pass + logger.error("et on remet a zero le fichier" ) + supprimer_repertoire(repertoire_temporaire_reception) + except Exception: + logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + +def md5_hash(file_path): + """Calcule le hash MD5 d'un fichier.""" + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + +def decompresser_archive(fichier_zip, repertoire_destination): + # Vérifier si le fichier ZIP existe + if not os.path.exists(fichier_zip): + raise FileNotFoundError(f"Le fichier ZIP {fichier_zip} n'existe pas.") + + # Créer le répertoire de destination s'il n'existe pas déjà + if not os.path.exists(repertoire_destination): + os.makedirs(repertoire_destination) + + # Ouvrir et extraire tout le contenu du fichier ZIP + with zipfile.ZipFile(fichier_zip, 'r') as zip_ref: + zip_ref.extractall(repertoire_destination) + +def check_and_create_directory_backup(data): + """ + Vérifie le type de transfert de fichiers (backup, package ou location), et crée + le répertoire approprié s'il n'existe pas encore, en fonction du système d'exploitation. + + Args: + data (dict): Dictionnaire contenant les informations sur le type et l'emplacement du transfert. + Clés attendues : + - 'type': Type de transfert (backup ou package). + - 'location': Chemin spécifique de l'emplacement (facultatif). + + Returns: + str: Chemin du répertoire créé ou existant, ou None si aucune action n'est effectuée. + + Raises: + None: Ne lève pas d'exception mais journalise les erreurs si le système d'exploitation est non supporté + ou si le chemin fourni est incorrect. + """ + directory_path = None + tmp_dir = None + logger.debug("START check_and_create_directory_backup") + # Vérification du type de donnée + # if not verifier_cles_non_vides(data, ['type', 'contenttype']): + # logger.error("Une ou plusieurs clés sont manquantes ou vides.") + # return None, None + + # Cas des backups + if data['type'].lower().startswith("backup"): + logger.debug("START iiii") + return get_directory_path_for_os('/var/lib/pulse2/zip_transfert', r'C:\Program Files\Pulse\var\zip_transfert') + # Cas des packages + elif data['type'].lower().startswith("package"): + logger.debug("START BBBB") + directory_path, tmp_dir = get_directory_path_for_os('/var/lib/pulse2/packages', r'C:\Program Files\Pulse\var\tmp\packages') + # Cas d'un emplacement personnalisé + elif "location" in data and data['location']: + logger.debug("START VVVVV") + directory_path, tmp_dir = validate_and_get_custom_location(data['location']) + + # Création du répertoire si valide + if directory_path: + if not os.path.exists(directory_path): + os.makedirs(directory_path, exist_ok=True) + return directory_path, tmp_dir + + +def get_directory_path_for_os(linux_path, windows_path): + """ + Retourne le chemin du répertoire en fonction du système d'exploitation. + Retourne également le répertoire des fichiers temporaires. + + Args: + linux_path (str): Chemin du répertoire pour les systèmes basés sur Unix. + windows_path (str): Chemin du répertoire pour Windows. + + Returns: + tuple: Un tuple contenant le chemin du répertoire approprié et le chemin du répertoire temporaire, + ou (None, None) si le système d'exploitation n'est pas supporté. + """ + logger.debug("START get_directory_path_for_os") + if platform.system() in ['Linux', 'Darwin']: + logger.debug("Linux or macOS detected") + return linux_path, "/tmp" + elif platform.system() == 'Windows': + logger.debug("Windows detected") + return windows_path, + else: + logger.error("Unsupported operating system") + return None, None + +def validate_and_get_custom_location(location): + """ + Valide et retourne le chemin personnalisé en fonction du système d'exploitation. + + Args: + location (str): Chemin personnalisé. + + Returns: + str: Chemin personnalisé validé ou None si le chemin est invalide. + """ + if platform.system() in ['Linux', 'Darwin']: + if location.startswith("/var/lib/pulse2"): + return location, "/tmp" + else: + logger.error("Invalid path for Linux/MacOS") + elif platform.system() == 'Windows': + if location.startswith(r"C:\Program Files\Pulse\var"): + return location, "C:\\Windows\\Temp" + else: + logger.error("Invalid path for Windows") + else: + logger.error("Unsupported operating system for location") + return None, None + +def verifier_cles_non_vides(data, cles): + """ + Vérifie si toutes les clés spécifiées existent dans le dictionnaire + et si leurs valeurs ne sont pas None ou vides. + + Args: + data (dict): Le dictionnaire à vérifier. + cles (list): Une liste de clés à vérifier dans le dictionnaire. + + Returns: + bool: True si toutes les clés existent et leurs valeurs ne sont pas vides, sinon False. + """ + logger.debug("verifier_cles_non_vides") + for cle in cles: + if cle not in data: # Vérifie l'existence et la valeur non vide + logger.error("cle missing %s" % cle ) + return False + if isinstance(data[cle], int): + continue + if not data[cle]: + return False + return True + +def get_file_size(file_path): + """ + Retourne la taille d'un fichier en octets. + + Args: + file_path (str): Chemin du fichier. + + Returns: + int: Taille du fichier en octets. + """ + try: + size = os.path.getsize(file_path) + return size + except Exception as e: + print(f"Erreur lors de la récupération de la taille du fichier : {e}") + return None + +def convert_size(size_bytes): + """ + Convertit une taille en octets en une taille lisible par un humain. + + Args: + size_bytes (int): Taille en octets. + + Returns: + str: Taille lisible par un humain. + """ + if size_bytes is None: + return "Taille inconnue" + + for unit in ['octets', 'Ko', 'Mo', 'Go', 'To']: + if size_bytes < 1024: + return f"{size_bytes:.2f} {unit}" + size_bytes /= 1024 + +def remplacer_caracteres(chaine): + # Remplacer \\ par @@ + chaine = chaine.replace('\\', '@@') + # Remplacer les espaces par @nbsp@ + chaine = chaine.replace(' ', '@nbsp@') + # Remplacer les barres obliques / par / + chaine = chaine.replace('/', '@47@') + # Remplacer les barres obliques \ par \ + chaine = chaine.replace('\\', '@92@') + # Remplacer . par \ + chaine = chaine.replace('.', '@46@') + return chaine + +def restaurer_caracteres(chaine): + # Remplacer @58@ par @ + chaine = chaine.replace('@58@', ':') + # Remplacer @46@ par . + chaine = chaine.replace('@46@', '.') + # Remplacer @92@ par \ + chaine = chaine.replace('@92@', '\\') + # Remplacer @47@ par / + chaine = chaine.replace('@47@', '/') + # Remplacer @nbsp@ par espace + chaine = chaine.replace('@nbsp@', ' ') + return chaine + + +def chemin_valide(chemin): + try: + # Utilise os.path.normpath pour normaliser le chemin + # et os.path.abspath pour obtenir un chemin absolu + chemin_normalise = os.path.abspath(chemin) + return True + except Exception as e: + return False + + +def supprimer_repertoire(repertoire): + if os.path.exists(repertoire): + shutil.rmtree(repertoire) + logger.debug(f"Le répertoire '{repertoire}' et tout son contenu ont été supprimés.") + else: + logger.debug(f"Le répertoire '{repertoire}' n'existe pas.") From 2a034df780b57424fb967e5b226f1f505a8d2f07 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Mon, 14 Oct 2024 18:05:37 +0200 Subject: [PATCH 11/13] [agent recombine] ajout de commentaire. (cherry picked from commit bf06c59c70efbda205d4da0fbc5247a17061f72f) --- pulse_xmpp_agent/plugins_common/plugin_recombine_file.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py index 56e9a3e6f..4d5db02d5 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py +++ b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py @@ -3,6 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # file plugin_recombine_file.py +# ce plugin recois les segment de fichier transferer et les reconstitut. +# puis il decompresse l'archive a l'endroit preciser. +# +# import logging import json import platform From 29403c5e9a8d40a052b4e2b2e66343274bc34cf6 Mon Sep 17 00:00:00 2001 From: jfkneib Date: Thu, 17 Oct 2024 11:38:00 +0200 Subject: [PATCH 12/13] [agent machine, agent substitut] transfert xmpp pluging agent pluging recombine_file plugin_backup_restore.py pluging substitut plugin_backup_restore_substitut.py plugin_load_send_Segment_file.py plugin_send_file_xmpp.py (cherry picked from commit 4e1b1d5c2ac3d43737809be3669571872cebd6f6) --- .../plugins_common/plugin_recombine_file.py | 327 ++++++------ .../lib/plugins/xmpp/__init__.py | 115 +++++ .../plugin_backup_restore_substitut.py | 38 +- .../plugin_load_send_Segment_file.py | 17 +- .../plugin_send_file_xmpp.py | 485 ++++++++++++++++++ 5 files changed, 810 insertions(+), 172 deletions(-) create mode 100644 pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py diff --git a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py index 4d5db02d5..6773250cc 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py +++ b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py @@ -3,10 +3,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later # file plugin_recombine_file.py -# ce plugin recois les segment de fichier transferer et les reconstitut. -# puis il decompresse l'archive a l'endroit preciser. -# -# import logging import json import platform @@ -26,7 +22,7 @@ plugin = {"VERSION": "1.0", "NAME": "recombine_file", "TYPE": "all"} # fmt: skip logger = logging.getLogger() -repertoire = "C:\Program Files\Pulse\var\zip_transfert" + @set_logging_level def action(objectxmpp, action, sessionid, data, message, dataerreur): @@ -35,6 +31,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): "call %s from %s session id %s" % (plugin, message["from"], sessionid) ) logger.debug("###################################################") + logger.debug("json entree %s" % json.dumps(data, indent=4 )) # verification des clefs if not verifier_cles_non_vides(data, [ "namefile", @@ -46,6 +43,16 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): "dir_segment"]): logger.error("message error recombine_file" ) return + + if data["contenttype"].lower().startswith("package"): + package = data["location"] + data['type'] = "location" + data["contenttype"] = "directory" + if platform.system() in ['Linux', 'Darwin']: + data['location'] = os.path.join( "/var/lib/pulse2/packages", package) + elif platform.system() == 'Windows': + data['location'] = os.path.join(r'C:\Program Files\Pulse\var\tmp\packages', package ) + # on veriy les types de transfert if platform.system() in ['Linux', 'Darwin']: if data['type'].lower().startswith("backup"): @@ -59,15 +66,17 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): elif not chemin_valide( data["location"] ): logger.error("location n'est pas 1 chemin correct %s" % data["location"] ) return - directory_path = data['location'] + directory_path = os.path.dirname(data['location']) + namefileresult=os.path.basename(data['location']) else: logger.error("type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]") + return tmp_dir = "/tmp" elif platform.system() == 'Windows': if data['type'].lower().startswith("backup"): - directory_path = r'C:/Program Files/Pulse/var/zip_transfert' + directory_path = r'C:\Program Files\Pulse\var\zip_transfert' elif data['type'].lower().startswith("packages"): - directory_path = r'C:/Program Files/Pulse/var/tmp/packages' + directory_path = r'C:\Program Files\Pulse\var\tmp\packages' elif data['type'].lower().startswith("location"): if not verifier_cles_non_vides(data, ["location"]): logger.error("type location. la location n'est pas transmise") @@ -75,7 +84,14 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): elif not chemin_valide( data["location"] ): logger.error("location n'est pas 1 chemin correct %s" % data["location"] ) return - directory_path = data['location'] + if data['contenttype'].lower().startswith("file") : + # on recuperer le repertoire ou le fichier ou doit se trouver + # quand c'est 1 fichier location doit contenir le path et le nom de fichier de reception + directory_path = os.path.dirname(data['location'].replace("/","\\")) + namefileresult=os.path.basename(data['location'].replace("/","\\")) + directory_path = directory_path.replace("\\","/") + else: + directory_path = data['location'] else: logger.error("type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]") return @@ -84,43 +100,56 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): logger.error("os inconue") return - repertoire_temporaire_reception = os.path.join(tmp_dir, data['dir_uuid_machine'], data['dir_segment'] ) + if data['type'].lower().startswith('location'): + repertoire_temporaire_reception = os.path.join(tmp_dir, data['dir_uuid_machine'] ) + else: + repertoire_temporaire_reception = os.path.join(tmp_dir, data['dir_uuid_machine'], data['dir_segment'] ) + try: if not os.path.exists(repertoire_temporaire_reception): - logger.debug("creation repertoire temporaire reception %s " % repertoire_temporaire_reception) + # logger.debug("creation repertoire temporaire reception %s " % repertoire_temporaire_reception) os.makedirs(repertoire_temporaire_reception, exist_ok=True) if not os.path.exists(directory_path): - logger.debug("creation %s " % directory_path) + # logger.debug("creation %s " % directory_path) os.makedirs(directory_path, exist_ok=True) except Exception: logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + # logger.error(f"repertoire_temporaire_reception: {repertoire_temporaire_reception}") + # logger.error(f"directory_path : {directory_path}" ) + + file_transfert = os.path.join( repertoire_temporaire_reception, data['namefile']) + if data['segment'] == 0: - logger.debug("DEBUT TRAITEMENT MANIFEST") - logger.debug("fichier %s" % json.dumps(data, indent=4 )) + # logger.debug("DEBUT TRAITEMENT MANIFEST") + # logger.debug("fichier %s" % json.dumps(data, indent=4 )) # manifest # creation des chemin manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") - logger.debug("creation manifeste transfert %s" % manifest_file) + # logger.debug("creation manifeste transfert %s" % manifest_file) try: with open(manifest_file, 'w') as json_file: json.dump(data, json_file, indent=4) except Exception as e: logger.error(f"Erreur lors de l'écriture du manifest de transfert en JSON : {e}") logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + # logger.debug("CREATION FICHIER MANIFESTE %s " % manifest_file ) + + # Création d'un fichier vide try: - logger.debug("creation file transfer %s" % file_transfert) + # logger.debug("creation file transfer %s" % file_transfert) with open( file_transfert, 'wb') as file: pass # Ne fait rien, mais crée le fichier vide except Exception as e: logger.error(f"Erreur lors de l'écriture du fichier JSON : {e}") logger.error("We hit the backtrace \n %s" % traceback.format_exc()) - logger.debug("FIN TRAITEMENT MANIFEST") + # logger.debug("FIN TRAITEMENT MANIFEST") else: - logger.debug("DEBUT TRAITEMENT SEGMENT") + # logger.debug("DEBUT TRAITEMENT SEGMENT") + if not verifier_cles_non_vides(data, ["nbtotal", "content", "nb", @@ -128,7 +157,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): logger.error("message error TRAITEMENT SEGMENT" ) return try: - logger.debug("creation file transfer %s" % file_transfert) + # logger.debug("creation file transfer %s" % file_transfert) with open( file_transfert, 'ab') as file: file.write(base64.b64decode(data['content']))# Décodage en Base64 @@ -143,7 +172,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): if data['nbtotal'] == data['segment']: try: md5 = md5_hash(file_transfert) - logger.error("md5 is %s" % md5 ) + # logger.error("md5 is %s" % md5 ) # on charge le manifest pour controler le md5 manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") with open(manifest_file, 'r') as json_file: @@ -151,156 +180,122 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): if data_manifest['md5'] == md5: logger.debug("transfert archive reussi md5 correct") else: - logger.error("le md5 n'est pas du transfert n'est pas correct") + logger.error("le md5 n'est pas du transfert n'est pas correct abandon et netoyage") supprimer_repertoire(repertoire_temporaire_reception) return except Exception: logger.error("transfert terminer. 1 erreur dans le md5 %s" % traceback.format_exc()) return try: + # if data['type'].lower().startswith('location + # on recupere le repertoire ou decompresser l'archive name_repertoire_reception = restaurer_caracteres(data['directory']) name_repertoire_decompression = re.sub(r'^[A-Za-z]:', '', name_repertoire_reception) name_repertoire_decompression1 = re.sub(r'\.zip$', '', name_repertoire_decompression) if data['type'].lower() in ['backup','packages']: name_repertoire_decomp = directory_path.replace("\\",'/') + name_repertoire_decompression1 + if data['contenttype'].lower().startswith('file'): + name_repertoire_decomp = os.path.dirname(name_repertoire_decomp) else: name_repertoire_decomp = directory_path - logger.error("directory_path %s" % directory_path) - - if data['contenttype'].lower().startswith('file'): - name_repertoire_decomp = os.path.dirname(name_repertoire_decomp) - - logger.error("il faut faire le traitement de decompression to %s" % name_repertoire_decomp) + # logger.debug("directory_path %s" % directory_path) + # logger.debug("il faut faire le traitement de decompression to %s" % name_repertoire_decomp) # on cree le repertoire si non exist if not os.path.exists(name_repertoire_decomp): - logger.debug("creation %s " % name_repertoire_decomp) + # logger.debug("creation %s " % name_repertoire_decomp) os.makedirs(name_repertoire_decomp, exist_ok=True) - logger.error("decompresser_archive %s"%(file_transfert) ) - decompresser_archive(file_transfert, name_repertoire_decomp) - logger.error("et on remet a zero le fichier" ) + # logger.debug("decompresser_archive file zip %s"%(file_transfert) ) + # logger.debug("decompresser_archive vers repertoire %s"%(name_repertoire_decomp) ) + + + if data['contenttype'].lower().startswith('file') and 'location' in data and data['location']: + # logger.debug("nouveau_nom file %s"%(namefileresult) ) + decompresser_archive(file_transfert, name_repertoire_decomp, nouveau_nom=namefileresult) + else: + decompresser_archive(file_transfert, name_repertoire_decomp) + # logger.error("et on remet a zero le fichier" ) # with open( file_transfert, 'wb') as file: # pass - logger.error("et on remet a zero le fichier" ) + logger.debug("nettoyage repertoire de travail" ) supprimer_repertoire(repertoire_temporaire_reception) except Exception: logger.error("We hit the backtrace \n %s" % traceback.format_exc()) def md5_hash(file_path): - """Calcule le hash MD5 d'un fichier.""" + """ + Calcule le hash MD5 d'un fichier. + + Paramètre: + file_path (str): Le chemin du fichier pour lequel le hash MD5 doit être calculé. + + Retourne: + str: Le hash MD5 sous forme de chaîne hexadécimale. + """ hash_md5 = hashlib.md5() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() -def decompresser_archive(fichier_zip, repertoire_destination): - # Vérifier si le fichier ZIP existe - if not os.path.exists(fichier_zip): - raise FileNotFoundError(f"Le fichier ZIP {fichier_zip} n'existe pas.") - - # Créer le répertoire de destination s'il n'existe pas déjà - if not os.path.exists(repertoire_destination): - os.makedirs(repertoire_destination) - - # Ouvrir et extraire tout le contenu du fichier ZIP - with zipfile.ZipFile(fichier_zip, 'r') as zip_ref: - zip_ref.extractall(repertoire_destination) - -def check_and_create_directory_backup(data): - """ - Vérifie le type de transfert de fichiers (backup, package ou location), et crée - le répertoire approprié s'il n'existe pas encore, en fonction du système d'exploitation. - - Args: - data (dict): Dictionnaire contenant les informations sur le type et l'emplacement du transfert. - Clés attendues : - - 'type': Type de transfert (backup ou package). - - 'location': Chemin spécifique de l'emplacement (facultatif). - - Returns: - str: Chemin du répertoire créé ou existant, ou None si aucune action n'est effectuée. - - Raises: - None: Ne lève pas d'exception mais journalise les erreurs si le système d'exploitation est non supporté - ou si le chemin fourni est incorrect. +def decompresser_archive(fichier_zip, repertoire_destination, nouveau_nom=None): """ - directory_path = None - tmp_dir = None - logger.debug("START check_and_create_directory_backup") - # Vérification du type de donnée - # if not verifier_cles_non_vides(data, ['type', 'contenttype']): - # logger.error("Une ou plusieurs clés sont manquantes ou vides.") - # return None, None - - # Cas des backups - if data['type'].lower().startswith("backup"): - logger.debug("START iiii") - return get_directory_path_for_os('/var/lib/pulse2/zip_transfert', r'C:\Program Files\Pulse\var\zip_transfert') - # Cas des packages - elif data['type'].lower().startswith("package"): - logger.debug("START BBBB") - directory_path, tmp_dir = get_directory_path_for_os('/var/lib/pulse2/packages', r'C:\Program Files\Pulse\var\tmp\packages') - # Cas d'un emplacement personnalisé - elif "location" in data and data['location']: - logger.debug("START VVVVV") - directory_path, tmp_dir = validate_and_get_custom_location(data['location']) - - # Création du répertoire si valide - if directory_path: - if not os.path.exists(directory_path): - os.makedirs(directory_path, exist_ok=True) - return directory_path, tmp_dir + Décompresse un fichier ZIP dans un répertoire spécifié. Si un nouveau nom est fourni, + le fichier extrait est renommé. + Paramètres: + fichier_zip (str): Le chemin du fichier ZIP à décompresser. + repertoire_destination (str): Le répertoire dans lequel extraire les fichiers. + nouveau_nom (str, optionnel): Nouveau nom pour le fichier extrait. Si défini, l'archive + doit contenir exactement un fichier. -def get_directory_path_for_os(linux_path, windows_path): - """ - Retourne le chemin du répertoire en fonction du système d'exploitation. - Retourne également le répertoire des fichiers temporaires. - - Args: - linux_path (str): Chemin du répertoire pour les systèmes basés sur Unix. - windows_path (str): Chemin du répertoire pour Windows. - - Returns: - tuple: Un tuple contenant le chemin du répertoire approprié et le chemin du répertoire temporaire, - ou (None, None) si le système d'exploitation n'est pas supporté. - """ - logger.debug("START get_directory_path_for_os") - if platform.system() in ['Linux', 'Darwin']: - logger.debug("Linux or macOS detected") - return linux_path, "/tmp" - elif platform.system() == 'Windows': - logger.debug("Windows detected") - return windows_path, - else: - logger.error("Unsupported operating system") - return None, None + Retourne: + bool: Retourne True si la décompression s'est effectuée avec succès, False sinon. -def validate_and_get_custom_location(location): + Exceptions: + FileNotFoundError: Si le fichier ZIP n'existe pas. + ValueError: Si l'archive contient plus d'un fichier lorsque nouveau_nom est spécifié. """ - Valide et retourne le chemin personnalisé en fonction du système d'exploitation. - - Args: - location (str): Chemin personnalisé. + try: + # Vérifier si le fichier ZIP existe + if not os.path.exists(fichier_zip): + raise FileNotFoundError(f"Le fichier ZIP {fichier_zip} n'existe pas.") + + # Créer le répertoire de destination s'il n'existe pas déjà + if not os.path.exists(repertoire_destination): + os.makedirs(repertoire_destination) + + # Ouvrir le fichier ZIP + with zipfile.ZipFile(fichier_zip, 'r') as zip_ref: + # Vérifier si nouveau_nom est défini + if nouveau_nom: + # Vérifier que l'archive contient exactement un fichier + noms_fichiers = zip_ref.namelist() + if len(noms_fichiers) != 1: + raise ValueError("L'archive ZIP doit contenir exactement un fichier.") + + # Extraire le fichier + nom_fichier = noms_fichiers[0] + zip_ref.extract(nom_fichier, repertoire_destination) + + # Renommer le fichier extrait + chemin_fichier_extrait = os.path.join(repertoire_destination, nom_fichier) + chemin_fichier_renomme = os.path.join(repertoire_destination, nouveau_nom) + + # Vérifier si le fichier de destination existe déjà + if os.path.exists(chemin_fichier_renomme): + os.remove(chemin_fichier_renomme) # Supprimer le fichier existant + + os.rename(chemin_fichier_extrait, chemin_fichier_renomme) + else: + # Extraire tout le contenu du fichier ZIP + zip_ref.extractall(repertoire_destination) - Returns: - str: Chemin personnalisé validé ou None si le chemin est invalide. - """ - if platform.system() in ['Linux', 'Darwin']: - if location.startswith("/var/lib/pulse2"): - return location, "/tmp" - else: - logger.error("Invalid path for Linux/MacOS") - elif platform.system() == 'Windows': - if location.startswith(r"C:\Program Files\Pulse\var"): - return location, "C:\\Windows\\Temp" - else: - logger.error("Invalid path for Windows") - else: - logger.error("Unsupported operating system for location") - return None, None + return True + except Exception as e: + logger.error(f"Erreur lors de la décompression de l'archive : {e}") + return False def verifier_cles_non_vides(data, cles): """ @@ -339,7 +334,7 @@ def get_file_size(file_path): size = os.path.getsize(file_path) return size except Exception as e: - print(f"Erreur lors de la récupération de la taille du fichier : {e}") + logger.error(f"Erreur lors de la récupération de la taille du fichier : {e}") return None def convert_size(size_bytes): @@ -361,33 +356,64 @@ def convert_size(size_bytes): size_bytes /= 1024 def remplacer_caracteres(chaine): - # Remplacer \\ par @@ + """ + Remplace certains caractères spéciaux dans une chaîne par des codes spécifiques. + + Paramètre: + chaine (str): La chaîne à modifier. + + Remplacements effectués: + - '\\' est remplacé par '@@'. + - Les espaces sont remplacés par '@nbsp@'. + - '/' est remplacé par '@47@'. + - '\\' est remplacé par '@92@'. + - '.' est remplacé par '@46@'. + + Retourne: + str: La chaîne modifiée avec les caractères remplacés. + """ chaine = chaine.replace('\\', '@@') - # Remplacer les espaces par @nbsp@ chaine = chaine.replace(' ', '@nbsp@') - # Remplacer les barres obliques / par / chaine = chaine.replace('/', '@47@') - # Remplacer les barres obliques \ par \ chaine = chaine.replace('\\', '@92@') - # Remplacer . par \ chaine = chaine.replace('.', '@46@') return chaine def restaurer_caracteres(chaine): - # Remplacer @58@ par @ + """ + Restaure les caractères spéciaux dans une chaîne modifiée. + + Paramètre: + chaine (str): La chaîne contenant les codes à restaurer. + + Remplacements effectués: + - '@58@' est remplacé par ':'. + - '@46@' est remplacé par '.'. + - '@92@' est remplacé par '\\'. + - '@47@' est remplacé par '/'. + - '@nbsp@' est remplacé par un espace. + + Retourne: + str: La chaîne restaurée avec les caractères originaux. + """ chaine = chaine.replace('@58@', ':') - # Remplacer @46@ par . chaine = chaine.replace('@46@', '.') - # Remplacer @92@ par \ chaine = chaine.replace('@92@', '\\') - # Remplacer @47@ par / chaine = chaine.replace('@47@', '/') - # Remplacer @nbsp@ par espace chaine = chaine.replace('@nbsp@', ' ') return chaine def chemin_valide(chemin): + """ + Vérifie si un chemin est valide en essayant de le normaliser et de l'obtenir en chemin absolu. + + Paramètre: + chemin (str): Le chemin à vérifier. + + Retourne: + bool: True si le chemin est valide, False sinon. + """ try: # Utilise os.path.normpath pour normaliser le chemin # et os.path.abspath pour obtenir un chemin absolu @@ -398,6 +424,19 @@ def chemin_valide(chemin): def supprimer_repertoire(repertoire): + """ + Supprime un répertoire ainsi que tout son contenu. + + Paramètre: + repertoire (str): Le chemin du répertoire à supprimer. + + Retourne: + None + + Actions: + - Si le répertoire existe, il est supprimé avec tout son contenu. + - Si le répertoire n'existe pas, un message de debug est enregistré dans le logger. + """ if os.path.exists(repertoire): shutil.rmtree(repertoire) logger.debug(f"Le répertoire '{repertoire}' et tout son contenu ont été supprimés.") diff --git a/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py b/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py index e1b68fc84..e7c06e84f 100644 --- a/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py +++ b/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py @@ -18,6 +18,8 @@ distinct, not_, delete, + Float, + text, ) from sqlalchemy.orm import sessionmaker, Query from sqlalchemy.exc import DBAPIError, NoSuchTableError, IntegrityError @@ -11716,3 +11718,116 @@ def get_os_xmpp_update_major_details( def _return_dict_from_dataset_mysql(self, resultproxy): return [rowproxy._asdict() for rowproxy in resultproxy] + + # ------------------------------------------------------------------------------- + + + @DatabaseHelper._sessionm + def search_machine(self, session, search_term): + """ + Recherche une machine qui correspond à un id, uuid_inventorymachine, jid ou serialnumber. + + Paramètres : + session (Session) : La session SQLAlchemy. + search_term (str) : Le terme de recherche. + + Retourne : + dict : Les informations de la machine trouvée. + """ + if not search_term: + return {} + # Requête SQL + query = text(""" + SELECT + id, + uuid_inventorymachine, + SUBSTRING_INDEX(jid, '/', 1) AS jid, + uuid_serial_machine, + hostname, + enabled + FROM + xmppmaster.machines + WHERE + uuid_serial_machine = :search_term + OR hostname = SUBSTRING_INDEX(SUBSTRING_INDEX(:search_term, '@', 1), '.', 1) + OR uuid_inventorymachine = :search_term + OR id = :search_term + OR jid =:search_term + LIMIT 1; + """) + logger.error("search_machine query %s" % query) + logger.error("search_machine search_term %s" % search_term) + # Exécution de la requête + result = session.execute(query, {'search_term': search_term}).fetchone() + + # Si aucun résultat n'est trouvé, retourner un dictionnaire vide + if result is None: + return {} + + # Construire le dictionnaire de résultats + result_machines = { + 'hostname': result['hostname'], + 'jid': result['jid'], + 'uuid_inventorymachine': result['uuid_inventorymachine'], + 'uuid_serial_machine': result['uuid_serial_machine'], + 'id': int(result['id']), + 'enabled': int(result['enabled']) + } + + return result_machines + + @DatabaseHelper._sessionm + def network_list_machine(self, session, id_machine): + """ + Renvoie la liste des réseaux associés à une machine spécifique. + + Paramètres : + session (Session) : La session SQLAlchemy. + id_machine (int) : L'identifiant de la machine. + + Retourne : + list : La liste des informations de réseau associées à la machine. + """ + if not id_machine: + return [] + + # Requête SQL + query = text(""" + SELECT + id, + macaddress, + ipaddress, + broadcast, + gateway, + mask, + mac, + machines_id + FROM + xmppmaster.network + WHERE + machines_id = :id_machine; + """) + + # Exécution de la requête + result = session.execute(query, {'id_machine': id_machine}).fetchall() + + # Si aucun résultat n'est trouvé, retourner une liste vide + if not result: + return [] + + # Construire la liste de résultats + network_list = [] + for row in result: + network_info = { + 'id': row['id'], + 'macaddress': row['macaddress'], + 'ipaddress': row['ipaddress'], + 'broadcast': row['broadcast'], + 'gateway': row['gateway'], + 'mask': row['mask'], + 'mac': row['mac'], + 'machines_id': row['machines_id'] + } + network_list.append(network_info) + + return network_list diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py index 075135623..e34ef164c 100644 --- a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py @@ -38,7 +38,7 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): logger.debug("data %s" % json.dumps(data['data'], indent=4)) try: datamsg = data['data'] - logger.debug("datamsg %s" % datamsg.keys()) + # logger.debug("datamsg %s" % datamsg.keys()) # Spécifier le chemin où créer le répertoire ZIP var_file_zipzer = "/var/lib/pulse2/zip_transfert" @@ -48,18 +48,18 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): # Traitement des répertoires à zipper if 'directorylist' in datamsg and datamsg['directorylist']: for directory in datamsg['directorylist']: - logger.debug("creation d'un fichier zip depuis 1 repertoire : %s" % directory) + # logger.debug("creation d'un fichier zip depuis 1 repertoire : %s" % directory) if directory: repertoire_zip = os.path.join(datamsg['base_path'], directory[0]) - logger.info("repertoire_zip %s" % repertoire_zip) + # logger.info("repertoire_zip %s" % repertoire_zip) name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], os.path.join(directory[1])) archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' - logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) + # logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) - logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) + # logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) if not zipper_repertoire(repertoire_zip, path_archive_fichier_name_zip): - logger.info("demande de compression dun repertoire inexistant") + logger.error("demande de compression dun repertoire inexistant") continue # Exemple d'utilisation manager = ZipFileManager(var_file_zipzer) @@ -69,19 +69,19 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): # Traitement des fichiers à zipper if 'filelist' in datamsg and datamsg['filelist']: for files in datamsg['filelist']: - logger.debug("creation d'un fichier zip depuis 1 fichier : %s" % files) + # logger.debug("creation d'un fichier zip depuis 1 fichier : %s" % files) if files: input_file = os.path.join(datamsg['base_path'], files[0]) - logger.info("imput file a zipper %s" % input_file) + # logger.info("imput file a zipper %s" % input_file) name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], os.path.join(files[1])) archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' - logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) + # logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) - logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) + # logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) if not zipper_fichier(input_file, path_archive_fichier_name_zip, fichier_vide=True): - logger.info("demande de compression dun fichier inexistant") + logger.error("demande de compression dun fichier inexistant") continue # Exemple d'utilisation manager = ZipFileManager(var_file_zipzer) @@ -133,10 +133,10 @@ def process_zip_files(input_dir, location, contenttype) - logger.debug(f"Fichier {file_name} découpé et enregistré dans {output_dir}") + # logger.debug(f"Fichier {file_name} découpé et enregistré dans {output_dir}") try: os.remove(file_path) - logger.debug(f"Le fichier {file_name} a été effacé avec succès.") + # logger.debug(f"Le fichier {file_name} a été effacé avec succès.") return True except FileNotFoundError: logger.error(f"Le fichier {file_name} n'existe pas.") @@ -181,7 +181,7 @@ def split_file(file_path, # Prendre seulement la 3e partie directory_or_file = parties[2] except ValueError as ve: - logger.error("pas 1 fichier zip") + logger.error("c'est pas 1 fichier zip") return None # Calculer le hash MD5 du fichier original @@ -205,8 +205,10 @@ def split_file(file_path, "location": location, "contenttype": contenttype } + # Formater le numéro de segment avec un format spécifique + segment_formatted = "{:06d}".format(part_num+1) - part_file_name = f"{file_uuid}_{part_num+1}.json" + part_file_name = f"{file_uuid}_{segment_formatted}.json" part_file_path = os.path.join(output_dir, part_file_name) with open(part_file_path, "w") as part_file: @@ -234,7 +236,7 @@ def split_file(file_path, with open(manifest_file_path, "w") as manifest_file: json.dump(manifest_data, manifest_file) - logger.debug(f"Fichier découpé en {total_parts} parties et manifest généré.") + # logger.debug(f"Fichier découpé en {total_parts} parties et manifest généré.") def recombine_file(manifest_path, input_dir, output_file): """Recompose le fichier original à partir des segments JSON et vérifie le hash MD5.""" @@ -265,7 +267,7 @@ def recombine_file(manifest_path, input_dir, output_file): if recombined_md5 == original_md5: logger.debug(f"Fichier recomposé avec succès, MD5 vérifié : {recombined_md5}") else: - logger.debug(f"Erreur : le MD5 recomposé {recombined_md5} ne correspond pas à l'original {original_md5}.") + logger.error(f"Erreur : le MD5 recomposé {recombined_md5} ne correspond pas à l'original {original_md5}.") def check_manifests(directory): """Vérifie tous les manifestes dans un répertoire et retourne deux listes : @@ -456,7 +458,7 @@ def delete_file(self, file): """ file_path = os.path.join(self.directory, file) os.remove(file_path) - logger.debug(f"Supprimé : {file_path}") + # logger.debug(f"Supprimé : {file_path}") def zipper_fichier(fichier, fichier_zip, fichier_vide=True): """ diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py index ffa632d10..831916481 100644 --- a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py @@ -57,9 +57,10 @@ def action(objectxmpp, action, sessionid, data, msg, dataerreur): # Installer le code dynamique : fonction de transfert de fichier objectxmpp.transfert_segment_file = types.MethodType(transfert_segment_file, objectxmpp) # Planifier l'appel de cette fonction + scedule_call_plugin_in_seconde = 15 objectxmpp.schedule( "transfert_segment_file", - 15, + scedule_call_plugin_in_seconde, objectxmpp.transfert_segment_file, repeat=True, ) @@ -67,7 +68,7 @@ def action(objectxmpp, action, sessionid, data, msg, dataerreur): def transfert_segment_file(self): """ Fonction de transfert de fichiers vers les machines présentes. - + cette fonction est scheduler a Args: self (object): L'objet XMPP. @@ -123,21 +124,19 @@ def transfert_segment_file(self): else: continue - a = 0 + indexfile = 0 for filesend in file_list_to_send: # logger.debug("*********************************") # logger.debug("filesend %s " % (filesend)) # logger.debug("*********************************") - if a == 0: + if indexfile == 0: data = manifeste - # logger.debug("a=0 *********************************%s" % data) else: data = lire_fichier_json(filesend) data['namefile'] = manifeste['namefile'] - # logger.debug("a=1 *********************************%s" % data) - data['segment'] = a + data['segment'] = indexfile data['dir_uuid_machine'] = dir_uuid_machine data['dir_segment'] = os.path.basename(repertoire_file_seg) msg_send = { @@ -146,13 +145,11 @@ def transfert_segment_file(self): "action": "recombine_file", "ret": 0 } - # logger.debug("msg_send: %s " % json.dumps(msg_send, indent=4)) - a = a + 1 + indexfile = indexfile + 1 # C'est la machine qui reçoit les segments qui doit recombiner le fichier. self.send_message(mto=machine['jid'], mbody=json.dumps(msg_send), mtype="chat") supprimer_repertoire(repertoire_file_seg) - supprimer_repertoire(os.path.join(var_file_zipper, dir_uuid_machine)) def supprimer_repertoire(chemin_repertoire): diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py new file mode 100644 index 000000000..618bfcbd1 --- /dev/null +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py @@ -0,0 +1,485 @@ +#!/usr/bin/python3 +# -*- coding: utf-8; -*- +# SPDX-FileCopyrightText: 2016-2023 Siveo +# SPDX-License-Identifier: GPL-3.0-or-later +# pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py + +# ce plugin est appelé pour envoye 1 fichier ou 1 repertoire sur 1 machine +# il a comme but de transferer des fichiers via xmpp +# il cree 1 archive zip, celle de ce que l'on veut transferer via xmpp. +# l'archive et decompose en morceau dans 1 repertoire /var/lib/pulse2/zip_transfert +# ou indique la destination sur la machine distante. + +""" +This plugin can be called from quick action +""" +import traceback +import json +import logging +import os +import hashlib +from datetime import datetime +import zipfile +import uuid +import time +import re +import base64 +from lib.plugins.xmpp import XmppMasterDatabase + +var_file_zipzer = "/var/lib/pulse2/zip_transfert" + +logger = logging.getLogger() + +plugin = {"VERSION": "1.0", "NAME": "send_file_xmpp", "TYPE": "substitut"} + +def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): + logger.debug("###################################################") + logger.debug("call %s from %s" % (plugin, message["from"])) + logger.debug("###################################################") + logger.debug("data %s" % json.dumps(data, indent=4)) + # Spécifier le chemin où créer le répertoire ZIP + # Créer le répertoire s'il n'existe pas + os.makedirs(var_file_zipzer, exist_ok=True) + + if not verifier_cles_non_vides(data, [ "machine_dest", + "path_fichier", + "install_machine_dest", + "contenttype"]): + logger.error("il manque des clefs dans le json d'entree") + return + try: + # datamsg=data['data'] + machine = XmppMasterDatabase().search_machine(data['machine_dest']) + if machine: + # logger.info("Machine destination %s" % json.dumps(machine, indent=4)) + if data['contenttype'].lower().startswith('file'): + # envoi file + if os.path.exists(data['path_fichier']): + # logger.info("imput file a zipper %s" % data['path_fichier']) + name_file_zip_actuel = generer_name_avec_timestamp(machine['uuid_serial_machine'], + data['install_machine_dest']) + + archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + # logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) + + path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + # logger.info("creation d'un fichier zip %s" %path_archive_fichier_name_zip) + + if not zipper_fichier(data['path_fichier'], + path_archive_fichier_name_zip, + fichier_vide=True): + # logger.error("demande de compression dun fichier inexistant") + return + + # Exemple d'utilisation + manager = ZipFileManager(var_file_zipzer) + manager.analyze_and_cleanup() + location = data['install_machine_dest'].replace("\\","/") + # location = os.path.dirname(data['install_machine_dest'].replace("\\","/")) + # logger.info("location %s" % location) + output_dir_list = process_zip_files(var_file_zipzer, + var_file_zipzer, + machine['uuid_serial_machine'], + segment_size=8000, + type_transfert = "location", + location = location, + contenttype = "file") + else: + logger.error("le fichier [%s] a envoyer n'existe pas" % data['path_fichier']) + elif data['contenttype'].lower().startswith('direct') or data['contenttype'].lower().startswith('package') : + if os.path.exists(data['path_fichier']) and os.path.isdir(data['path_fichier']): + logger.info("imput directory a zipper %s" % data['path_fichier']) + name_file_zip_actuel = generer_name_avec_timestamp( machine['uuid_serial_machine'], + data['install_machine_dest']) + archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join(var_file_zipzer, + archive_fichier_name_zip) + # logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) + if not zipper_repertoire( data['path_fichier'], path_archive_fichier_name_zip): + logger.error("demande de compression d'un repertoire inexistant") + return + if data['contenttype'].lower().startswith('package') : + contenttype = 'package' + else: + contenttype = 'directory' + manager = ZipFileManager(var_file_zipzer) + manager.analyze_and_cleanup() + location = data['install_machine_dest'].replace("\\","/") + # logger.info("location %s" % location) + output_dir_list = process_zip_files(var_file_zipzer, + var_file_zipzer, + machine['uuid_serial_machine'], + segment_size=8000, + type_transfert = "location", + location = location, + contenttype = contenttype) + + else: + logger.error("le directory [%s] a envoyer n'existe pas %s " % data['path_fichier']) + except Exception: + logger.error("%s" % (traceback.format_exc())) + +def generer_name_avec_timestamp(jid_dest_backup, pathnamefile, millisecondes=False): + """ + Génère un UUID aléatoire et y ajoute un timestamp. + + :param millisecondes: Si True, utilise le timestamp en millisecondes. Par défaut, c'est False. + :return: Une chaîne contenant l'UUID et le timestamp. + """ + # emplacement = remplacer_caracteres(pathnamefile) + emplacement = pathnamefile + # Obtenir le timestamp + if millisecondes: + timestamp = int(time.time() * 1000) # Timestamp en millisecondes + else: + timestamp = int(time.time()) # Timestamp en secondes + # Combiner le UUID et le timestamp + nom_file_emplacement = f"{timestamp}_{jid_dest_backup}_{emplacement}" + return remplacer_caracteres(nom_file_emplacement) + + +def verifier_cles_non_vides(data, cles): + """ + Vérifie si toutes les clés spécifiées existent dans le dictionnaire + et si leurs valeurs ne sont pas None ou vides. + + Args: + data (dict): Le dictionnaire à vérifier. + cles (list): Une liste de clés à vérifier dans le dictionnaire. + + Returns: + bool: True si toutes les clés existent et leurs valeurs ne sont pas vides, sinon False. + """ + logger.debug("verifier_cles_non_vides") + for cle in cles: + if cle not in data: # Vérifie l'existence et la valeur non vide + logger.error("cle missing %s" % cle ) + return False + if isinstance(data[cle], int): + continue + if not data[cle]: + return False + return True + + +def process_zip_files(input_dir, + output_dir_base_trunck, + uuid_serial_machine, + segment_size=8000, + type_transfert = "backup", + location = None, + contenttype = "directory"): + """ + Lit tous les fichiers ZIP d'un répertoire, les découpe et enregistre les segments dans un répertoire de sortie basé sur le JID. + contenttype file ou directory + """ + output_dir_list=[] + output_dir_base = f"{output_dir_base_trunck}/{uuid_serial_machine}" + + # Vérifier si le répertoire de base existe, sinon le créer + if not os.path.exists(output_dir_base): + os.makedirs(output_dir_base) + + # Parcourir tous les fichiers dans le répertoire d'entrée + for file_name in os.listdir(input_dir): + if file_name.endswith(".zip"): # Vérifier si c'est un fichier ZIP + file_path = os.path.join(input_dir, file_name) + output_dir = os.path.join(output_dir_base, os.path.splitext(file_name)[0]) + + # Créer le répertoire de sortie pour chaque fichier ZIP + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Appeler la fonction split_file pour découper le fichier ZIP + split_file(file_path, + output_dir, + segment_size, + type_transfert, + location, + contenttype) + + # logger.debug(f"Fichier {file_name} découpé et enregistré dans {output_dir}") + try: + os.remove(file_path) + # logger.debug(f"Le fichier {file_name} a été effacé avec succès.") + return True + except FileNotFoundError: + logger.error(f"Le fichier {file_name} n'existe pas.") + return output_dir_list + except PermissionError: + logger.error(f"Vous n'avez pas les permissions nécessaires pour effacer le fichier {file_name}.") + return output_dir_list + except Exception as e: + logger.error(f"Une erreur s'est produite lors de l'effacement du fichier {file_name}: {e}") + return output_dir_list + output_dir_list.append(output_dir) + return output_dir_list + +def md5_hash(file_path): + """Calcule le hash MD5 d'un fichier.""" + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + +def split_file(file_path, + output_dir, + segment_size=8000, + type_transfert = "backup", + location = None , + contenttype = "directory"): + """Découpe un fichier en segments et enregistre chaque segment sous forme de fichier JSON en base64.""" + # Vérifier si le répertoire de sortie existe, sinon le créer + # logger.error(f"split_file file_path {file_path}: ") + # logger.error(f"split_file output_dir {output_dir}: ") + # logger.error(f"split_file contenttype {contenttype}: ") + + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Obtenir les informations de base du fichier + file_name = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + total_parts = (file_size + segment_size - 1) // segment_size + file_uuid = str(uuid.uuid4()) + directory_or_file= file_name + if file_name.endswith(".zip"): + directory_or_file = file_name[:-4] + try: + parties = file_name.split('_') + # Prendre seulement la 3e partie + directory_or_file = parties[2] + except ValueError as ve: + logger.error("Ce n'est pas 1 fichier zip") + return None + + # Calculer le hash MD5 du fichier original + original_md5 = md5_hash(file_path) + + # Lecture et découpe du fichier en segments + with open(file_path, "rb") as f: + part_num = 0 + while chunk := f.read(segment_size): + # Encodage de la partie en base64 + encoded_content = base64.b64encode(chunk).decode('utf-8') + + # Création du fichier JSON pour cette partie + part_data = { + "namefile": file_name, + "directory":directory_or_file, + "nb": part_num + 1, + "nbtotal": total_parts, + "content": encoded_content, + "type": type_transfert, + "location": location, + "contenttype": contenttype + } + # Formater le numéro de segment avec un format spécifique + segment_formatted = "{:06d}".format(part_num+1) + part_file_name = f"{file_uuid}_{segment_formatted}.json" + part_file_path = os.path.join(output_dir, part_file_name) + + with open(part_file_path, "w") as part_file: + json.dump(part_data, part_file) + + part_num += 1 + + # Création du fichier manifeste + # en dernier permet de savoir si archive prete a l'envoi + manifest_data = { + "namefile": file_name, + "directory":directory_or_file, + "creation": str(datetime.now()), + "nb_total": total_parts, + "md5": original_md5, + "type_file": "zip" if file_name.endswith('.zip') else "unknown", + "size_trunck": segment_size, + "type": type_transfert, + "location": location, + "contenttype": contenttype + } + + manifest_file_name = f"{file_uuid}_000000.manif" + manifest_file_path = os.path.join(output_dir, manifest_file_name) + + with open(manifest_file_path, "w") as manifest_file: + json.dump(manifest_data, manifest_file) + + # logger.debug(f"Fichier découpé en {total_parts} parties et manifest généré.") + + +def remplacer_caracteres(chaine): + # Remplacer @ par @@ + chaine = chaine.replace('@', '@64@') + # Remplacer les : par : + chaine = chaine.replace(':', '@58@') + # Remplacer les espaces par @nbsp@ + chaine = chaine.replace(' ', '@nbsp@') + # Remplacer les barres obliques / par / + chaine = chaine.replace('/', '@47@') + # Remplacer les barres obliques \ par \ + chaine = chaine.replace('\\', '@92@') + # Remplacer . par \ + chaine = chaine.replace('.', '@46@') + return chaine + +def restaurer_caracteres(chaine): + chaine = chaine.replace('@58@', ':') + # Remplacer @46@ par . + chaine = chaine.replace('@46@', '.') + # Remplacer @92@ par \ + chaine = chaine.replace('@92@', '\\') + # Remplacer @47@ par / + chaine = chaine.replace('@47@', '/') + # Remplacer @nbsp@ par espace + chaine = chaine.replace('@nbsp@', ' ') + # Remplacer @@ par @ + chaine = chaine.replace('@64@', '@') + + return chaine + +class ZipFileManager: + """ + Classe qui gère les fichiers ZIP dans un répertoire en conservant les fichiers les plus récents + basés sur un horodatage (timestamp) dans le nom du fichier, et en supprimant les fichiers plus anciens. + + Le format attendu des fichiers ZIP est : _.zip + """ + + def __init__(self, directory): + """ + Initialise un gestionnaire de fichiers ZIP pour le répertoire spécifié. + + :param directory: Le répertoire contenant les fichiers ZIP à gérer. + """ + self.directory = directory + self.files_to_keep = {} + + def analyze_and_cleanup(self): + """ + Analyse les fichiers ZIP dans le répertoire et supprime les fichiers obsolètes. + + Garde uniquement le fichier ZIP le plus récent pour chaque combinaison unique de nom de fichier (sans timestamp). + """ + # Lire tous les fichiers .zip dans le répertoire + zip_files = [f for f in os.listdir(self.directory) if f.endswith('.zip')] + + # Analyser chaque fichier + for file in zip_files: + # Vérifier que le fichier correspond au format attendu : _.zip + match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + if match: + timestamp, file_rest = match.groups() + key = file_rest + + # Garder le fichier avec le plus grand timestamp (le plus récent) + if key not in self.files_to_keep or int(timestamp) > int(self.files_to_keep[key][0]): + self.files_to_keep[key] = (timestamp, file) + + # Supprimer les fichiers obsolètes + for file in zip_files: + match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + if match: + timestamp, file_rest = match.groups() + key = file_rest + + # Si un fichier plus récent existe, supprimer l'ancien + if key in self.files_to_keep and self.files_to_keep[key][1] != file: + self.delete_file(file) + + def delete_file(self, file): + """ + Supprime un fichier ZIP du répertoire. + + :param file: Le nom du fichier à supprimer. + """ + file_path = os.path.join(self.directory, file) + os.remove(file_path) + # logger.debug(f"Supprimé : {file_path}") + + +def zipper_fichier(fichier, fichier_zip, fichier_vide=True): + """ + Zipe un seul fichier. + + Args: + fichier (str): Le chemin du fichier à zipper. + fichier_zip (str): Le nom du fichier zip de sortie. + fichier_vide (bool, optional): Si True, inclut les fichiers vides dans le zip. (par défaut: True) + + Returns: + None: La fonction crée un fichier zip et n'a pas de valeur de retour. + """ + # Vérifier si le répertoire existe + if not os.path.exists(fichier): + return None + + with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + if os.path.islink(fichier): + # Ajouter le lien symbolique en tant que tel dans le zip sans compression + zipf.write(fichier, os.path.basename(fichier), compress_type=zipfile.ZIP_STORED) + else: + # Inclure les fichiers vides si demandé + if fichier_vide or os.path.getsize(fichier) > 0: + zipf.write(fichier, os.path.basename(fichier)) + return True + + +def zipper_repertoire(repertoire, + fichier_zip, + resoudre_liens=False, + repertoire_vide=True, + fichier_vide=True): + """ + Zipe le contenu d'un répertoire, y compris tous les fichiers, sous-répertoires, + répertoires vides et fichiers vides. + + Args: + repertoire (str): Le chemin du répertoire à zipper. + fichier_zip (str): Le nom du fichier zip de sortie. + resoudre_liens (bool, optional): Si True, résout les liens symboliques et ajoute leurs cibles au zip. + Si False, ajoute les liens symboliques tels quels. (par défaut: False) + repertoire_vide (bool, optional): Si True, inclut les répertoires vides dans le zip. (par défaut: False) + fichier_vide (bool, optional): Si True, inclut les fichiers vides dans le zip. (par défaut: False) + + Returns: + None: Si le répertoire n'existe pas ou est vide. + True: Si le répertoire n'est pas vide et le fichier zip a été créé. + """ + # Vérifier si le répertoire existe + if not os.path.exists(repertoire): + return None + + # Vérifier si le répertoire est vide + if not os.listdir(repertoire): + return None + + with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Parcourt tous les fichiers et sous-répertoires dans le répertoire spécifié + for root, dirs, files in os.walk(repertoire): + # Ajouter le répertoire courant (même s'il est vide) + chemin_rel_repertoire = os.path.relpath(root, repertoire) + if repertoire_vide or files: # Inclut les répertoires vides ou ceux avec des fichiers + zipf.write(root, chemin_rel_repertoire) # Ajoute le répertoire courant au zip + + for file in files: + chemin_complet = os.path.join(root, file) # Chemin complet du fichier + chemin_rel = os.path.relpath(chemin_complet, repertoire) # Chemin relatif pour le zip + + if os.path.islink(chemin_complet): + if resoudre_liens: + # Résoudre le lien symbolique et ajouter la cible au zip + chemin_cible = os.readlink(chemin_complet) + zipf.write(chemin_cible, chemin_rel) + else: + # Ajouter le lien symbolique en tant que tel dans le zip + zipf.write(chemin_complet, chemin_rel) + else: + # Inclure les fichiers vides si demandé + if fichier_vide or os.path.getsize(chemin_complet) > 0: + zipf.write(chemin_complet, chemin_rel) + + return True From dfdfb96650cd32eaa292ce3c42ac4e5690bcdeb6 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:17:12 +0000 Subject: [PATCH 13/13] style: format code with Black and PHP CS Fixer This commit fixes the style issues introduced in 29403c5 according to the output from Black and PHP CS Fixer. Details: https://github.com/medulla-tech/medulla-agent/pull/561 --- pulse_xmpp_agent/lib/networkinfo.py | 10 +- .../plugins_common/plugin_backup_restore.py | 177 +++++++----- .../plugins_common/plugin_recombine_file.py | 242 +++++++++------- .../lib/networkinfo.py | 11 +- .../lib/plugins/xmpp/__init__.py | 45 +-- .../plugin_backup_restore_substitut.py | 221 +++++++++------ .../plugin_load_send_Segment_file.py | 79 ++++-- .../plugin_send_file_xmpp.py | 266 +++++++++++------- 8 files changed, 652 insertions(+), 399 deletions(-) diff --git a/pulse_xmpp_agent/lib/networkinfo.py b/pulse_xmpp_agent/lib/networkinfo.py index 2e9458b52..d9e9783f4 100644 --- a/pulse_xmpp_agent/lib/networkinfo.py +++ b/pulse_xmpp_agent/lib/networkinfo.py @@ -57,6 +57,7 @@ def find_common_addresses(list1, list2): return common_addresses + def get_CIDR_ipv4_addresses(exclude_localhost=True): """ Récupère les adresses IPv4 au format CIDR pour chaque interface réseau de la machine. @@ -85,7 +86,9 @@ def get_CIDR_ipv4_addresses(exclude_localhost=True): if ip and mask: if exclude_localhost and ip == "127.0.0.1": continue - cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + cidr = ipaddress.IPv4Network( + f"{ip}/{mask}", strict=False + ).with_prefixlen ipv4_addresses.append(cidr) ip, mask = None, None elif system == "Linux": @@ -111,10 +114,13 @@ def get_CIDR_ipv4_addresses(exclude_localhost=True): mask = parts[3] # Le masque est généralement le quatrième champ if exclude_localhost and ip == "127.0.0.1": continue - cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + cidr = ipaddress.IPv4Network( + f"{ip}/{mask}", strict=False + ).with_prefixlen ipv4_addresses.append(cidr) return ipv4_addresses + class networkagentinfo: def __init__(self, sessionid, action="resultgetinfo", param=[]): self.sessionid = sessionid diff --git a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py index 7aeb306f2..d9ea4f26f 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py +++ b/pulse_xmpp_agent/plugins_common/plugin_backup_restore.py @@ -20,9 +20,16 @@ from lib.agentconffile import ( directoryconffile, ) + # from lib.networkinfo import find_common_addresses, get_CIDR_ipv4_addresses import subprocess -from lib.utils import pulseuser_useraccount_mustexist, pulseuser_profile_mustexist, create_idrsa_on_client, getHomedrive, simplecommand +from lib.utils import ( + pulseuser_useraccount_mustexist, + pulseuser_profile_mustexist, + create_idrsa_on_client, + getHomedrive, + simplecommand, +) import platform import configparser import json @@ -42,6 +49,7 @@ config_condition = Condition() + def action(objectxmpp, action, sessionid, data, message, dataerreur): """ Fonction principale du plugin "backup_restore" qui gère l'action de restauration. @@ -64,7 +72,6 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): logger.debug("%s" % json.dumps(data, indent=4)) # strjidagent = str(objectxmpp.boundjid.bare) - compteurcallplugin = getattr(objectxmpp, f"num_call{action}", None) if compteurcallplugin is None: logger.error(f"num_call attribute for action {action} not found on objectxmpp") @@ -75,7 +82,10 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): # tant que pas configurer les concurant attentent pour profiter de la conf aussi. # et pas provoquer des erreurs par manque de configuration if compteurcallplugin == 0: - if not hasattr(objectxmpp, 'configuration_done') or not objectxmpp.configuration_done: + if ( + not hasattr(objectxmpp, "configuration_done") + or not objectxmpp.configuration_done + ): logger.debug("Starting initial configuration") read_conf_plugin_backup_restore(objectxmpp) objectxmpp.configuration_done = True @@ -83,7 +93,10 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): logger.debug("Configuration done") else: # Si la configuration n'est pas encore faite, attendre sa fin - while not hasattr(objectxmpp, 'configuration_done') or not objectxmpp.configuration_done: + while ( + not hasattr(objectxmpp, "configuration_done") + or not objectxmpp.configuration_done + ): config_condition.wait() # Execution du corps du plugin une fois la configuration terminee @@ -96,7 +109,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): if not remote_host: logger.error("le serveur urbackup n'est pas dans le reseau de la machine.") logger.error("SERVEUR IP : %s", server_ip_networks) - logger.error("CLIENT IP : %s", client_ip_networks ) + logger.error("CLIENT IP : %s", client_ip_networks) return except Exception as e: logger.error("termine plugin %s" % (traceback.format_exc())) @@ -105,9 +118,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): try: # Make sure user account and profile exists - result, message = pulseuser_useraccount_mustexist( - objectxmpp.username - ) + result, message = pulseuser_useraccount_mustexist(objectxmpp.username) if result is False: logger.error(f"{message}") return @@ -118,8 +129,7 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): return logger.debug(f"{message}") result, message = create_idrsa_on_client( - objectxmpp.username, - data['key_private'] + objectxmpp.username, data["key_private"] ) if result is False: logger.error(f"{message}") @@ -131,14 +141,17 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): id_rsa_path = os.path.join(getHomedrive(), ".ssh", objectxmpp.private_name_key) - copy_files_and_directories(objectxmpp.remote_user, - remote_host, - data['filelist'], - data['directorylist'], - data['base_path'], - private_key_path=id_rsa_path, - restore_to_backup_location = objectxmpp.restore_to_backup_location, - ssh_port = objectxmpp.reverseserver_ssh_port) + copy_files_and_directories( + objectxmpp.remote_user, + remote_host, + data["filelist"], + data["directorylist"], + data["base_path"], + private_key_path=id_rsa_path, + restore_to_backup_location=objectxmpp.restore_to_backup_location, + ssh_port=objectxmpp.reverseserver_ssh_port, + ) + def read_conf_plugin_backup_restore(objectxmpp): """ @@ -165,7 +178,9 @@ def read_conf_plugin_backup_restore(objectxmpp): # Si le fichier de configuration n'existe pas, le créer avec un contenu par défaut if not os.path.isfile(configfilename): - logger.warning(f"Configuration file {configfilename} not found. Creating it...") + logger.warning( + f"Configuration file {configfilename} not found. Creating it..." + ) config_content = """[backup_restore] remote_user = urbackup @@ -194,10 +209,12 @@ def read_conf_plugin_backup_restore(objectxmpp): """ # Écrire le fichier de configuration avec le contenu par défaut - with open(configfilename, 'w') as configfile: + with open(configfilename, "w") as configfile: configfile.write(config_content) - logger.info(f"Configuration file {configfilename} has been created with default content.") + logger.info( + f"Configuration file {configfilename} has been created with default content." + ) # Charger les valeurs du fichier de configuration Config.read(configfilename) @@ -206,29 +223,43 @@ def read_conf_plugin_backup_restore(objectxmpp): logger.debug(f"Loaded configuration from {configfilename}") # Récupérer les valeurs de configuration ou les valeurs par défaut - objectxmpp.remote_user = Config.get('backup_restore', 'remote_user', fallback='urbackup') - objectxmpp.private_name_key = Config.get('backup_restore', 'private_name_key', fallback='pulseuser_backup_id_rsa') - objectxmpp.username = Config.get('backup_restore', 'username', fallback='pulseuser') - objectxmpp.restore_to_backup_location = Config.getboolean('backup_restore', 'restore_to_backup_location', fallback=True) - objectxmpp.reverseserver_ssh_port = Config.getint('backup_restore', 'reverseserver_ssh_port', fallback=22) - logger.debug(f"Configuration values set: remote_user={objectxmpp.remote_user}, " - f"private_name_key={objectxmpp.private_name_key}, " - f"username={objectxmpp.username}, " - f"restore_to_backup_location={objectxmpp.restore_to_backup_location}") + objectxmpp.remote_user = Config.get( + "backup_restore", "remote_user", fallback="urbackup" + ) + objectxmpp.private_name_key = Config.get( + "backup_restore", "private_name_key", fallback="pulseuser_backup_id_rsa" + ) + objectxmpp.username = Config.get( + "backup_restore", "username", fallback="pulseuser" + ) + objectxmpp.restore_to_backup_location = Config.getboolean( + "backup_restore", "restore_to_backup_location", fallback=True + ) + objectxmpp.reverseserver_ssh_port = Config.getint( + "backup_restore", "reverseserver_ssh_port", fallback=22 + ) + logger.debug( + f"Configuration values set: remote_user={objectxmpp.remote_user}, " + f"private_name_key={objectxmpp.private_name_key}, " + f"username={objectxmpp.username}, " + f"restore_to_backup_location={objectxmpp.restore_to_backup_location}" + ) except Exception as e: logger.error(f"Error reading or creating configuration: {str(e)}") logger.error("\n%s" % (traceback.format_exc())) -def copy_files_and_directories(remote_user, - remote_host, - files, - directories, - base_path, - private_key_path=None, - restore_to_backup_location=True, - ssh_port=22): +def copy_files_and_directories( + remote_user, + remote_host, + files, + directories, + base_path, + private_key_path=None, + restore_to_backup_location=True, + ssh_port=22, +): """ Copies files and directories from a remote host to the local machine. @@ -254,21 +285,20 @@ def copy_files_and_directories(remote_user, # Determiner le systeme d'exploitation system = platform.system() # Chemins des executables - if system == 'Windows': - rsync_path = r'C:\Windows\SysWOW64\rsync.exe' - ssh_path = r'C:\Progra~1\OpenSSH\ssh.exe' - scp_path = r'C:\Progra~1\OpenSSH\scp.exe' + if system == "Windows": + rsync_path = r"C:\Windows\SysWOW64\rsync.exe" + ssh_path = r"C:\Progra~1\OpenSSH\ssh.exe" + scp_path = r"C:\Progra~1\OpenSSH\scp.exe" else: # Linux - rsync_path = 'rsync' - ssh_path = 'ssh' - scp_path = 'scp' + rsync_path = "rsync" + ssh_path = "ssh" + scp_path = "scp" # Verifier si rsync est disponible que pour windows # cette ligne sera a decomenter rsync_available = False # rsync_available = os.path.exists(rsync_path) if system == 'Windows' else True - # quand on poura ectire la commande rsync en windows. avec clef ssh # C:\Windows\SysWOW64\rsync.exe -L -z --rsync-path=rsync # -e "C:/Program Files/OpenSSH/ssh.exe @@ -297,12 +327,7 @@ def copy_files_and_directories(remote_user, if rsync_available: cmd = ( """%s -L -z --rsync-path=rsync -e "%s -o IdentityFile=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o Batchmode=yes -o PasswordAuthentication=no -o ServerAliveInterval=10 -o CheckHostIP=no -o LogLevel=ERROR -o ConnectTimeout=10 -p %s" -av --chmod=777 """ - % ( - rsync_path, - ssh_path, - private_key_path, - ssh_port - ) + % (rsync_path, ssh_path, private_key_path, ssh_port) ) # Copier les fichiers @@ -322,26 +347,26 @@ def copy_files_and_directories(remote_user, # remotesrc = "urbackup@10.10.0.100:/media/BACKUP/urbackup/amu-win-6/240916-1727/Users/desktop.ini" create_directories(get_directory_path(modified_path_dest)) # dest = ' C:/Users/blablabla.txt' - command = cmd + remotesrc + '"'+modified_path_dest+'"' + command = cmd + remotesrc + '"' + modified_path_dest + '"' # command = cmd + remotesrc + modified_path_dest logger.debug(f"Command: {command}") obj = simplecommand(command) logger.warning(f"Transfer file : {obj['code']} {obj['result']}") - if obj['code'] == 1: + if obj["code"] == 1: logger.warning(f"Transfer mais link non creer : { obj['code']}") logger.warning(f"nb link {len(obj['result'])}") logger.warning("liste link") - for linelogresult in obj['result']: + for linelogresult in obj["result"]: logger.warning("{ linelogresult.strip()}") - elif obj['code'] != 0: + elif obj["code"] != 0: logger.error(f"Transfer error: { obj['code']}") logger.error(f"Transfer error: sur nb file : {len(obj['result'])}") else: - if len(obj['result']) > 0: - for linelogresult in obj['result']: + if len(obj["result"]) > 0: + for linelogresult in obj["result"]: logger.debug("Transfer successful") logger.debug("{ linelogresult.strip()}") else: @@ -365,17 +390,17 @@ def copy_files_and_directories(remote_user, remotesrc = """%s@%s:"%s" """ % (remote_user, remote_host, srcdirectory) # create_directories(modified_path_dest) create_directories(get_directory_path(modified_path_dest)) - command = cmd + remotesrc + '"'+modified_path_dest+'"' + command = cmd + remotesrc + '"' + modified_path_dest + '"' logger.debug(f"Command: {command}") obj = simplecommand(command) if rsync_available: - if obj['code'] != 0: + if obj["code"] != 0: logger.error(f"Transfer error code error: { obj['code']}") logger.error(f"error message : {obj['result']}") else: logger.debug("Transfer successful") else: - if obj['code'] == 1: + if obj["code"] == 1: logger.warning(f"Transfer mais link non creer : { obj['code']}") logger.warning(f"nb link {len(obj['result'])}") logger.warning("liste link from serveur %s" % base_path) @@ -385,23 +410,24 @@ def copy_files_and_directories(remote_user, logger.warning("to %s" % backup_path_slach_linux) stringmessage = scp_path + ": Download of file " + base_path # Remplacement de stringmessage dans chaque élément de la liste - for linelogresult in obj['result']: + for linelogresult in obj["result"]: linestr = linelogresult.replace(stringmessage, "").strip() # if restore_to_backup_location: - linestr1 = linestr.replace( backup_path_slach_linux, "") + linestr1 = linestr.replace(backup_path_slach_linux, "") linestr1 = linestr1.replace(str(scp_path), "") logger.warning(f"{linestr1}") - elif obj['code'] != 0: + elif obj["code"] != 0: logger.error(f"Transfer error: { obj['code']}") logger.error(f"Transfer error: sur nb file : {len(obj['result'])}") else: - if len(obj['result']) > 0: - for linelogresult in obj['result']: + if len(obj["result"]) > 0: + for linelogresult in obj["result"]: logger.debug("Transfer successful") logger.debug(f"{linelogresult.strip()}") else: logger.warning("Transfer successful") + def get_ip_and_netmask_linux(exclude_local=True): """ Retrieves the IP addresses and netmasks of the local machine. @@ -427,6 +453,7 @@ def get_ip_and_netmask_linux(exclude_local=True): ip_netmask_list.append((ip, netmask)) return ip_netmask_list + def find_best_server_address(server_addresses, client_addresses): """ Finds the best server address that is in the same network as the client. @@ -439,14 +466,19 @@ def find_best_server_address(server_addresses, client_addresses): :rtype: str or None """ for client_ip, client_netmask in client_addresses: - client_network = ipaddress.IPv4Network(f"{client_ip}/{client_netmask}", strict=False) + client_network = ipaddress.IPv4Network( + f"{client_ip}/{client_netmask}", strict=False + ) for server_ip, server_netmask in server_addresses: - server_network = ipaddress.IPv4Network(f"{server_ip}/{server_netmask}", strict=False) + server_network = ipaddress.IPv4Network( + f"{server_ip}/{server_netmask}", strict=False + ) # Verifie si le client et le serveur sont dans le meme reseau if client_network.overlaps(server_network): return server_ip # Retourne l'adresse du serveur dans le meme reseau que le client return None # Aucun reseau commun trouve + def get_backup_path(): """ Determines the backup path based on the operating system. @@ -470,6 +502,7 @@ def get_backup_path(): return backup_path + def modify_backup_path(base_dir, restore_path): """ Modifies the backup path to include the drive letter. @@ -480,11 +513,12 @@ def modify_backup_path(base_dir, restore_path): :rtype: str """ # Convertir le chemin de restauration en chemin absolu - drive_letter = restore_path.replace(':', '_0') + drive_letter = restore_path.replace(":", "_0") modified_path = os.path.join(base_dir, drive_letter) - modified_path = modified_path.replace('\\', '/') + modified_path = modified_path.replace("\\", "/") return modified_path + def create_directories(path): """ Creates the necessary directories. @@ -495,6 +529,7 @@ def create_directories(path): # Créer les répertoires nécessaires os.makedirs(path, exist_ok=True) + def get_directory_path(file_path): """ Gets the directory path from a file path. @@ -508,10 +543,12 @@ def get_directory_path(file_path): directory_path = os.path.dirname(file_path) return directory_path + class FileHasher: """ A class to calculate the MD5 hash of a file. """ + def __init__(self, file_path): """ Initializes the FileHasher with the file path. diff --git a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py index 6773250cc..dcf5e3528 100644 --- a/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py +++ b/pulse_xmpp_agent/plugins_common/plugin_recombine_file.py @@ -27,83 +27,100 @@ @set_logging_level def action(objectxmpp, action, sessionid, data, message, dataerreur): logger.debug("###################################################") - logger.debug( - "call %s from %s session id %s" % (plugin, message["from"], sessionid) - ) + logger.debug("call %s from %s session id %s" % (plugin, message["from"], sessionid)) logger.debug("###################################################") - logger.debug("json entree %s" % json.dumps(data, indent=4 )) + logger.debug("json entree %s" % json.dumps(data, indent=4)) # verification des clefs - if not verifier_cles_non_vides(data, [ "namefile", - "directory", - "type", - "contenttype", - "segment", - "dir_uuid_machine", - "dir_segment"]): - logger.error("message error recombine_file" ) + if not verifier_cles_non_vides( + data, + [ + "namefile", + "directory", + "type", + "contenttype", + "segment", + "dir_uuid_machine", + "dir_segment", + ], + ): + logger.error("message error recombine_file") return if data["contenttype"].lower().startswith("package"): package = data["location"] - data['type'] = "location" + data["type"] = "location" data["contenttype"] = "directory" - if platform.system() in ['Linux', 'Darwin']: - data['location'] = os.path.join( "/var/lib/pulse2/packages", package) - elif platform.system() == 'Windows': - data['location'] = os.path.join(r'C:\Program Files\Pulse\var\tmp\packages', package ) + if platform.system() in ["Linux", "Darwin"]: + data["location"] = os.path.join("/var/lib/pulse2/packages", package) + elif platform.system() == "Windows": + data["location"] = os.path.join( + r"C:\Program Files\Pulse\var\tmp\packages", package + ) # on veriy les types de transfert - if platform.system() in ['Linux', 'Darwin']: - if data['type'].lower().startswith("backup"): - directory_path = '/var/lib/pulse2/zip_transfert' - elif data['type'].lower().startswith("packages"): - directory_path = '/var/lib/pulse2/packages/' - elif data['type'].lower().startswith("location"): + if platform.system() in ["Linux", "Darwin"]: + if data["type"].lower().startswith("backup"): + directory_path = "/var/lib/pulse2/zip_transfert" + elif data["type"].lower().startswith("packages"): + directory_path = "/var/lib/pulse2/packages/" + elif data["type"].lower().startswith("location"): if not verifier_cles_non_vides(data, ["location"]): logger.error("type location. la location n'est pas transmise") return - elif not chemin_valide( data["location"] ): - logger.error("location n'est pas 1 chemin correct %s" % data["location"] ) + elif not chemin_valide(data["location"]): + logger.error( + "location n'est pas 1 chemin correct %s" % data["location"] + ) return - directory_path = os.path.dirname(data['location']) - namefileresult=os.path.basename(data['location']) + directory_path = os.path.dirname(data["location"]) + namefileresult = os.path.basename(data["location"]) else: - logger.error("type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]") + logger.error( + "type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]" + ) return tmp_dir = "/tmp" - elif platform.system() == 'Windows': - if data['type'].lower().startswith("backup"): - directory_path = r'C:\Program Files\Pulse\var\zip_transfert' - elif data['type'].lower().startswith("packages"): - directory_path = r'C:\Program Files\Pulse\var\tmp\packages' - elif data['type'].lower().startswith("location"): + elif platform.system() == "Windows": + if data["type"].lower().startswith("backup"): + directory_path = r"C:\Program Files\Pulse\var\zip_transfert" + elif data["type"].lower().startswith("packages"): + directory_path = r"C:\Program Files\Pulse\var\tmp\packages" + elif data["type"].lower().startswith("location"): if not verifier_cles_non_vides(data, ["location"]): logger.error("type location. la location n'est pas transmise") return - elif not chemin_valide( data["location"] ): - logger.error("location n'est pas 1 chemin correct %s" % data["location"] ) + elif not chemin_valide(data["location"]): + logger.error( + "location n'est pas 1 chemin correct %s" % data["location"] + ) return - if data['contenttype'].lower().startswith("file") : + if data["contenttype"].lower().startswith("file"): # on recuperer le repertoire ou le fichier ou doit se trouver # quand c'est 1 fichier location doit contenir le path et le nom de fichier de reception - directory_path = os.path.dirname(data['location'].replace("/","\\")) - namefileresult=os.path.basename(data['location'].replace("/","\\")) - directory_path = directory_path.replace("\\","/") + directory_path = os.path.dirname(data["location"].replace("/", "\\")) + namefileresult = os.path.basename(data["location"].replace("/", "\\")) + directory_path = directory_path.replace("\\", "/") else: - directory_path = data['location'] + directory_path = data["location"] else: - logger.error("type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]") + logger.error( + "type transfert incorect, le type doit etre dans la liste [ 'backup', 'packages', 'location' ]" + ) return tmp_dir = r"C:\Windows\Temp" else: logger.error("os inconue") return - if data['type'].lower().startswith('location'): - repertoire_temporaire_reception = os.path.join(tmp_dir, data['dir_uuid_machine'] ) + if data["type"].lower().startswith("location"): + repertoire_temporaire_reception = os.path.join( + tmp_dir, data["dir_uuid_machine"] + ) else: - repertoire_temporaire_reception = os.path.join(tmp_dir, data['dir_uuid_machine'], data['dir_segment'] ) + repertoire_temporaire_reception = os.path.join( + tmp_dir, data["dir_uuid_machine"], data["dir_segment"] + ) try: if not os.path.exists(repertoire_temporaire_reception): @@ -119,29 +136,29 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): # logger.error(f"repertoire_temporaire_reception: {repertoire_temporaire_reception}") # logger.error(f"directory_path : {directory_path}" ) + file_transfert = os.path.join(repertoire_temporaire_reception, data["namefile"]) - file_transfert = os.path.join( repertoire_temporaire_reception, data['namefile']) - - if data['segment'] == 0: + if data["segment"] == 0: # logger.debug("DEBUT TRAITEMENT MANIFEST") # logger.debug("fichier %s" % json.dumps(data, indent=4 )) # manifest # creation des chemin - manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") + manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") # logger.debug("creation manifeste transfert %s" % manifest_file) try: - with open(manifest_file, 'w') as json_file: + with open(manifest_file, "w") as json_file: json.dump(data, json_file, indent=4) except Exception as e: - logger.error(f"Erreur lors de l'écriture du manifest de transfert en JSON : {e}") + logger.error( + f"Erreur lors de l'écriture du manifest de transfert en JSON : {e}" + ) logger.error("We hit the backtrace \n %s" % traceback.format_exc()) # logger.debug("CREATION FICHIER MANIFESTE %s " % manifest_file ) - # Création d'un fichier vide try: # logger.debug("creation file transfer %s" % file_transfert) - with open( file_transfert, 'wb') as file: + with open(file_transfert, "wb") as file: pass # Ne fait rien, mais crée le fichier vide except Exception as e: logger.error(f"Erreur lors de l'écriture du fichier JSON : {e}") @@ -150,52 +167,65 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): else: # logger.debug("DEBUT TRAITEMENT SEGMENT") - if not verifier_cles_non_vides(data, ["nbtotal", - "content", - "nb", - "directory"]): - logger.error("message error TRAITEMENT SEGMENT" ) + if not verifier_cles_non_vides(data, ["nbtotal", "content", "nb", "directory"]): + logger.error("message error TRAITEMENT SEGMENT") return try: # logger.debug("creation file transfer %s" % file_transfert) - with open( file_transfert, 'ab') as file: - file.write(base64.b64decode(data['content']))# Décodage en Base64 + with open(file_transfert, "ab") as file: + file.write(base64.b64decode(data["content"])) # Décodage en Base64 file_size_bytes = get_file_size(file_transfert) if file_size_bytes is not None: file_size_human = convert_size(file_size_bytes) - logger.debug(f"La taille du fichier est : {file_size_bytes} octets ({file_size_human})") + logger.debug( + f"La taille du fichier est : {file_size_bytes} octets ({file_size_human})" + ) except Exception as e: logger.error(f"Erreur lors de l'écriture du fichier JSON : {e}") logger.error("We hit the backtrace \n %s" % traceback.format_exc()) - if data['nbtotal'] == data['segment']: + if data["nbtotal"] == data["segment"]: try: md5 = md5_hash(file_transfert) # logger.error("md5 is %s" % md5 ) # on charge le manifest pour controler le md5 - manifest_file = os.path.join(repertoire_temporaire_reception, "manifeste") - with open(manifest_file, 'r') as json_file: + manifest_file = os.path.join( + repertoire_temporaire_reception, "manifeste" + ) + with open(manifest_file, "r") as json_file: data_manifest = json.load(json_file) - if data_manifest['md5'] == md5: + if data_manifest["md5"] == md5: logger.debug("transfert archive reussi md5 correct") else: - logger.error("le md5 n'est pas du transfert n'est pas correct abandon et netoyage") + logger.error( + "le md5 n'est pas du transfert n'est pas correct abandon et netoyage" + ) supprimer_repertoire(repertoire_temporaire_reception) return except Exception: - logger.error("transfert terminer. 1 erreur dans le md5 %s" % traceback.format_exc()) + logger.error( + "transfert terminer. 1 erreur dans le md5 %s" + % traceback.format_exc() + ) return try: # if data['type'].lower().startswith('location # on recupere le repertoire ou decompresser l'archive - name_repertoire_reception = restaurer_caracteres(data['directory']) - name_repertoire_decompression = re.sub(r'^[A-Za-z]:', '', name_repertoire_reception) - name_repertoire_decompression1 = re.sub(r'\.zip$', '', name_repertoire_decompression) - if data['type'].lower() in ['backup','packages']: - name_repertoire_decomp = directory_path.replace("\\",'/') + name_repertoire_decompression1 - if data['contenttype'].lower().startswith('file'): + name_repertoire_reception = restaurer_caracteres(data["directory"]) + name_repertoire_decompression = re.sub( + r"^[A-Za-z]:", "", name_repertoire_reception + ) + name_repertoire_decompression1 = re.sub( + r"\.zip$", "", name_repertoire_decompression + ) + if data["type"].lower() in ["backup", "packages"]: + name_repertoire_decomp = ( + directory_path.replace("\\", "/") + + name_repertoire_decompression1 + ) + if data["contenttype"].lower().startswith("file"): name_repertoire_decomp = os.path.dirname(name_repertoire_decomp) else: name_repertoire_decomp = directory_path @@ -209,20 +239,28 @@ def action(objectxmpp, action, sessionid, data, message, dataerreur): # logger.debug("decompresser_archive file zip %s"%(file_transfert) ) # logger.debug("decompresser_archive vers repertoire %s"%(name_repertoire_decomp) ) - - if data['contenttype'].lower().startswith('file') and 'location' in data and data['location']: + if ( + data["contenttype"].lower().startswith("file") + and "location" in data + and data["location"] + ): # logger.debug("nouveau_nom file %s"%(namefileresult) ) - decompresser_archive(file_transfert, name_repertoire_decomp, nouveau_nom=namefileresult) + decompresser_archive( + file_transfert, + name_repertoire_decomp, + nouveau_nom=namefileresult, + ) else: decompresser_archive(file_transfert, name_repertoire_decomp) # logger.error("et on remet a zero le fichier" ) # with open( file_transfert, 'wb') as file: - # pass - logger.debug("nettoyage repertoire de travail" ) + # pass + logger.debug("nettoyage repertoire de travail") supprimer_repertoire(repertoire_temporaire_reception) except Exception: logger.error("We hit the backtrace \n %s" % traceback.format_exc()) + def md5_hash(file_path): """ Calcule le hash MD5 d'un fichier. @@ -239,6 +277,7 @@ def md5_hash(file_path): hash_md5.update(chunk) return hash_md5.hexdigest() + def decompresser_archive(fichier_zip, repertoire_destination, nouveau_nom=None): """ Décompresse un fichier ZIP dans un répertoire spécifié. Si un nouveau nom est fourni, @@ -267,21 +306,27 @@ def decompresser_archive(fichier_zip, repertoire_destination, nouveau_nom=None): os.makedirs(repertoire_destination) # Ouvrir le fichier ZIP - with zipfile.ZipFile(fichier_zip, 'r') as zip_ref: + with zipfile.ZipFile(fichier_zip, "r") as zip_ref: # Vérifier si nouveau_nom est défini if nouveau_nom: # Vérifier que l'archive contient exactement un fichier noms_fichiers = zip_ref.namelist() if len(noms_fichiers) != 1: - raise ValueError("L'archive ZIP doit contenir exactement un fichier.") + raise ValueError( + "L'archive ZIP doit contenir exactement un fichier." + ) # Extraire le fichier nom_fichier = noms_fichiers[0] zip_ref.extract(nom_fichier, repertoire_destination) # Renommer le fichier extrait - chemin_fichier_extrait = os.path.join(repertoire_destination, nom_fichier) - chemin_fichier_renomme = os.path.join(repertoire_destination, nouveau_nom) + chemin_fichier_extrait = os.path.join( + repertoire_destination, nom_fichier + ) + chemin_fichier_renomme = os.path.join( + repertoire_destination, nouveau_nom + ) # Vérifier si le fichier de destination existe déjà if os.path.exists(chemin_fichier_renomme): @@ -297,6 +342,7 @@ def decompresser_archive(fichier_zip, repertoire_destination, nouveau_nom=None): logger.error(f"Erreur lors de la décompression de l'archive : {e}") return False + def verifier_cles_non_vides(data, cles): """ Vérifie si toutes les clés spécifiées existent dans le dictionnaire @@ -312,7 +358,7 @@ def verifier_cles_non_vides(data, cles): logger.debug("verifier_cles_non_vides") for cle in cles: if cle not in data: # Vérifie l'existence et la valeur non vide - logger.error("cle missing %s" % cle ) + logger.error("cle missing %s" % cle) return False if isinstance(data[cle], int): continue @@ -320,6 +366,7 @@ def verifier_cles_non_vides(data, cles): return False return True + def get_file_size(file_path): """ Retourne la taille d'un fichier en octets. @@ -337,6 +384,7 @@ def get_file_size(file_path): logger.error(f"Erreur lors de la récupération de la taille du fichier : {e}") return None + def convert_size(size_bytes): """ Convertit une taille en octets en une taille lisible par un humain. @@ -350,11 +398,12 @@ def convert_size(size_bytes): if size_bytes is None: return "Taille inconnue" - for unit in ['octets', 'Ko', 'Mo', 'Go', 'To']: + for unit in ["octets", "Ko", "Mo", "Go", "To"]: if size_bytes < 1024: return f"{size_bytes:.2f} {unit}" size_bytes /= 1024 + def remplacer_caracteres(chaine): """ Remplace certains caractères spéciaux dans une chaîne par des codes spécifiques. @@ -372,13 +421,14 @@ def remplacer_caracteres(chaine): Retourne: str: La chaîne modifiée avec les caractères remplacés. """ - chaine = chaine.replace('\\', '@@') - chaine = chaine.replace(' ', '@nbsp@') - chaine = chaine.replace('/', '@47@') - chaine = chaine.replace('\\', '@92@') - chaine = chaine.replace('.', '@46@') + chaine = chaine.replace("\\", "@@") + chaine = chaine.replace(" ", "@nbsp@") + chaine = chaine.replace("/", "@47@") + chaine = chaine.replace("\\", "@92@") + chaine = chaine.replace(".", "@46@") return chaine + def restaurer_caracteres(chaine): """ Restaure les caractères spéciaux dans une chaîne modifiée. @@ -396,11 +446,11 @@ def restaurer_caracteres(chaine): Retourne: str: La chaîne restaurée avec les caractères originaux. """ - chaine = chaine.replace('@58@', ':') - chaine = chaine.replace('@46@', '.') - chaine = chaine.replace('@92@', '\\') - chaine = chaine.replace('@47@', '/') - chaine = chaine.replace('@nbsp@', ' ') + chaine = chaine.replace("@58@", ":") + chaine = chaine.replace("@46@", ".") + chaine = chaine.replace("@92@", "\\") + chaine = chaine.replace("@47@", "/") + chaine = chaine.replace("@nbsp@", " ") return chaine @@ -439,6 +489,8 @@ def supprimer_repertoire(repertoire): """ if os.path.exists(repertoire): shutil.rmtree(repertoire) - logger.debug(f"Le répertoire '{repertoire}' et tout son contenu ont été supprimés.") + logger.debug( + f"Le répertoire '{repertoire}' et tout son contenu ont été supprimés." + ) else: logger.debug(f"Le répertoire '{repertoire}' n'existe pas.") diff --git a/pulse_xmpp_master_substitute/lib/networkinfo.py b/pulse_xmpp_master_substitute/lib/networkinfo.py index 7828d76ed..e6fc8fd4c 100644 --- a/pulse_xmpp_master_substitute/lib/networkinfo.py +++ b/pulse_xmpp_master_substitute/lib/networkinfo.py @@ -26,7 +26,6 @@ import pythoncom - def find_common_addresses(list1, list2): """ Trouve les adresses IP communes entre deux listes de réseaux CIDR. @@ -56,6 +55,7 @@ def find_common_addresses(list1, list2): return common_addresses + def get_CIDR_ipv4_addresses(exclude_localhost=True): """ Récupère les adresses IPv4 au format CIDR pour chaque interface réseau de la machine. @@ -84,7 +84,9 @@ def get_CIDR_ipv4_addresses(exclude_localhost=True): if ip and mask: if exclude_localhost and ip == "127.0.0.1": continue - cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + cidr = ipaddress.IPv4Network( + f"{ip}/{mask}", strict=False + ).with_prefixlen ipv4_addresses.append(cidr) ip, mask = None, None elif system == "Linux": @@ -110,10 +112,13 @@ def get_CIDR_ipv4_addresses(exclude_localhost=True): mask = parts[3] # Le masque est généralement le quatrième champ if exclude_localhost and ip == "127.0.0.1": continue - cidr = ipaddress.IPv4Network(f'{ip}/{mask}', strict=False).with_prefixlen + cidr = ipaddress.IPv4Network( + f"{ip}/{mask}", strict=False + ).with_prefixlen ipv4_addresses.append(cidr) return ipv4_addresses + class networkagentinfo: def __init__(self, sessionid, action="resultgetinfo", param=[]): self.sessionid = sessionid diff --git a/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py b/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py index e7c06e84f..267bbcde9 100644 --- a/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py +++ b/pulse_xmpp_master_substitute/lib/plugins/xmpp/__init__.py @@ -11721,7 +11721,6 @@ def _return_dict_from_dataset_mysql(self, resultproxy): # ------------------------------------------------------------------------------- - @DatabaseHelper._sessionm def search_machine(self, session, search_term): """ @@ -11737,7 +11736,8 @@ def search_machine(self, session, search_term): if not search_term: return {} # Requête SQL - query = text(""" + query = text( + """ SELECT id, uuid_inventorymachine, @@ -11754,11 +11754,12 @@ def search_machine(self, session, search_term): OR id = :search_term OR jid =:search_term LIMIT 1; - """) + """ + ) logger.error("search_machine query %s" % query) logger.error("search_machine search_term %s" % search_term) # Exécution de la requête - result = session.execute(query, {'search_term': search_term}).fetchone() + result = session.execute(query, {"search_term": search_term}).fetchone() # Si aucun résultat n'est trouvé, retourner un dictionnaire vide if result is None: @@ -11766,12 +11767,12 @@ def search_machine(self, session, search_term): # Construire le dictionnaire de résultats result_machines = { - 'hostname': result['hostname'], - 'jid': result['jid'], - 'uuid_inventorymachine': result['uuid_inventorymachine'], - 'uuid_serial_machine': result['uuid_serial_machine'], - 'id': int(result['id']), - 'enabled': int(result['enabled']) + "hostname": result["hostname"], + "jid": result["jid"], + "uuid_inventorymachine": result["uuid_inventorymachine"], + "uuid_serial_machine": result["uuid_serial_machine"], + "id": int(result["id"]), + "enabled": int(result["enabled"]), } return result_machines @@ -11792,7 +11793,8 @@ def network_list_machine(self, session, id_machine): return [] # Requête SQL - query = text(""" + query = text( + """ SELECT id, macaddress, @@ -11806,10 +11808,11 @@ def network_list_machine(self, session, id_machine): xmppmaster.network WHERE machines_id = :id_machine; - """) + """ + ) # Exécution de la requête - result = session.execute(query, {'id_machine': id_machine}).fetchall() + result = session.execute(query, {"id_machine": id_machine}).fetchall() # Si aucun résultat n'est trouvé, retourner une liste vide if not result: @@ -11819,14 +11822,14 @@ def network_list_machine(self, session, id_machine): network_list = [] for row in result: network_info = { - 'id': row['id'], - 'macaddress': row['macaddress'], - 'ipaddress': row['ipaddress'], - 'broadcast': row['broadcast'], - 'gateway': row['gateway'], - 'mask': row['mask'], - 'mac': row['mac'], - 'machines_id': row['machines_id'] + "id": row["id"], + "macaddress": row["macaddress"], + "ipaddress": row["ipaddress"], + "broadcast": row["broadcast"], + "gateway": row["gateway"], + "mask": row["mask"], + "mac": row["mac"], + "machines_id": row["machines_id"], } network_list.append(network_info) diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py index e34ef164c..dea140c19 100644 --- a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_backup_restore_substitut.py @@ -31,13 +31,14 @@ plugin = {"VERSION": "1.0", "NAME": "backup_restore_substitut", "TYPE": "mastersub"} + def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): logger.debug("###################################################") logger.debug("call %s from %s" % (plugin, message["from"])) logger.debug("###################################################") - logger.debug("data %s" % json.dumps(data['data'], indent=4)) + logger.debug("data %s" % json.dumps(data["data"], indent=4)) try: - datamsg = data['data'] + datamsg = data["data"] # logger.debug("datamsg %s" % datamsg.keys()) # Spécifier le chemin où créer le répertoire ZIP @@ -46,64 +47,85 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): os.makedirs(var_file_zipzer, exist_ok=True) # Traitement des répertoires à zipper - if 'directorylist' in datamsg and datamsg['directorylist']: - for directory in datamsg['directorylist']: + if "directorylist" in datamsg and datamsg["directorylist"]: + for directory in datamsg["directorylist"]: # logger.debug("creation d'un fichier zip depuis 1 repertoire : %s" % directory) if directory: - repertoire_zip = os.path.join(datamsg['base_path'], directory[0]) + repertoire_zip = os.path.join(datamsg["base_path"], directory[0]) # logger.info("repertoire_zip %s" % repertoire_zip) - name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], - os.path.join(directory[1])) - archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + name_file_zip_actuel = generer_name_avec_timestamp( + datamsg["machine_dest_backup"]["uuid_serial_machine"], + os.path.join(directory[1]), + ) + archive_fichier_name_zip = f"{name_file_zip_actuel}.zip" # logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) - path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join( + var_file_zipzer, archive_fichier_name_zip + ) # logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) - if not zipper_repertoire(repertoire_zip, path_archive_fichier_name_zip): + if not zipper_repertoire( + repertoire_zip, path_archive_fichier_name_zip + ): logger.error("demande de compression dun repertoire inexistant") continue # Exemple d'utilisation manager = ZipFileManager(var_file_zipzer) manager.analyze_and_cleanup() - output_dir_list = process_zip_files(var_file_zipzer, var_file_zipzer, datamsg['machine_dest_backup']['uuid_serial_machine'], segment_size=64000) + output_dir_list = process_zip_files( + var_file_zipzer, + var_file_zipzer, + datamsg["machine_dest_backup"]["uuid_serial_machine"], + segment_size=64000, + ) # Traitement des fichiers à zipper - if 'filelist' in datamsg and datamsg['filelist']: - for files in datamsg['filelist']: + if "filelist" in datamsg and datamsg["filelist"]: + for files in datamsg["filelist"]: # logger.debug("creation d'un fichier zip depuis 1 fichier : %s" % files) if files: - input_file = os.path.join(datamsg['base_path'], files[0]) + input_file = os.path.join(datamsg["base_path"], files[0]) # logger.info("imput file a zipper %s" % input_file) - name_file_zip_actuel = generer_name_avec_timestamp(datamsg['machine_dest_backup']['uuid_serial_machine'], - os.path.join(files[1])) - archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + name_file_zip_actuel = generer_name_avec_timestamp( + datamsg["machine_dest_backup"]["uuid_serial_machine"], + os.path.join(files[1]), + ) + archive_fichier_name_zip = f"{name_file_zip_actuel}.zip" # logger.info("archive_fichier_name_zip %s" % archive_fichier_name_zip) - path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join( + var_file_zipzer, archive_fichier_name_zip + ) # logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) - if not zipper_fichier(input_file, path_archive_fichier_name_zip, fichier_vide=True): + if not zipper_fichier( + input_file, path_archive_fichier_name_zip, fichier_vide=True + ): logger.error("demande de compression dun fichier inexistant") continue # Exemple d'utilisation manager = ZipFileManager(var_file_zipzer) manager.analyze_and_cleanup() - output_dir_list = process_zip_files(var_file_zipzer, - var_file_zipzer, - datamsg['machine_dest_backup']['uuid_serial_machine'], - segment_size=64000, - type_transfert="backup", - location=None, - contenttype="file" - ) + output_dir_list = process_zip_files( + var_file_zipzer, + var_file_zipzer, + datamsg["machine_dest_backup"]["uuid_serial_machine"], + segment_size=64000, + type_transfert="backup", + location=None, + contenttype="file", + ) except Exception: logger.error("%s" % (traceback.format_exc())) -def process_zip_files(input_dir, - output_dir_base_trunck, - uuid_serial_machine, - segment_size=8000, - type_transfert="backup", - location=None, - contenttype="directory"): + +def process_zip_files( + input_dir, + output_dir_base_trunck, + uuid_serial_machine, + segment_size=8000, + type_transfert="backup", + location=None, + contenttype="directory", +): """ Lit tous les fichiers ZIP d'un répertoire, les découpe et enregistre les segments dans un répertoire de sortie basé sur le JID. contenttype file ou directory @@ -126,12 +148,14 @@ def process_zip_files(input_dir, os.makedirs(output_dir) # Appeler la fonction split_file pour découper le fichier ZIP - split_file(file_path, - output_dir, - segment_size, - type_transfert, - location, - contenttype) + split_file( + file_path, + output_dir, + segment_size, + type_transfert, + location, + contenttype, + ) # logger.debug(f"Fichier {file_name} découpé et enregistré dans {output_dir}") try: @@ -142,14 +166,19 @@ def process_zip_files(input_dir, logger.error(f"Le fichier {file_name} n'existe pas.") return output_dir_list except PermissionError: - logger.error(f"Vous n'avez pas les permissions nécessaires pour effacer le fichier {file_name}.") + logger.error( + f"Vous n'avez pas les permissions nécessaires pour effacer le fichier {file_name}." + ) return output_dir_list except Exception as e: - logger.error(f"Une erreur s'est produite lors de l'effacement du fichier {file_name}: {e}") + logger.error( + f"Une erreur s'est produite lors de l'effacement du fichier {file_name}: {e}" + ) return output_dir_list output_dir_list.append(output_dir) return output_dir_list + def md5_hash(file_path): """Calcule le hash MD5 d'un fichier.""" hash_md5 = hashlib.md5() @@ -158,12 +187,15 @@ def md5_hash(file_path): hash_md5.update(chunk) return hash_md5.hexdigest() -def split_file(file_path, - output_dir, - segment_size=8000, - type_transfert="backup", - location=None, - contenttype="directory"): + +def split_file( + file_path, + output_dir, + segment_size=8000, + type_transfert="backup", + location=None, + contenttype="directory", +): """Découpe un fichier en segments et enregistre chaque segment sous forme de fichier JSON en base64.""" if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -177,7 +209,7 @@ def split_file(file_path, if file_name.endswith(".zip"): directory_or_file = file_name[:-4] try: - parties = file_name.split('_') + parties = file_name.split("_") # Prendre seulement la 3e partie directory_or_file = parties[2] except ValueError as ve: @@ -192,7 +224,7 @@ def split_file(file_path, part_num = 0 while chunk := f.read(segment_size): # Encodage de la partie en base64 - encoded_content = base64.b64encode(chunk).decode('utf-8') + encoded_content = base64.b64encode(chunk).decode("utf-8") # Création du fichier JSON pour cette partie part_data = { @@ -203,10 +235,10 @@ def split_file(file_path, "content": encoded_content, "type": type_transfert, "location": location, - "contenttype": contenttype + "contenttype": contenttype, } # Formater le numéro de segment avec un format spécifique - segment_formatted = "{:06d}".format(part_num+1) + segment_formatted = "{:06d}".format(part_num + 1) part_file_name = f"{file_uuid}_{segment_formatted}.json" part_file_path = os.path.join(output_dir, part_file_name) @@ -223,11 +255,11 @@ def split_file(file_path, "creation": str(datetime.now()), "nb_total": total_parts, "md5": original_md5, - "type_file": "zip" if file_name.endswith('.zip') else "unknown", + "type_file": "zip" if file_name.endswith(".zip") else "unknown", "size_trunck": segment_size, "type": type_transfert, "location": location, - "contenttype": contenttype + "contenttype": contenttype, } manifest_file_name = f"{file_uuid}_0.manif" @@ -238,6 +270,7 @@ def split_file(file_path, # logger.debug(f"Fichier découpé en {total_parts} parties et manifest généré.") + def recombine_file(manifest_path, input_dir, output_file): """Recompose le fichier original à partir des segments JSON et vérifie le hash MD5.""" # Lire le fichier manifeste @@ -267,7 +300,10 @@ def recombine_file(manifest_path, input_dir, output_file): if recombined_md5 == original_md5: logger.debug(f"Fichier recomposé avec succès, MD5 vérifié : {recombined_md5}") else: - logger.error(f"Erreur : le MD5 recomposé {recombined_md5} ne correspond pas à l'original {original_md5}.") + logger.error( + f"Erreur : le MD5 recomposé {recombined_md5} ne correspond pas à l'original {original_md5}." + ) + def check_manifests(directory): """Vérifie tous les manifestes dans un répertoire et retourne deux listes : @@ -306,36 +342,39 @@ def check_manifests(directory): return complete_manifests, incomplete_manifests + def remplacer_caracteres(chaine): # Remplacer @ par @@ - chaine = chaine.replace('@', '@64@') + chaine = chaine.replace("@", "@64@") # Remplacer les : par : - chaine = chaine.replace(':', '@58@') + chaine = chaine.replace(":", "@58@") # Remplacer les espaces par @nbsp@ - chaine = chaine.replace(' ', '@nbsp@') + chaine = chaine.replace(" ", "@nbsp@") # Remplacer les barres obliques / par / - chaine = chaine.replace('/', '@47@') + chaine = chaine.replace("/", "@47@") # Remplacer les barres obliques \ par \ - chaine = chaine.replace('\\', '@92@') + chaine = chaine.replace("\\", "@92@") # Remplacer . par \ - chaine = chaine.replace('.', '@46@') + chaine = chaine.replace(".", "@46@") return chaine + def restaurer_caracteres(chaine): - chaine = chaine.replace('@58@', ':') + chaine = chaine.replace("@58@", ":") # Remplacer @46@ par . - chaine = chaine.replace('@46@', '.') + chaine = chaine.replace("@46@", ".") # Remplacer @92@ par \ - chaine = chaine.replace('@92@', '\\') + chaine = chaine.replace("@92@", "\\") # Remplacer @47@ par / - chaine = chaine.replace('@47@', '/') + chaine = chaine.replace("@47@", "/") # Remplacer @nbsp@ par espace - chaine = chaine.replace('@nbsp@', ' ') + chaine = chaine.replace("@nbsp@", " ") # Remplacer @@ par @ - chaine = chaine.replace('@64@', '@') + chaine = chaine.replace("@64@", "@") return chaine + def time_since_timestamp(timestamp): """ Calcule le temps écoulé depuis un timestamp donné. @@ -354,7 +393,7 @@ def time_since_timestamp(timestamp): # Convertir la différence en jours, heures, minutes et secondes days = time_difference // (24 * 3600) - time_difference %= (24 * 3600) + time_difference %= 24 * 3600 hours = time_difference // 3600 time_difference %= 3600 minutes = time_difference // 60 @@ -362,6 +401,7 @@ def time_since_timestamp(timestamp): return int(days), int(hours), int(minutes), int(seconds) + def generer_name_avec_timestamp(jid_dest_backup, pathnamefile, millisecondes=False): """ Génère un UUID aléatoire et y ajoute un timestamp. @@ -380,6 +420,7 @@ def generer_name_avec_timestamp(jid_dest_backup, pathnamefile, millisecondes=Fal nom_file_emplacement = f"{timestamp}_{jid_dest_backup}_{emplacement}" return remplacer_caracteres(nom_file_emplacement) + def generer_uuid_avec_timestamp(millisecondes=False): """ Génère un UUID aléatoire et y ajoute un timestamp. @@ -401,6 +442,7 @@ def generer_uuid_avec_timestamp(millisecondes=False): return uuid_timestamp + class ZipFileManager: """ Classe qui gère les fichiers ZIP dans un répertoire en conservant les fichiers les plus récents @@ -425,23 +467,25 @@ def analyze_and_cleanup(self): Garde uniquement le fichier ZIP le plus récent pour chaque combinaison unique de nom de fichier (sans timestamp). """ # Lire tous les fichiers .zip dans le répertoire - zip_files = [f for f in os.listdir(self.directory) if f.endswith('.zip')] + zip_files = [f for f in os.listdir(self.directory) if f.endswith(".zip")] # Analyser chaque fichier for file in zip_files: # Vérifier que le fichier correspond au format attendu : _.zip - match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + match = re.match(r"(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip", file) if match: timestamp, file_rest = match.groups() key = file_rest # Garder le fichier avec le plus grand timestamp (le plus récent) - if key not in self.files_to_keep or int(timestamp) > int(self.files_to_keep[key][0]): + if key not in self.files_to_keep or int(timestamp) > int( + self.files_to_keep[key][0] + ): self.files_to_keep[key] = (timestamp, file) # Supprimer les fichiers obsolètes for file in zip_files: - match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + match = re.match(r"(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip", file) if match: timestamp, file_rest = match.groups() key = file_rest @@ -460,6 +504,7 @@ def delete_file(self, file): os.remove(file_path) # logger.debug(f"Supprimé : {file_path}") + def zipper_fichier(fichier, fichier_zip, fichier_vide=True): """ Zipe un seul fichier. @@ -476,17 +521,26 @@ def zipper_fichier(fichier, fichier_zip, fichier_vide=True): if not os.path.exists(fichier): return None - with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(fichier_zip, "w", zipfile.ZIP_DEFLATED) as zipf: if os.path.islink(fichier): # Ajouter le lien symbolique en tant que tel dans le zip sans compression - zipf.write(fichier, os.path.basename(fichier), compress_type=zipfile.ZIP_STORED) + zipf.write( + fichier, os.path.basename(fichier), compress_type=zipfile.ZIP_STORED + ) else: # Inclure les fichiers vides si demandé if fichier_vide or os.path.getsize(fichier) > 0: zipf.write(fichier, os.path.basename(fichier)) return True -def zipper_repertoire(repertoire, fichier_zip, resoudre_liens=False, repertoire_vide=True, fichier_vide=True): + +def zipper_repertoire( + repertoire, + fichier_zip, + resoudre_liens=False, + repertoire_vide=True, + fichier_vide=True, +): """ Zipe le contenu d'un répertoire, y compris tous les fichiers, sous-répertoires, répertoires vides et fichiers vides. @@ -511,17 +565,23 @@ def zipper_repertoire(repertoire, fichier_zip, resoudre_liens=False, repertoire_ if not os.listdir(repertoire): return None - with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(fichier_zip, "w", zipfile.ZIP_DEFLATED) as zipf: # Parcourt tous les fichiers et sous-répertoires dans le répertoire spécifié for root, dirs, files in os.walk(repertoire): # Ajouter le répertoire courant (même s'il est vide) chemin_rel_repertoire = os.path.relpath(root, repertoire) - if repertoire_vide or files: # Inclut les répertoires vides ou ceux avec des fichiers - zipf.write(root, chemin_rel_repertoire) # Ajoute le répertoire courant au zip + if ( + repertoire_vide or files + ): # Inclut les répertoires vides ou ceux avec des fichiers + zipf.write( + root, chemin_rel_repertoire + ) # Ajoute le répertoire courant au zip for file in files: chemin_complet = os.path.join(root, file) # Chemin complet du fichier - chemin_rel = os.path.relpath(chemin_complet, repertoire) # Chemin relatif pour le zip + chemin_rel = os.path.relpath( + chemin_complet, repertoire + ) # Chemin relatif pour le zip if os.path.islink(chemin_complet): if resoudre_liens: @@ -538,6 +598,7 @@ def zipper_repertoire(repertoire, fichier_zip, resoudre_liens=False, repertoire_ return True + def decompresser_archive(fichier_zip, repertoire_destination): # Vérifier si le fichier ZIP existe if not os.path.exists(fichier_zip): @@ -548,5 +609,5 @@ def decompresser_archive(fichier_zip, repertoire_destination): os.makedirs(repertoire_destination) # Ouvrir et extraire tout le contenu du fichier ZIP - with zipfile.ZipFile(fichier_zip, 'r') as zip_ref: + with zipfile.ZipFile(fichier_zip, "r") as zip_ref: zip_ref.extractall(repertoire_destination) diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py index 831916481..f9da869cf 100644 --- a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_load_send_Segment_file.py @@ -31,6 +31,7 @@ var_file_zipper = "/var/lib/pulse2/zip_transfert" + def action(objectxmpp, action, sessionid, data, msg, dataerreur): """ Fonction principale du plugin, appelée lors de l'exécution du plugin. @@ -55,7 +56,9 @@ def action(objectxmpp, action, sessionid, data, msg, dataerreur): if compteurcallplugin == 0: read_conf_load_send_Segment_file(objectxmpp) # Installer le code dynamique : fonction de transfert de fichier - objectxmpp.transfert_segment_file = types.MethodType(transfert_segment_file, objectxmpp) + objectxmpp.transfert_segment_file = types.MethodType( + transfert_segment_file, objectxmpp + ) # Planifier l'appel de cette fonction scedule_call_plugin_in_seconde = 15 objectxmpp.schedule( @@ -65,6 +68,7 @@ def action(objectxmpp, action, sessionid, data, msg, dataerreur): repeat=True, ) + def transfert_segment_file(self): """ Fonction de transfert de fichiers vers les machines présentes. @@ -78,7 +82,9 @@ def transfert_segment_file(self): type_transfert = "backup" location = None transfert_de_fichier_a_nb_machine_simultanement = 10 - repertoires_uuid = lister_repertoires_uuid(var_file_zipper)[:transfert_de_fichier_a_nb_machine_simultanement] + repertoires_uuid = lister_repertoires_uuid(var_file_zipper)[ + :transfert_de_fichier_a_nb_machine_simultanement + ] # Envoyer les fichiers tronqués. for dir_uuid_machine in repertoires_uuid: # dir est un répertoire mais le nom du répertoire est le UUID de la machine cible. @@ -87,15 +93,25 @@ def transfert_segment_file(self): machine = XmppMasterDatabase().getMachinefromuuidsetup(str(dir_uuid_machine)) if machine: - if not machine['enabled']: + if not machine["enabled"]: # La machine n'est pas présente, on ne peut pas lui envoyer des fichiers. - logger.debug("Machine %s eteinte : on ne peut pas transferer de fichiers a cette machine" % machine['hostname'] ) - logger.debug("tout les demande de transfert pour la machine %s sont annuler" % machine['hostname']) + logger.debug( + "Machine %s eteinte : on ne peut pas transferer de fichiers a cette machine" + % machine["hostname"] + ) + logger.debug( + "tout les demande de transfert pour la machine %s sont annuler" + % machine["hostname"] + ) supprimer_repertoire(os.path.join(var_file_zipper, dir_uuid_machine)) continue - logger.debug("TRANSFERT SEGMENT FILE to machine %s " % (machine['hostname'] )) + logger.debug( + "TRANSFERT SEGMENT FILE to machine %s " % (machine["hostname"]) + ) # La machine est présente. On peut lui envoyer les fichiers. - list_repertoire_to_file_for_send = get_uuid_directories(os.path.join(var_file_zipper, dir_uuid_machine)) + list_repertoire_to_file_for_send = get_uuid_directories( + os.path.join(var_file_zipper, dir_uuid_machine) + ) for repertoire_file_seg in list_repertoire_to_file_for_send: sessionid = getRandomName(5, "transfert_file") file_list_to_send = lister_fichiers(repertoire_file_seg) @@ -108,7 +124,7 @@ def transfert_segment_file(self): # Le premier fichier de la liste doit être un fichier .manif pour manifeste. if file_list_to_send[0].endswith(".manif"): manifeste = lire_fichier_json(file_list_to_send[0]) - if len(file_list_to_send) != manifeste['nb_total'] + 1: + if len(file_list_to_send) != manifeste["nb_total"] + 1: # Pas encore préparé continue @@ -134,24 +150,27 @@ def transfert_segment_file(self): data = manifeste else: data = lire_fichier_json(filesend) - data['namefile'] = manifeste['namefile'] + data["namefile"] = manifeste["namefile"] - data['segment'] = indexfile - data['dir_uuid_machine'] = dir_uuid_machine - data['dir_segment'] = os.path.basename(repertoire_file_seg) + data["segment"] = indexfile + data["dir_uuid_machine"] = dir_uuid_machine + data["dir_segment"] = os.path.basename(repertoire_file_seg) msg_send = { "sessionid": sessionid, "data": data, "action": "recombine_file", - "ret": 0 + "ret": 0, } indexfile = indexfile + 1 # C'est la machine qui reçoit les segments qui doit recombiner le fichier. - self.send_message(mto=machine['jid'], mbody=json.dumps(msg_send), mtype="chat") + self.send_message( + mto=machine["jid"], mbody=json.dumps(msg_send), mtype="chat" + ) supprimer_repertoire(repertoire_file_seg) supprimer_repertoire(os.path.join(var_file_zipper, dir_uuid_machine)) + def supprimer_repertoire(chemin_repertoire): """ Supprime un répertoire et tout son contenu. @@ -170,12 +189,17 @@ def supprimer_repertoire(chemin_repertoire): logger.debug(f"Le répertoire {chemin_repertoire} n'existe pas.") return False except PermissionError: - logger.debug(f"Vous n'avez pas les permissions nécessaires pour supprimer le répertoire {chemin_repertoire}.") + logger.debug( + f"Vous n'avez pas les permissions nécessaires pour supprimer le répertoire {chemin_repertoire}." + ) return False except Exception as e: - logger.debug(f"Une erreur s'est produite lors de la suppression du répertoire {chemin_repertoire}: {e}") + logger.debug( + f"Une erreur s'est produite lors de la suppression du répertoire {chemin_repertoire}: {e}" + ) return False + def lire_fichier_json(chemin_fichier): """ Lit le contenu d'un fichier JSON et le stocke dans un dictionnaire. @@ -187,7 +211,7 @@ def lire_fichier_json(chemin_fichier): dict: Le contenu du fichier JSON sous forme de dictionnaire. """ try: - with open(chemin_fichier, 'r', encoding='utf-8') as fichier: + with open(chemin_fichier, "r", encoding="utf-8") as fichier: contenu = json.load(fichier) return contenu except FileNotFoundError: @@ -200,6 +224,7 @@ def lire_fichier_json(chemin_fichier): logger.debug(f"Erreur lors de la lecture du fichier {chemin_fichier}: {e}") return None + def get_uuid_directories(base_directory): """ Renvoie les répertoires dans un répertoire de base dont le nom commence par 10 chiffres. @@ -213,7 +238,9 @@ def get_uuid_directories(base_directory): matching_dirs = [] # Expression régulière pour 10 chiffres au début du nom - pattern = re.compile(r'^\d{10}_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') + pattern = re.compile( + r"^\d{10}_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + ) # Parcourt les fichiers et répertoires dans le répertoire de base for dir_name in os.listdir(base_directory): @@ -224,6 +251,7 @@ def get_uuid_directories(base_directory): return matching_dirs + def lister_fichiers(repertoire): """ Lit tous les fichiers dans un répertoire donné et renvoie une liste triée des fichiers. @@ -240,7 +268,11 @@ def lister_fichiers(repertoire): contenu = os.listdir(repertoire) # logger.debug(f"CONTENU {contenu}") # Filtrer pour ne conserver que les fichiers - fichiers = [os.path.join(repertoire, f) for f in contenu if os.path.isfile(os.path.join(repertoire, f))] + fichiers = [ + os.path.join(repertoire, f) + for f in contenu + if os.path.isfile(os.path.join(repertoire, f)) + ] # logger.debug(f"fichiers {fichiers}") # Trier la liste des fichiers fichiers.sort() @@ -250,6 +282,7 @@ def lister_fichiers(repertoire): logger.debug(f"Erreur lors de la lecture du répertoire {repertoire}: {e}") return [] + def lire_contenu_fichier(chemin_fichier): """ Lit le contenu texte d'un fichier et le stocke dans une variable. @@ -261,13 +294,14 @@ def lire_contenu_fichier(chemin_fichier): str: Le contenu texte du fichier. """ try: - with open(chemin_fichier, 'r', encoding='utf-8') as fichier: + with open(chemin_fichier, "r", encoding="utf-8") as fichier: contenu = fichier.read() return contenu except Exception as e: logger.debug(f"Erreur lors de la lecture du fichier {chemin_fichier}: {e}") return None + def lister_repertoires_uuid(repertoire_principal): """ Renvoie une liste de tous les répertoires dans le répertoire principal qui ont un nom de la forme UUID. @@ -279,7 +313,9 @@ def lister_repertoires_uuid(repertoire_principal): list: Une liste des répertoires ayant un nom de la forme UUID. """ # Expression régulière pour vérifier le format UUID - uuid_pattern = re.compile(r'^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$') + uuid_pattern = re.compile( + r"^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + ) # Liste pour stocker les répertoires UUID repertoires_uuid = [] # Parcourir les éléments dans le répertoire principal @@ -289,6 +325,7 @@ def lister_repertoires_uuid(repertoire_principal): repertoires_uuid.append(item) return repertoires_uuid + def read_conf_load_send_Segment_file(objectxmpp): """ Lire la configuration du plugin. diff --git a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py index 618bfcbd1..4c759dccb 100644 --- a/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py +++ b/pulse_xmpp_master_substitute/pluginsmastersubstitute/plugin_send_file_xmpp.py @@ -32,6 +32,7 @@ plugin = {"VERSION": "1.0", "NAME": "send_file_xmpp", "TYPE": "substitut"} + def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): logger.debug("###################################################") logger.debug("call %s from %s" % (plugin, message["from"])) @@ -41,85 +42,111 @@ def action(xmppobject, action, sessionid, data, message, ret, dataobj=None): # Créer le répertoire s'il n'existe pas os.makedirs(var_file_zipzer, exist_ok=True) - if not verifier_cles_non_vides(data, [ "machine_dest", - "path_fichier", - "install_machine_dest", - "contenttype"]): + if not verifier_cles_non_vides( + data, ["machine_dest", "path_fichier", "install_machine_dest", "contenttype"] + ): logger.error("il manque des clefs dans le json d'entree") return try: # datamsg=data['data'] - machine = XmppMasterDatabase().search_machine(data['machine_dest']) + machine = XmppMasterDatabase().search_machine(data["machine_dest"]) if machine: # logger.info("Machine destination %s" % json.dumps(machine, indent=4)) - if data['contenttype'].lower().startswith('file'): + if data["contenttype"].lower().startswith("file"): # envoi file - if os.path.exists(data['path_fichier']): + if os.path.exists(data["path_fichier"]): # logger.info("imput file a zipper %s" % data['path_fichier']) - name_file_zip_actuel = generer_name_avec_timestamp(machine['uuid_serial_machine'], - data['install_machine_dest']) + name_file_zip_actuel = generer_name_avec_timestamp( + machine["uuid_serial_machine"], data["install_machine_dest"] + ) - archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' + archive_fichier_name_zip = f"{name_file_zip_actuel}.zip" # logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) - path_archive_fichier_name_zip = os.path.join(var_file_zipzer, archive_fichier_name_zip) + path_archive_fichier_name_zip = os.path.join( + var_file_zipzer, archive_fichier_name_zip + ) # logger.info("creation d'un fichier zip %s" %path_archive_fichier_name_zip) - if not zipper_fichier(data['path_fichier'], - path_archive_fichier_name_zip, - fichier_vide=True): + if not zipper_fichier( + data["path_fichier"], + path_archive_fichier_name_zip, + fichier_vide=True, + ): # logger.error("demande de compression dun fichier inexistant") return # Exemple d'utilisation manager = ZipFileManager(var_file_zipzer) manager.analyze_and_cleanup() - location = data['install_machine_dest'].replace("\\","/") + location = data["install_machine_dest"].replace("\\", "/") # location = os.path.dirname(data['install_machine_dest'].replace("\\","/")) # logger.info("location %s" % location) - output_dir_list = process_zip_files(var_file_zipzer, - var_file_zipzer, - machine['uuid_serial_machine'], - segment_size=8000, - type_transfert = "location", - location = location, - contenttype = "file") + output_dir_list = process_zip_files( + var_file_zipzer, + var_file_zipzer, + machine["uuid_serial_machine"], + segment_size=8000, + type_transfert="location", + location=location, + contenttype="file", + ) else: - logger.error("le fichier [%s] a envoyer n'existe pas" % data['path_fichier']) - elif data['contenttype'].lower().startswith('direct') or data['contenttype'].lower().startswith('package') : - if os.path.exists(data['path_fichier']) and os.path.isdir(data['path_fichier']): - logger.info("imput directory a zipper %s" % data['path_fichier']) - name_file_zip_actuel = generer_name_avec_timestamp( machine['uuid_serial_machine'], - data['install_machine_dest']) - archive_fichier_name_zip = f'{name_file_zip_actuel}.zip' - logger.info("archive_fichier_name_zip %s" %archive_fichier_name_zip) - path_archive_fichier_name_zip = os.path.join(var_file_zipzer, - archive_fichier_name_zip) + logger.error( + "le fichier [%s] a envoyer n'existe pas" % data["path_fichier"] + ) + elif data["contenttype"].lower().startswith("direct") or data[ + "contenttype" + ].lower().startswith("package"): + if os.path.exists(data["path_fichier"]) and os.path.isdir( + data["path_fichier"] + ): + logger.info("imput directory a zipper %s" % data["path_fichier"]) + name_file_zip_actuel = generer_name_avec_timestamp( + machine["uuid_serial_machine"], data["install_machine_dest"] + ) + archive_fichier_name_zip = f"{name_file_zip_actuel}.zip" + logger.info( + "archive_fichier_name_zip %s" % archive_fichier_name_zip + ) + path_archive_fichier_name_zip = os.path.join( + var_file_zipzer, archive_fichier_name_zip + ) # logger.info("creation d'un fichier zip %s" % path_archive_fichier_name_zip) - if not zipper_repertoire( data['path_fichier'], path_archive_fichier_name_zip): - logger.error("demande de compression d'un repertoire inexistant") + if not zipper_repertoire( + data["path_fichier"], path_archive_fichier_name_zip + ): + logger.error( + "demande de compression d'un repertoire inexistant" + ) return - if data['contenttype'].lower().startswith('package') : - contenttype = 'package' + if data["contenttype"].lower().startswith("package"): + contenttype = "package" else: - contenttype = 'directory' + contenttype = "directory" manager = ZipFileManager(var_file_zipzer) manager.analyze_and_cleanup() - location = data['install_machine_dest'].replace("\\","/") + location = data["install_machine_dest"].replace("\\", "/") # logger.info("location %s" % location) - output_dir_list = process_zip_files(var_file_zipzer, - var_file_zipzer, - machine['uuid_serial_machine'], - segment_size=8000, - type_transfert = "location", - location = location, - contenttype = contenttype) + output_dir_list = process_zip_files( + var_file_zipzer, + var_file_zipzer, + machine["uuid_serial_machine"], + segment_size=8000, + type_transfert="location", + location=location, + contenttype=contenttype, + ) else: - logger.error("le directory [%s] a envoyer n'existe pas %s " % data['path_fichier']) + logger.error( + "le directory [%s] a envoyer n'existe pas %s " + % data["path_fichier"] + ) except Exception: logger.error("%s" % (traceback.format_exc())) + def generer_name_avec_timestamp(jid_dest_backup, pathnamefile, millisecondes=False): """ Génère un UUID aléatoire et y ajoute un timestamp. @@ -154,7 +181,7 @@ def verifier_cles_non_vides(data, cles): logger.debug("verifier_cles_non_vides") for cle in cles: if cle not in data: # Vérifie l'existence et la valeur non vide - logger.error("cle missing %s" % cle ) + logger.error("cle missing %s" % cle) return False if isinstance(data[cle], int): continue @@ -163,18 +190,20 @@ def verifier_cles_non_vides(data, cles): return True -def process_zip_files(input_dir, - output_dir_base_trunck, - uuid_serial_machine, - segment_size=8000, - type_transfert = "backup", - location = None, - contenttype = "directory"): +def process_zip_files( + input_dir, + output_dir_base_trunck, + uuid_serial_machine, + segment_size=8000, + type_transfert="backup", + location=None, + contenttype="directory", +): """ Lit tous les fichiers ZIP d'un répertoire, les découpe et enregistre les segments dans un répertoire de sortie basé sur le JID. contenttype file ou directory """ - output_dir_list=[] + output_dir_list = [] output_dir_base = f"{output_dir_base_trunck}/{uuid_serial_machine}" # Vérifier si le répertoire de base existe, sinon le créer @@ -192,12 +221,14 @@ def process_zip_files(input_dir, os.makedirs(output_dir) # Appeler la fonction split_file pour découper le fichier ZIP - split_file(file_path, - output_dir, - segment_size, - type_transfert, - location, - contenttype) + split_file( + file_path, + output_dir, + segment_size, + type_transfert, + location, + contenttype, + ) # logger.debug(f"Fichier {file_name} découpé et enregistré dans {output_dir}") try: @@ -208,14 +239,19 @@ def process_zip_files(input_dir, logger.error(f"Le fichier {file_name} n'existe pas.") return output_dir_list except PermissionError: - logger.error(f"Vous n'avez pas les permissions nécessaires pour effacer le fichier {file_name}.") + logger.error( + f"Vous n'avez pas les permissions nécessaires pour effacer le fichier {file_name}." + ) return output_dir_list except Exception as e: - logger.error(f"Une erreur s'est produite lors de l'effacement du fichier {file_name}: {e}") + logger.error( + f"Une erreur s'est produite lors de l'effacement du fichier {file_name}: {e}" + ) return output_dir_list output_dir_list.append(output_dir) return output_dir_list + def md5_hash(file_path): """Calcule le hash MD5 d'un fichier.""" hash_md5 = hashlib.md5() @@ -224,19 +260,21 @@ def md5_hash(file_path): hash_md5.update(chunk) return hash_md5.hexdigest() -def split_file(file_path, - output_dir, - segment_size=8000, - type_transfert = "backup", - location = None , - contenttype = "directory"): + +def split_file( + file_path, + output_dir, + segment_size=8000, + type_transfert="backup", + location=None, + contenttype="directory", +): """Découpe un fichier en segments et enregistre chaque segment sous forme de fichier JSON en base64.""" # Vérifier si le répertoire de sortie existe, sinon le créer # logger.error(f"split_file file_path {file_path}: ") # logger.error(f"split_file output_dir {output_dir}: ") # logger.error(f"split_file contenttype {contenttype}: ") - if not os.path.exists(output_dir): os.makedirs(output_dir) @@ -245,11 +283,11 @@ def split_file(file_path, file_size = os.path.getsize(file_path) total_parts = (file_size + segment_size - 1) // segment_size file_uuid = str(uuid.uuid4()) - directory_or_file= file_name + directory_or_file = file_name if file_name.endswith(".zip"): directory_or_file = file_name[:-4] try: - parties = file_name.split('_') + parties = file_name.split("_") # Prendre seulement la 3e partie directory_or_file = parties[2] except ValueError as ve: @@ -264,21 +302,21 @@ def split_file(file_path, part_num = 0 while chunk := f.read(segment_size): # Encodage de la partie en base64 - encoded_content = base64.b64encode(chunk).decode('utf-8') + encoded_content = base64.b64encode(chunk).decode("utf-8") # Création du fichier JSON pour cette partie part_data = { "namefile": file_name, - "directory":directory_or_file, + "directory": directory_or_file, "nb": part_num + 1, "nbtotal": total_parts, "content": encoded_content, "type": type_transfert, "location": location, - "contenttype": contenttype + "contenttype": contenttype, } # Formater le numéro de segment avec un format spécifique - segment_formatted = "{:06d}".format(part_num+1) + segment_formatted = "{:06d}".format(part_num + 1) part_file_name = f"{file_uuid}_{segment_formatted}.json" part_file_path = os.path.join(output_dir, part_file_name) @@ -291,15 +329,15 @@ def split_file(file_path, # en dernier permet de savoir si archive prete a l'envoi manifest_data = { "namefile": file_name, - "directory":directory_or_file, + "directory": directory_or_file, "creation": str(datetime.now()), "nb_total": total_parts, "md5": original_md5, - "type_file": "zip" if file_name.endswith('.zip') else "unknown", + "type_file": "zip" if file_name.endswith(".zip") else "unknown", "size_trunck": segment_size, "type": type_transfert, "location": location, - "contenttype": contenttype + "contenttype": contenttype, } manifest_file_name = f"{file_uuid}_000000.manif" @@ -313,34 +351,36 @@ def split_file(file_path, def remplacer_caracteres(chaine): # Remplacer @ par @@ - chaine = chaine.replace('@', '@64@') + chaine = chaine.replace("@", "@64@") # Remplacer les : par : - chaine = chaine.replace(':', '@58@') + chaine = chaine.replace(":", "@58@") # Remplacer les espaces par @nbsp@ - chaine = chaine.replace(' ', '@nbsp@') + chaine = chaine.replace(" ", "@nbsp@") # Remplacer les barres obliques / par / - chaine = chaine.replace('/', '@47@') + chaine = chaine.replace("/", "@47@") # Remplacer les barres obliques \ par \ - chaine = chaine.replace('\\', '@92@') + chaine = chaine.replace("\\", "@92@") # Remplacer . par \ - chaine = chaine.replace('.', '@46@') + chaine = chaine.replace(".", "@46@") return chaine + def restaurer_caracteres(chaine): - chaine = chaine.replace('@58@', ':') + chaine = chaine.replace("@58@", ":") # Remplacer @46@ par . - chaine = chaine.replace('@46@', '.') + chaine = chaine.replace("@46@", ".") # Remplacer @92@ par \ - chaine = chaine.replace('@92@', '\\') + chaine = chaine.replace("@92@", "\\") # Remplacer @47@ par / - chaine = chaine.replace('@47@', '/') + chaine = chaine.replace("@47@", "/") # Remplacer @nbsp@ par espace - chaine = chaine.replace('@nbsp@', ' ') + chaine = chaine.replace("@nbsp@", " ") # Remplacer @@ par @ - chaine = chaine.replace('@64@', '@') + chaine = chaine.replace("@64@", "@") return chaine + class ZipFileManager: """ Classe qui gère les fichiers ZIP dans un répertoire en conservant les fichiers les plus récents @@ -365,23 +405,25 @@ def analyze_and_cleanup(self): Garde uniquement le fichier ZIP le plus récent pour chaque combinaison unique de nom de fichier (sans timestamp). """ # Lire tous les fichiers .zip dans le répertoire - zip_files = [f for f in os.listdir(self.directory) if f.endswith('.zip')] + zip_files = [f for f in os.listdir(self.directory) if f.endswith(".zip")] # Analyser chaque fichier for file in zip_files: # Vérifier que le fichier correspond au format attendu : _.zip - match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + match = re.match(r"(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip", file) if match: timestamp, file_rest = match.groups() key = file_rest # Garder le fichier avec le plus grand timestamp (le plus récent) - if key not in self.files_to_keep or int(timestamp) > int(self.files_to_keep[key][0]): + if key not in self.files_to_keep or int(timestamp) > int( + self.files_to_keep[key][0] + ): self.files_to_keep[key] = (timestamp, file) # Supprimer les fichiers obsolètes for file in zip_files: - match = re.match(r'(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip', file) + match = re.match(r"(\d+)_([A-Za-z0-9@:_\-\\]+)\.zip", file) if match: timestamp, file_rest = match.groups() key = file_rest @@ -413,14 +455,16 @@ def zipper_fichier(fichier, fichier_zip, fichier_vide=True): Returns: None: La fonction crée un fichier zip et n'a pas de valeur de retour. """ - # Vérifier si le répertoire existe + # Vérifier si le répertoire existe if not os.path.exists(fichier): return None - with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(fichier_zip, "w", zipfile.ZIP_DEFLATED) as zipf: if os.path.islink(fichier): # Ajouter le lien symbolique en tant que tel dans le zip sans compression - zipf.write(fichier, os.path.basename(fichier), compress_type=zipfile.ZIP_STORED) + zipf.write( + fichier, os.path.basename(fichier), compress_type=zipfile.ZIP_STORED + ) else: # Inclure les fichiers vides si demandé if fichier_vide or os.path.getsize(fichier) > 0: @@ -428,11 +472,13 @@ def zipper_fichier(fichier, fichier_zip, fichier_vide=True): return True -def zipper_repertoire(repertoire, - fichier_zip, - resoudre_liens=False, - repertoire_vide=True, - fichier_vide=True): +def zipper_repertoire( + repertoire, + fichier_zip, + resoudre_liens=False, + repertoire_vide=True, + fichier_vide=True, +): """ Zipe le contenu d'un répertoire, y compris tous les fichiers, sous-répertoires, répertoires vides et fichiers vides. @@ -457,17 +503,23 @@ def zipper_repertoire(repertoire, if not os.listdir(repertoire): return None - with zipfile.ZipFile(fichier_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(fichier_zip, "w", zipfile.ZIP_DEFLATED) as zipf: # Parcourt tous les fichiers et sous-répertoires dans le répertoire spécifié for root, dirs, files in os.walk(repertoire): # Ajouter le répertoire courant (même s'il est vide) chemin_rel_repertoire = os.path.relpath(root, repertoire) - if repertoire_vide or files: # Inclut les répertoires vides ou ceux avec des fichiers - zipf.write(root, chemin_rel_repertoire) # Ajoute le répertoire courant au zip + if ( + repertoire_vide or files + ): # Inclut les répertoires vides ou ceux avec des fichiers + zipf.write( + root, chemin_rel_repertoire + ) # Ajoute le répertoire courant au zip for file in files: chemin_complet = os.path.join(root, file) # Chemin complet du fichier - chemin_rel = os.path.relpath(chemin_complet, repertoire) # Chemin relatif pour le zip + chemin_rel = os.path.relpath( + chemin_complet, repertoire + ) # Chemin relatif pour le zip if os.path.islink(chemin_complet): if resoudre_liens: