Contrat — Auto-sauvegarde embarquée du routeur (à corriger par l’app)
Public : équipe app mobile MikhmoAI. Rédigé le 2026-08-20.
L’app pose quatre tâches Moailte_Backup_* sur chaque routeur provisionné. Elles sont
posées à l’envers, et le défaut est silencieux : le script journalise un succès qu’il n’a
jamais vérifié. Ce document donne le corps exact à poser et la mesure qui tranche.
Le backend ne corrigera pas ces scripts — décision produit du 2026-08-20, cohérente avec « un seul écrivain » : c’est l’app qui écrit sur le routeur. Le backend constate l’état à chaque sauvegarde et le remonte, rien de plus.
L’ampleur, mesurée sur un échantillon de 20 routeurs (2026-08-20)
17 joignables, 3 injoignables. Chaque routeur a été interrogé directement.
| Constat | Routeurs |
|---|---|
| Scripts portant le corps sûr | 1 / 17 |
| Planificateurs appelant correctement leur script | 12 / 17 |
| Au moins un planificateur exécutant son PROPRE code (défaut n°2) | 2 / 17 |
| Aucun planificateur — les scripts existent, rien ne les lance | 2 / 17 |
| Rien du tout — ni script, ni planificateur | 1 / 17 |
Trois lectures s’imposent.
Le parc entier est sur le corps dangereux. Un seul routeur porte le corps sûr, et c’est
celui qui a servi de référence. Sur les 16 autres, un save qui échoue laisse le client
sans aucune sauvegarde.
Le défaut n°2 n’est pas anecdotique. Sur l’un des deux routeurs concernés, ce ne sont pas un mais les quatre planificateurs qui portent leur code en dur : aucune correction du corps des scripts ne l’atteindrait, quelle qu’elle soit.
Et un troisième cas apparaît, plus grave que les deux autres. Trois routeurs sur dix-sept n’ont aucun planificateur — deux ont pourtant leurs scripts, un n’a strictement rien. Leur auto-sauvegarde ne tourne donc jamais, et rien ne le signale : pas d’erreur, pas de journal, juste un fichier qui n’existe pas. Aucune correction de corps de script n’y changera quoi que ce soit ; seule la POSE des tâches, par l’app, le peut.
Défaut n°1 — le script efface AVANT d’écrire
Corps actuellement posé :
:local b "Moailte_Backup_24H.backup";
:local id [/file find name~".*$b"];
:if ([:len $id]>0) do={/file remove $id; :delay 5s;}; ← EFFACE D'ABORD
/system backup save name="Moailte_Backup_24H";
:log info "Moailte: AutoBackup -> $b";
Si le save échoue, le client n’a plus rien — pas une vieille sauvegarde, RIEN. Et la
ligne de journal annonce un succès sans l’avoir contrôlé.
Ce n’est pas théorique. Sur un routeur de référence, le planificateur avait démarré le script
la veille à 03:00 — last-started le prouve — et le fichier datait de 41 jours, sur
63 passages comptés. Lancé à la main, le même script réussit : ce n’est pas sa logique qui est
en cause, c’est son exécution planifiée. Sa conception, elle, garantit qu’un échec détruit
la sauvegarde au lieu de la conserver.
Le corps à poser
Écrire, vérifier, puis remplacer. À aucun instant le client ne se retrouve sans sauvegarde.
/file remove [find where name=Moailte_Backup_24H_new.backup];
/system backup save dont-encrypt=yes name=Moailte_Backup_24H_new;
:delay 8s;
:local n [/file find where name=Moailte_Backup_24H_new.backup];
:if ([:len $n]>0 && [/file get [:pick $n 0] size]>0) do={
/file remove [find where name=Moailte_Backup_24H.backup];
:delay 2s;
/file set [:pick $n 0] name=Moailte_Backup_24H.backup;
:log info Moailte-AutoBackup-OK-Moailte_Backup_24H
} else={:log error Moailte-AutoBackup-ECHEC-Moailte_Backup_24H}
Idem pour Moailte_Backup_48H, _15D, _30D.
Quatre points non négociables :
- le ménage d’entrée ne vise QUE
_new, jamais la sauvegarde du client. Une exécution coupée laisse un_newde ~7 Mo derrière elle — observé sur une flash où il ne restait que 19 Mo libres. Sans ce ménage, chaque interruption en ajoute un, et c’est la place manquante qui finit par empêcher la sauvegarde suivante ; - la taille est contrôlée (
size]>0). Sans ça, un fichier de 0 octet passe pour une sauvegarde valide et remplace la bonne — pire que ne rien faire ; - la suppression est DANS la branche de succès, jamais avant ;
- aucun guillemet double dans le corps. Il voyage dans une valeur
source="…": un guillemet interne demanderait un échappement dont la moindre erreur produit un script tronqué qui s’exécute sans rien faire.
Défaut n°2 — le planificateur quotidien ignore le script
C’est celui qu’on a failli manquer, et il annule la correction du n°1.
Sur le routeur de référence, trois planificateurs sur quatre appellent bien leur script :
Moailte_Backup_48H_Job on-event = Moailte_Backup_48H_Job ← appelle le script
Moailte_Backup_15D_Job on-event = Moailte_Backup_15D_Job
Moailte_Backup_30D_Job on-event = Moailte_Backup_30D_Job
Le quotidien, lui, porte le code d’origine EN DUR :
Moailte_Backup_24H_Job on-event = :local b "…";/file remove $id;…/system backup save…
Corriger le corps du script Moailte_Backup_24H_Job ne change donc rien : personne ne
l’appelle. Le job qui compte le plus est le seul que la correction n’atteint pas.
À faire : ramener le on-event du quotidien à la forme des trois autres —
on-event=Moailte_Backup_24H_Job — après avoir posé le script. Et seulement si le script
existe : pointer un on-event vers un nom inexistant supprime la sauvegarde au lieu de la
réparer.
Ni l’horaire, ni l’intervalle, ni le propriétaire, ni la politique ne doivent bouger.
La mesure qui tranche, quel que soit le transport
:local ok 0;
:foreach s in=[/system script find where name~"Moailte_Backup"] do={
:if ([:find [/system script get $s source] "_new.backup"]>=0) do={:set ok ($ok+1)}};
:local inline 0;
:foreach t in=[/system scheduler find where name~"Moailte_Backup"] do={
:if ([:find [/system scheduler get $t on-event] "backup save"]>=0) do={:set inline ($inline+1)}};
:put ("corriges=" . $ok . "|total=" . [:len [/system script find where name~"Moailte_Backup"]] .
"|sched=" . [:len [/system scheduler find where name~"Moailte_Backup"]] . "|inline=" . $inline)
Un routeur est sain si et seulement si total>0, corriges==total, sched>0 et
inline==0. Le sched>0 n’est pas décoratif : trois routeurs sur dix-sept mesurés ont des
scripts et aucun planificateur pour les lancer. Les trois premières conditions seules mentent : elles annonçaient « réparé » sur
le routeur de référence pendant que le quotidien effaçait toujours avant d’écrire.
C’est exactement cette lecture que le backend exécute après chaque sauvegarde, et dont il
persiste le verdict (HEALTHY / NEEDS_APP_FIX).
Le piège du $ — il dépend du transport
RouterOS interpole $var à l’intérieur des chaînes. Si l’app pose le script par une
commande CLI/SSH, les $ doivent être échappés : sans quoi les variables sont résolues au
moment de la POSE — à vide. Le script s’installe, passe tous les contrôles de présence, et ne
fait rien.
Par l’API RouterOS (protocole binaire) ou REST, la valeur est passée en paramètre et n’est jamais parsée comme script : aucun échappement n’est nécessaire.
Le test qui tranche, quel que soit le chemin :
:put [:len [/system script find where name="Moailte_Backup_24H_Job" and source~"_new.backup"]]
→ doit renvoyer 1
Un script présent qui renvoie 0 est inerte. C’est le sinistre déjà vécu avec le gardien
MoailteAI-Guard, dont toutes les variables avaient été résolues à vide à la pose.
Ce qui reste inexpliqué, et que cette correction ne résout PAS
Sur le routeur de référence, un Moailte_Backup_48H_new.backup de 6,8 Mio porte la date du
2026-08-20 03:00:04 : le save est allé au bout, puis tout ce qui suit a disparu — ni
contrôle, ni renommage, ni ligne de journal. Même signature que la panne d’origine : la
première commande passe, la suite s’évapore. La cause reste inconnue.
Le corps ci-dessus ne répare pas ça. Il garantit seulement qu’un tel échec ne détruit rien :
l’ancienne sauvegarde reste en place, et l’orphelin _new est nettoyé au passage suivant.