PCsoleil Informatique

Entreprise Individuelle
Services informatiques Brignoles et Centre-VAR | Professionnels et particuliers

Je me pointe chez une petite entreprise, elle a besoin de faire passer ses PC sous Windows 7 en version Windows 10 voire 11 (oui figurez-vous qu’il y en a encore, il était temps). Et… Je procède. Voilà, je pourrais arrêter là et me dire que j’ai créé l’article le plus court de mon blog, mais comme son titre n’exprime pas l’intervention évoquée je continue pour vous parler du reste ;

J’apprends que les données de l’entreprise sont placées dans un Synology DS220+ qui est physiquement présent dans les bureaux de la Société, dossiers accessibles par les 5 unités centrales et autant de salariés + gérant. Une sauvegarde a été configurée il y a quelques années, elle cible un disque dur externe branché directement au NAS. Je vérifie son bon fonctionnement : Évidemment elle ne fonctionne plus, depuis 2024… Et nous sommes en 2026.

Aucun étonnement n’émane de ma part, c’est presque toujours comme ça : une MAJ de l’O.S. trop restrictive, un fichier détérioré que n’arrive pas à interpréter le système de backup, un protocole de flux empêché par l’évolution de sécurité des systèmes, etc… Tout concourt à stopper sans bruit la bienveillante mission de cet automatisme.

J’ai déjà réfléchi à un backup « instoppable », au moins dans son aspect notification d’erreur. Le triptyque Restic, Healthchecks, Notification double Mail / WhatsApp va bientôt s’ébranler et s’élancer sous vos yeux.

Réglages préliminaires

NAS Synology DS220+

Je m’identifie en local dans les locaux de l’entreprise via mon PC portable (des restrictions empêchent souvent les réglages de sécurité en utilisant un accès externe type Quick Connect). La sauvegarde en défaut est lancée depuis l’App Hyper Backup, les logs indiquent un problème de droits sur les dossiers du disque dur externe, mais je n’ai pas regardé plus que ça. Je souhaite désactiver ce backup et mettre ma solution en place. NB : Je me crée un compte admin bien à moi, protégé par la double authentification via l’App android Secure Signin de Synology que je mets sur mon propre Smartphone.

J’apprends que la communauté Synology a mis en ligne un package du puissant outil de backup Restic. Je ne vais pas faire un exposé de sa pertinence ; J’en ai déjà parlé . L’outil évolue en résolvant ses bugs depuis 2015, et ses compétiteurs (Kopia, Plakar, Borg) ont moins de bouteilles pour un résultat équivalent dans cet exercice donc…

Sur l’interface du NAS je vais sur Centre de paquets > Paramètres > Sources de paquet et rentre l’URL https://packages.synocommunity.com/ . Je clique sur « Tous les paquets » et l’App Restic apparait (version 0.18.1.1 présentement) et je l’installe. Je vérifie aussi que Python (package officiel cette fois) est bien installé : OK.

Puis Panneau de configuration > Périphériques externes. Le disque externe se montre. Onglet Paramètres : Je place mon compte Admin Synology comme le seul autorisé à lire et écrire sur ce volume. PS : Oui je sais un backup sur un disque USB connecté au NAS ce n’est pas « secure », mais la société a prévu de se munir d’un autre NAS, placé physiquement chez le gérant. A ce moment je modifierai la cible de rétention pour ce dernier.

Ensuite, toujours Panneau de configuration, mais Dossiers partagés. Bien sûr l’ancien admin système a créé un dossier de sauvegarde nommé usbshare1 sur le disque externe, mais je serre la vis de la sécurité en cochant dans l’onglet Modifier les options « Cacher ce dossier partagé » et « Masquer les sous-dossiers et les fichiers des utilisateurs sans autorisations » puis onglet Permissions je m’autorise exclusivement en Lecture / Ecriture. Je crée aussi le dossier restic_cache auquel j’applique les mêmes sécurités (pas sur le disque externe ; sur le disque du NAS).

Les dossiers de travail à sauvegarder de la société sont présents : Je vois Commun, Comptabilité et Direction. 

Direction File Station : Le dossier usbshare1 du disque externe contient un ancien backup d’Hybrid share que je garde par prévenance, mais je crée à côté le dossier qui contiendra les backups de Restic: restic_repo. Il faudra bien un dossier qui contienne les scripts ; J’utiliserai le moment venu un sous-dossier déjà conçu dans le combo de disques durs internes : Appli/Scripts.

Pour cette intervention j’ai dû beaucoup tester mes scripts en ligne de commande ; J’active donc le service SSH, encore sous le Panneau de configuration > Terminal & SNMP. L’accès SSH externe sera bloqué via le routeur mais c’est une autre histoire. Rappel : Presque tout la config. est effectuée dans les bureaux de la PME.

Déjà, attention au fait que les noms des dossiers sur l’interface GUI du NAS peuvent très bien être différents de leurs « vrai » nom. Pour les révéler, je me rends sur l’interface du NAS dans File Station et note les bons chemins (clic droit sur un dossier et propriété), ceux des dossiers qui seront appelés dans mes scripts. Sinon les scripts qui se basent dessus vont se plaindre d’un « Folder not Found ».

Dommage qu’on ne puisse plus directement ouvrir une ligne de commande sur l’interface du NAS (on pouvait avant avec Gate One). Je me connecte donc en SSH depuis mon PC portable sous Windows 11. Les lignes précédées d’un # ne sont pas des commandes mais des commentaires.

Premier backup en ligne de commande

				
					ssh mon_nom_utilisateurn@adresse_ip_local_NAS
# Je rentre mon mot de passe
# Je m'attribue les droits nécessaires sur les dossiers de dépôt de cache Restic
chmod -R 755 /volumeUSB1/usbshare/restic_repo
chmod -R 755 /volume1/restic_cache
# Je prépare avec Restic le dossier (dépôt) qui accueillera les backup
restic init --repo /volumeUSB1/usbshare/restic_repo
# Je crée le mot de passe du dépôt
# Je lance un 1er backup pour voir si tout se passe bien
restic -r /volumeUSB1/usbshare/restic_repo --verbose backup /volume1/Commun /volume1/Comptabilite /volume1/Direction --cache-dir /volume1/restic_cache
				
			

Vous vous demandez peut-être pourquoi je déclare un dossier de cache ? Parce que sans ça, Restic se plaint qu’il ne le trouve pas, sans doute parce que mon utilisateur Syno n’a pas de dossier dédié (Home comme on dit sous Linux). En tout cas la copie des 135 Go abouti sous une demi-heure, et le résultat pesé est de bien moindre volume que la somme des dossiers de travail (merci la déduplication). Soit 91 Go ! 

Les scripts Python

Je ne parlerai pas de Healthchecks.io car j’ai déjà écrit un article sur la façon de l’utiliser. Pour rappel c’est un service en ligne (gratuit pour 20 Checks) que les scripts vont pinger pour dire si le backup s’est bien déroulé. 

Par contre je vais expliciter l’autre service de notification ; Ntfy. (Je vais doubler les notifications par redondance et sécurité même si HealthChecks.io est sûr). Son installation est déroutante de simplicité : Télécharger l’App « ntfy – PUT/POST to your phone » sur L’App Store ou Play Store. Pas d’incription, rien. Et voilà ; Merci de votre attention.

PS: J’ai créé un fichier texte qui va accueillir les logs de Restic depuis File Station, dans volume1/Appli/Scripts que j’appelle restic_backup_errors.txt. Comme ça nul besoin de rajouter dans le script Python un « If Log exist blabla.. Else etc.. » 

Scripts de Backup et de vérification

				
					import os
import subprocess
import requests
import logging

from restic_config import (REPOSITORY, PASSWORD, CACHE, LOG, SOURCES, EXCLUDES, FAIL_URL, NTFY_URL, SUCCESS_URL)

env = os.environ.copy()
env["RESTIC_REPOSITORY"] = REPOSITORY
env["RESTIC_PASSWORD"] = PASSWORD
env["RESTIC_CACHE_DIR"] = CACHE

logging.basicConfig(filename = LOG, level = logging.ERROR, format = "%(asctime)s | %(message)s", datefmt = "%d/%m/%Y %H:%M:%S")

with open(LOG, a+", encoding= "utf-8") as log:
    log.seek(0)
    lines= log.readlines()
    log.seek(0)
    log.writelines(lines[-365:])
    log.truncate()

unlock_command = ["restic", "unlock"]
backup_command = ["restic", "backup", *SOURCES, *EXCLUDES, "--no-scan"]
check_command = ["restic", "check"]
forget_prune_command = ["restic", "forget", "--keep-daily", "7", "--keep-weekly", "52", "--keep-monthly", "24", "--keep-yearly", "22", "--prune"]

def run(command): return subprocess.run(command, env = env, text = True, stderr = subprocess.PIPE)

def stop_if_error(result):
    if result.returncode != 0:
        command = " ".join(result.args)
        error_message = (f"BACKUP ERREUR | Commande: {command} | Code retour: {result.returncode} | Erreur: {result.stderr}")             
        logging.error(error_message)
        requests.get(FAIL_URL, timeout = 10)
        requests.post(NTFY_URL, data = error_message.encode("utf-8"), timeout = 10)
        raise SystemExit(result.returncode)

unlock_result = run(unlock_command)
stop_if_error(unlock_result)

backup_result = run(backup_command)
stop_if_error(backup_result)

check_result = run(check_command)
stop_if_error(check_result)

forget_prune_result = run(forget_prune_command)
stop_if_error(forget_prune_result)

requests.get(SUCCESS_URL, timeout = 10)
				
			

Au début du script, les imports chargent les modules Python nécessaires.

Ligne 6: « from restic_config import… » C’est la façon de coder pour aller chercher des informations dans un autre fichier python, nommé restic_config.py et situé dans le même dossier que le fichier restic_backup.py (non je n’ai pas oublié « .py » après restic_config dans le script). Plutôt que charger le script principal, je déporte les infos dessus, un peu comme une base de données.

Lignes 8 à 11: env[« RESTIC_REPOSITORY »] et le reste des env: Ce sont des variables que comprend nativement Restic. Ici je définie le dépôt des backups comme ça je pourrais au cas où le changer facilement, plutôt que d’alourdir les commandes Restic.

Ligne 13: « logging.basicConfig… »: Un log sera écrit dans le dossier dont le chemin est dans la variable LOG. Mais pas à chaque fois que le script se lancera, seulement sur une erreur de backup (mais on verra cela dans la suite des explications).

Lignes 15 à 20: Lorsque le Log sera long de plus de 365 lignes, les lignes du début du Log (les plus anciennes donc) seront effacées.

Ligne 22 à 25: Commandes qui seront appelées plus tard par les scripts des lignes 36, 39, 42, 45.

Ligne 27: Lorsque Python exécutera les Run des lignes citées précédemment, il lancera la ligne 27 où la variable command prend la valeur du code Restic lancé (ex: restic, unlock). Tout cela dans un sous process qui comportera: La commande Restic, l’environnement (les valeurs des variables comme le chemin vers le dépôt Restic, le mot de passe protégeant celui-ci, etc..). Puis on signifie que la commande est à interpréter en texte et pas en binaire et qu’il faut capturer le « débriefing » d’erreur généré par Restic, comme ça on pourra l’a récupérer en tant que variables pour l’afficher quand on veut.

Lignes 29 à 36 : La fonction s’exécute lorsque les commandes stop_if_error plus bas se lancent. Elle dit que, si la sortie erreur n’est pas 0, alors on écrit le message d’erreur Restic dans le Log et on envoie un ping d’échec à Healthchecks.io ainsi qu’un message via NTFY sur le téléphone (façon WhatsApp) avec dedans un texte évoquant l’erreur, choisi par mes soins, suivi de la sortie erreur Restic. Ensuite le script est arrêté purement et simplement, du coup si Python exécute la commande Unlock et qu’il y a une erreur, les commandes plus bas ne sont pas exécutées (backup etc..) et encore moins la dernière censée envoyer un ping Healthchecks de succès.

Lignes 38 à 48 : Là se situent les fonctions qui vont se lancer automatiquement, cela paraît peu intuitif de placer les scripts exécutables à ce niveau, cependant, Python doit connaitre d’abord les informations et les processus sous-jacents avant. Mais finalement c’est logique ! Ex: Avant de s’élancer dans les chemins un randonneur doit savoir où aller et connaitre les « sous-process » utiles (faire une triangulation, du feu…). Python doit tout savoir en détail comme un perdreau de l’année.

Pour être plus clair sur le déroulement : Python ingurgite les infos du script et rencontre la première commande exécutable : unlock_result = run (unlock_command), Python lit la valeur de la variable unlock_command (ligne 22), qui est une simple commande restic de déverrouillage du Dépôt, et la place telle quelle dans la variable command de la fonction def run(command) de la ligne 27. 

Cette fonction exécute la commande Restic en lui donnant toutes les variables requises grace à env (chemin du dépôt, son mot de passe, chemin du cache Restic), enfin il mémorise la sortie que Restic fait à chaque fois qu’il finit ses exécutions (Code erreur, message disant que le backup s’est bien déroulé, ou message disant le contraire avec l’erreur qu’il a rencontré comme un mot de passe de dépôt erroné, un chemin vers le backup introuvable). Seule les débriefs Restic tagués Erreur seront consignés (stderr).

Ces infos sont mémorisées dans la variable unlock_result sous forme d’un objet textuel contenant plusieurs informations. Dans ces infos on a les valeurs de unlock.args (c’est la commande restic) mais aussi unlock.returncode (0 si ça s’est bien passé, ou par exemple 12 si mot de passe du dépôt faux).

Commande unlock_result = run(unlock_command) fini. Qu’a-t-on après ? la commande stop_if_error(unlock_result). Python vérifie comment exécuter précisément cette fonction en décortiquant la ligne 29, qu’il lance alors. Il fait prendre à la variable result les valeurs de la variable unlock_result.

Si la « sous variable » result.returncode n’est pas 0, c’est que Restic a rencontré un problème, alors le Log est enrichi d’une ligne datée avec le souci rencontré, un ping fail est envoyé à Healthchecks.io, une notification Push est envoyée sur mon Tel auprès de l’App NTFY et le script est stoppé (raise SystemExit etc..).

Cette logique continue avec le reste des commandes Lignes 41 à 48 (backup_result = run(backup_command) puis stop_if_error) etc… A chaque fois si Restic se plaint, le script est brutalement stoppé avec raise SystemExit(result.returncode).

Mais si aucune erreur n’est rencontrée, la suite du code abouti naturellement à la commande requests.get(SUCCESS_URL, timeout = 10) c-à-d l’envoie d’un ping de succès au service Healthchecks.io (la variable SUCCESS_URL contenant le lien https en question).

Que contient le script restic_config.py placé au même niveau de dossier que restic_backup ? Ceci:

				
					REPOSITORY = "/volumeUSB1/usbshare/restic_repo"
PASSWORD = "Henonpasoubliédanonymiser"
CACHE = "/volume1/restic_cache"
LOG = "/volume1/Appli/Scripts/restic_backup_errors.txt"
SOURCES = ["/volume1/Commun", "/volume1/Comptabilite", "/volume1/Direction"]

EXCLUDES = [
    "--iexclude", "**/@eaDir/**", "--iexclude", "**/@Recycle/**", "--iexclude", "**/#recycle/**", "--iexclude", "**/~$*",
    "--iexclude", "**/*.tmp", "--iexclude", "**/thumb.db", "--iexclude", "**/desktop.ini", "--iexclude", "/volume*/@*"
]

NTFY_URL = "https://ntfy.sh/làaussijyaipense"
FAIL_URL = "https://hc-ping.com/ainsiquecettepartie/fail"
SUCCESS_URL = "https://hc-ping.com/sansoubliercellecionnestpasunperdreau"


				
			

Sauvegarder est un geste bien prévenant mais vérifier les backups s’impose, alors j’ai créé un autre script pour lire entièrement le dépôt Restic et m’alerter si problème, que ce soit une corruption du disque dur le contenant, une modification malveillante des données. De toute façon, du moment que le code retour Restic n’est pas 0 je serais prévenu. 

Vous me direz que j’avais pourtant placé dans le script principal une commande de check de backup, mais elle ne fait que vérifier la cohérence du dépôt, et je ne peux pas intégrer la commande de check intégral car elle serait trop longue à s’exécuter quotidiennement. En effet la commande restic check –read-data de mon script additionnel relit l’intégralité des données… 

Je nomme le fichier contenant ce script restic_check_full.py et le place dans le même dossier que les autres. Il utilise aussi restic_config.py comme source d’infos.

				
					import os
import subprocess
import requests
import logging

from restic_config import (REPOSITORY, PASSWORD, CACHE, LOG, FAIL_URL, NTFY_URL, SUCCESS_URL)

env = os.environ.copy()
env["RESTIC_REPOSITORY"] = REPOSITORY
env["RESTIC_PASSWORD"] = PASSWORD
env["RESTIC_CACHE_DIR"] = CACHE

logging.basicConfig(filename = LOG, level = logging.ERROR, format = "%(asctime)s | %(message)s", datefmt = "%d/%m/%Y %H:%M:%S")

with open(LOG, "r+", encoding= "utf-8") as log:
    log.seek(0)
    lines = log.readlines()
    log.seek(0)
    log.writelines(lines[-365:])
    log.truncate()

check_full_command = ["restic", "check", "--read-data"]

def run(command): return subprocess.run(command, env = env, text = True, stderr= subprocess.PIPE)

def stop_if_error(result):
    if result.returncode != 0:
        logging.error("RESTIC ERROR PLANIFIED TASK SYNO NAS - COMMAND: %s - CODE RETOUR %s : %s", result.args, result.returncode, result.stderr)
        requests.get(FAIL_URL, timeout = 10)
        requests.post(NTFY_URL, data = f"RESTIC ERROR PLANIFIED TASK SYNO NAS - COMMAND: {result.args} - CODE RETOUR: {result.returncode}".encode("utf-8"), timeout = 10)
        raise SystemExit(result.returncode)

check_full_result = run(check_full_command)
stop_if_error(check_full_result)

requests.get(SUCCESS_URL, timeout = 10)
				
			

Réglage de ntfy et Heatlchecks

L’Install de l’App en question est facile, et son paramétrage tout autant ; Sur mon Tel j’ouvre ntfy, clic sur le signe + en bas à droite et choisis un nom à mon alerte. Attention, ce nom complète l’URL https://ntfy.sh/. On doit placer ici un nom du genre restic_backup_notify car cela va générer une URL. Dans l’exemple c’est https://ntfy.sh/restic_backup_notify. 

Cette URL est insérée dans restic_config.py en tant que valeur pour la variable NTFY_URL.

J’effleure juste la configuration des notifications envoyées à Healthchecks; Comme je l’ai dit précédemment j’avais fait un article dessus donc je dirais brièvement ceci : Renseigner dans restic_config.py la variable FAIL_URL en lui donnant la valeur du ping Healthchecks Fail : Sur Healthchecks.io lors de la création d’un Check, l’interface vous attribue deux choses ; Une adresse email et une URL qui ressemble à ceci : https://hc-ping.com/04e4a973-d9be-4e92-80aa-77bcef8bc12b.

Cette URL devra être renseignée comme valeur pour la variable FAIL_URL de restic_config.py en la complétant avec /fail à la fin, ce qui donne : https://hc-ping.com/04e4a973-d9be-4e92-80aa-77bcef8bc12b/fail.

Ainsi Healthchecks comprendra en recevant cette requête (ou ping comme ils disent) que la sauvegarde a échoué et qu’il faut prévenir par Email le responsable désigné dans l’onglet Intégration du compte Healthchecks.io .

Le réglage CRON contient l’expression : 30 22 * * 1-5. Explications : Je dis à Healthchecks de s’attendre à recevoir un ping de succès à 22H30 tous les jours de tous les mois, mais l’expression 1-5 affine cette notion de quotidienneté en signifiant finalement de ne pas s’attendre à recevoir le ping autrement que du lundi au vendredi (un peu tordu mais c’est comme ça). 

La Grace Time est réglée sur 3 jours, c-à-d que même si Healthchecks ne reçoit pas de ping -disons un vendredi à 22H30- il attendra 3 jours avant d’alerter par les moyens qui lui sont donnés (Email, SMS, notifications push diverses), donc il patientera jusqu’au lundi prochain à 22H30 dernier carat pour envoyer l’alerte.

Tâches planifiées sur le NAS

Pour automatiser les backups quotidiens et la vérification intégrale de ce dernier, j’élabore deux tâches planifiées différentes. 

Panneaux de configuration > Planificateur de tâches > Bouton Créer > Tâche planifiée > Script défini par l’utilisateur. 

Commençons par le script principal restic_backup.py ; Onglet tâche je nomme ma tâche, je place mon nom d’admin NAS en tant que concepteur de celle-ci. Onglet programmer je choisis Toutes les semaines (cela donne le choix des jours) et coche tous les jours à 22H, sauf samedi et dimanche . Onglet Paramètres de tâche j’écris la commande suivante dans le champ « Script défini par l’utilisateur » :

cd /volume1/Appli/Scripts && python restic_backup.py

Ainsi le NAS comprendra qu’il devra se « placer » dans le répertoire contenant les scripts python et après (&&) lancer la commande. Normalement, pas besoin de renseigner le cheminement vers le programme Python ; Il devrait être connu de fait par le Synology.

Et pour le script de Check intégral je réédite une tâche semblable mais je change ses occurrences pour seulement chaque dimanche, à 7H du matin. Le champ « Script défini par l’utilisateur » est enrichi du code : 

cd /volume1/Appli/Scripts && python restic_check_full.py

Et voilà, si je n’ai rien oublié dans cet article, la configuration de la sauvegarde est terminée. Si la PME m’appel à l’avenir pour m’alerter qu’elle a effacé ou perdu des documents je lancerais : Soit une commande Restic de restauration, soit une application appelée Restic Browser depuis mon PC portable, dans les locaux Sté.

Restic browser est un outil capable d’ouvrir le dépôt Restic et l’afficher tel que l’explorateur Windows affiche les dossiers de votre ordinateur ; Si mon obligé se rappel précisément des noms et chemins des fichiers disparus, la restauration, ainsi ciblée, sera plus rapide et intelligente (on disait Smart il y a quelques petites années déjà).

Besoin d'un devis informatique ?
Parlons-en !

Formulaire devis 2
Téléphone Demande complexe ou difficile à décrire ? Appelez au 06.28.07.77.83

Diagnostic de 35€ offert si devis accepté

Demande de devis