[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
"""Soustraire UNE VM au cache : l'exception par adresse MAC.
|
|
|
|
|
|
|
|
|
|
Le détournement est transparent et vaut pour tout le pont. Une VM ne peut pas
|
|
|
|
|
s'y soustraire de l'intérieur, et lui retirer l'autorité ne la dispense de
|
|
|
|
|
rien : elle est interceptée quand même et échoue sur un certificat qu'elle ne
|
|
|
|
|
reconnaît pas. L'exception se pose donc sur l'HÔTE, et elle a besoin d'un
|
|
|
|
|
identifiant que la VM porte de façon stable.
|
|
|
|
|
|
|
|
|
|
D'où ce que ces tests tiennent. L'adresse MAC est choisie AVANT la création :
|
|
|
|
|
la lire après laisserait la fenêtre où cloud-init télécharge déjà. Une MAC
|
|
|
|
|
posée à la main l'emporte sur le tirage. Et l'exception l'emporte sur
|
|
|
|
|
l'autorité, qu'il serait absurde de faire approuver à une VM qui ne
|
|
|
|
|
rencontrera jamais le cache.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
|
|
|
sys.path.insert(0, str(RACINE))
|
|
|
|
|
|
|
|
|
|
from script.qemu import deploy_qemu # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FauxRunner:
|
|
|
|
|
"""Retient les commandes. Rend ce qu'on lui a dit de rendre."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, reponses=None):
|
|
|
|
|
self.commandes = []
|
|
|
|
|
self.reponses = reponses or {}
|
|
|
|
|
self.use_sudo = True
|
|
|
|
|
self.dry_run = False
|
|
|
|
|
|
|
|
|
|
def run(self, cmd, *, privileged=False, check=True, capture=False):
|
|
|
|
|
self.commandes.append(list(cmd))
|
|
|
|
|
for motif, reponse in self.reponses.items():
|
|
|
|
|
if motif in " ".join(cmd):
|
|
|
|
|
return reponse
|
|
|
|
|
return (0, "") if capture else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def args_neufs(**kw):
|
|
|
|
|
base = dict(
|
|
|
|
|
name="vm-essai",
|
|
|
|
|
network="network=default,model=virtio",
|
|
|
|
|
cache_bypass=True,
|
|
|
|
|
cache_ca="",
|
|
|
|
|
distro="arch",
|
|
|
|
|
dry_run=False,
|
|
|
|
|
)
|
|
|
|
|
base.update(kw)
|
|
|
|
|
return argparse.Namespace(**base)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLaMACChoisie(unittest.TestCase):
|
|
|
|
|
def test_une_mac_deja_demandee_lemporte(self):
|
|
|
|
|
"""Une MAC posée à la main sert une réservation DHCP : la remplacer
|
|
|
|
|
casserait ce que l'appelant a réglé ailleurs."""
|
|
|
|
|
reseau = "network=default,model=virtio,mac=52:54:00:de:ad:be"
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
deploy_qemu.mac_du_network(reseau), "52:54:00:de:ad:be"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
deploy_qemu.network_avec_mac(reseau, "52:54:00:11:11:11"), reseau
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_sans_mac_elle_est_ajoutee(self):
|
|
|
|
|
reseau = deploy_qemu.network_avec_mac(
|
|
|
|
|
"network=default,model=virtio", "52:54:00:11:22:33"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
reseau, "network=default,model=virtio,mac=52:54:00:11:22:33"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
deploy_qemu.mac_du_network(reseau), "52:54:00:11:22:33"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_la_mac_tiree_porte_le_prefixe_qemu(self):
|
|
|
|
|
mac = deploy_qemu.mac_neuve(set())
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
mac.startswith(deploy_qemu.MAC_PREFIXE),
|
|
|
|
|
f"{mac} n'est pas une adresse QEMU",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(len(mac.split(":")), 6)
|
|
|
|
|
|
|
|
|
|
def test_une_mac_prise_nest_pas_retiree(self):
|
|
|
|
|
"""Libvirt refuse un doublon, mais à la création — après le
|
|
|
|
|
téléchargement de l'image. Le refus doit venir avant."""
|
|
|
|
|
prises = set()
|
|
|
|
|
for _ in range(30):
|
|
|
|
|
mac = deploy_qemu.mac_neuve(prises)
|
|
|
|
|
self.assertNotIn(mac, prises)
|
|
|
|
|
prises.add(mac)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLExceptionEstPoseeAvant(unittest.TestCase):
|
|
|
|
|
def test_la_mac_entre_dans_le_reseau_avant_la_creation(self):
|
|
|
|
|
args = args_neufs()
|
|
|
|
|
runner = FauxRunner({"is-active": (0, "")})
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu.os.path, "isfile", return_value=True
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu.shutil, "which", return_value="/usr/bin/nft"
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu, "macs_deja_prises", return_value=set()
|
|
|
|
|
),
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
):
|
|
|
|
|
mac = deploy_qemu.cache_bypass_apply(args, runner)
|
|
|
|
|
self.assertTrue(mac, "aucune MAC retenue")
|
|
|
|
|
self.assertIn(f"mac={mac}", args.network)
|
|
|
|
|
pose = [c for c in runner.commandes if "--bypass-add" in " ".join(c)]
|
|
|
|
|
self.assertTrue(pose, "l'exception n'a pas été posée")
|
|
|
|
|
self.assertIn("nft -f -", " ".join(pose[0]))
|
|
|
|
|
|
2026-09-16 03:00:17 -04:00
|
|
|
def test_le_message_du_binaire_suit_la_langue(self):
|
|
|
|
|
"""La commande passe par sudo, qui retire EL_LANG : sans l'option, le
|
|
|
|
|
binaire répondrait en français à qui déploie en anglais."""
|
|
|
|
|
args = args_neufs()
|
|
|
|
|
args.lang = "en"
|
|
|
|
|
runner = FauxRunner({"is-active": (0, "")})
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu.os.path, "isfile", return_value=True
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu.shutil, "which", return_value="/usr/bin/nft"
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu, "macs_deja_prises", return_value=set()
|
|
|
|
|
),
|
2026-09-16 03:00:17 -04:00
|
|
|
):
|
|
|
|
|
deploy_qemu.cache_bypass_apply(args, runner)
|
|
|
|
|
pose = [c for c in runner.commandes if "--bypass-add" in " ".join(c)]
|
|
|
|
|
self.assertIn("--lang en", " ".join(pose[0]))
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
def test_sans_cache_installe_rien_nest_pose(self):
|
|
|
|
|
"""Exiger un cache pour pouvoir s'en passer n'aurait aucun sens :
|
|
|
|
|
sans lui, rien n'intercepte."""
|
|
|
|
|
args = args_neufs()
|
|
|
|
|
runner = FauxRunner()
|
|
|
|
|
with mock.patch.object(
|
|
|
|
|
deploy_qemu.os.path, "isfile", return_value=False
|
|
|
|
|
):
|
|
|
|
|
self.assertEqual(deploy_qemu.cache_bypass_apply(args, runner), "")
|
|
|
|
|
self.assertEqual(runner.commandes, [])
|
|
|
|
|
self.assertNotIn("mac=", args.network)
|
|
|
|
|
|
|
|
|
|
def test_service_arrete_rien_nest_pose(self):
|
|
|
|
|
args = args_neufs()
|
|
|
|
|
runner = FauxRunner({"is-active": (3, "")})
|
|
|
|
|
with mock.patch.object(
|
|
|
|
|
deploy_qemu.os.path, "isfile", return_value=True
|
|
|
|
|
):
|
|
|
|
|
self.assertEqual(deploy_qemu.cache_bypass_apply(args, runner), "")
|
|
|
|
|
self.assertNotIn("mac=", args.network)
|
|
|
|
|
|
|
|
|
|
def test_sans_la_demande_rien_ne_bouge(self):
|
|
|
|
|
args = args_neufs(cache_bypass=False)
|
|
|
|
|
runner = FauxRunner()
|
|
|
|
|
self.assertEqual(deploy_qemu.cache_bypass_apply(args, runner), "")
|
|
|
|
|
self.assertEqual(runner.commandes, [])
|
|
|
|
|
|
|
|
|
|
def test_sans_nft_le_service_est_repose(self):
|
|
|
|
|
"""iptables n'a pas d'ensemble nommé : l'exception n'entre qu'en
|
|
|
|
|
reposant la chaîne, et seul un redémarrage la repose."""
|
|
|
|
|
args = args_neufs()
|
|
|
|
|
runner = FauxRunner({"is-active": (0, "")})
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu.os.path, "isfile", return_value=True
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(deploy_qemu.shutil, "which", return_value=None),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
deploy_qemu, "macs_deja_prises", return_value=set()
|
|
|
|
|
),
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
):
|
|
|
|
|
deploy_qemu.cache_bypass_apply(args, runner)
|
|
|
|
|
dit = [" ".join(c) for c in runner.commandes]
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
any("systemctl" in c and "restart" in c for c in dit),
|
|
|
|
|
f"le service n'est pas reposé : {dit}",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLExceptionLemporteSurLAutorite(unittest.TestCase):
|
|
|
|
|
def test_pas_dautorite_dans_une_vm_exceptee(self):
|
|
|
|
|
"""Elle ne rencontrera jamais le cache : poser cette signature dans
|
|
|
|
|
son magasin n'aurait aucun objet."""
|
|
|
|
|
args = args_neufs(cache_ca="/var/lib/erplibre_go_qemu_cache/ca.crt")
|
|
|
|
|
self.assertEqual(deploy_qemu.cache_files(args), [])
|
|
|
|
|
|
|
|
|
|
def test_la_ligne_de_commande_ne_porte_pas_les_deux(self):
|
|
|
|
|
from script.todo.qemu_deploy import QemuDeployMixin
|
|
|
|
|
|
|
|
|
|
parts = QemuDeployMixin._qemu_build_deploy_parts(
|
|
|
|
|
mock.MagicMock(_qemu_script_path=lambda: "deploy_qemu.py"),
|
|
|
|
|
"arch",
|
|
|
|
|
"latest",
|
|
|
|
|
"x86_64",
|
|
|
|
|
"vm",
|
|
|
|
|
4096,
|
|
|
|
|
2,
|
|
|
|
|
"20G",
|
|
|
|
|
"",
|
|
|
|
|
"master",
|
|
|
|
|
True,
|
|
|
|
|
cache_ca="/var/lib/erplibre_go_qemu_cache/ca.crt",
|
|
|
|
|
cache_bypass=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("--cache-bypass", parts)
|
|
|
|
|
self.assertNotIn("--cache-ca", parts)
|
|
|
|
|
|
|
|
|
|
def test_sans_exception_lautorite_passe(self):
|
|
|
|
|
"""Le cas courant : la VM approuve l'autorité et traverse le cache."""
|
|
|
|
|
from script.todo.qemu_deploy import QemuDeployMixin
|
|
|
|
|
|
|
|
|
|
parts = QemuDeployMixin._qemu_build_deploy_parts(
|
|
|
|
|
mock.MagicMock(_qemu_script_path=lambda: "deploy_qemu.py"),
|
|
|
|
|
"arch",
|
|
|
|
|
"latest",
|
|
|
|
|
"x86_64",
|
|
|
|
|
"vm",
|
|
|
|
|
4096,
|
|
|
|
|
2,
|
|
|
|
|
"20G",
|
|
|
|
|
"",
|
|
|
|
|
"master",
|
|
|
|
|
True,
|
|
|
|
|
cache_ca="/var/lib/erplibre_go_qemu_cache/ca.crt",
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("--cache-ca", parts)
|
|
|
|
|
self.assertNotIn("--cache-bypass", parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLeMenageDesExceptions(unittest.TestCase):
|
|
|
|
|
"""Une exception qui survit à sa VM est le danger de cette liste.
|
|
|
|
|
|
|
|
|
|
L'adresse MAC est libérée par la destruction et se réattribue : l'entrée
|
|
|
|
|
restée derrière soustrairait au cache une machine neuve que personne n'a
|
|
|
|
|
exceptée. Ni la VM ni le cache ne le diraient — la VM télécharge
|
|
|
|
|
normalement, le journal du cache reste seulement muet à son sujet.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_une_vm_disparue_rend_son_entree_orpheline(self):
|
|
|
|
|
from script.todo.qemu_cache_menu import QemuCacheMenuMixin as M
|
|
|
|
|
|
|
|
|
|
entrees = [
|
|
|
|
|
("52:54:00:00:00:01", "vm-vivante"),
|
|
|
|
|
("52:54:00:00:00:02", "vm-detruite"),
|
|
|
|
|
]
|
|
|
|
|
with mock.patch.object(
|
|
|
|
|
M, "_cache_domaines", return_value={"vm-vivante"}
|
|
|
|
|
):
|
|
|
|
|
orphelines = M._cache_bypass_orphelines(entrees)
|
|
|
|
|
self.assertEqual(orphelines, [("52:54:00:00:00:02", "vm-detruite")])
|
|
|
|
|
|
|
|
|
|
def test_une_entree_sans_nom_nest_jamais_orpheline(self):
|
|
|
|
|
"""Posée à la main : rien ne dit à quelle VM elle se rapporte, et la
|
|
|
|
|
retirer d'office déferait le travail de quelqu'un."""
|
|
|
|
|
from script.todo.qemu_cache_menu import QemuCacheMenuMixin as M
|
|
|
|
|
|
|
|
|
|
with mock.patch.object(M, "_cache_domaines", return_value=set()):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
M._cache_bypass_orphelines([("52:54:00:00:00:03", "")]), []
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_le_menage_retire_du_fichier_et_du_noyau(self):
|
|
|
|
|
"""Les deux : le fichier est ce que le service reposera, l'ensemble
|
|
|
|
|
est ce qui s'applique en ce moment."""
|
|
|
|
|
from script.todo import qemu_cache_menu as menu
|
|
|
|
|
|
|
|
|
|
execute = mock.MagicMock()
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(menu.os.path, "isfile", return_value=True),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
menu.QemuCacheMenuMixin,
|
|
|
|
|
"_cache_bypass_orphelines",
|
|
|
|
|
return_value=[("52:54:00:00:00:04", "partie")],
|
|
|
|
|
),
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
):
|
|
|
|
|
self.assertEqual(menu.bypass_menage(execute), 1)
|
|
|
|
|
cmd = execute.exec_command_live.call_args.args[0]
|
|
|
|
|
self.assertIn("--bypass-del 52:54:00:00:00:04", cmd)
|
|
|
|
|
self.assertIn("nft -f -", cmd)
|
|
|
|
|
|
|
|
|
|
def test_sans_cache_pose_le_menage_ne_fait_rien(self):
|
|
|
|
|
from script.todo import qemu_cache_menu as menu
|
|
|
|
|
|
|
|
|
|
execute = mock.MagicMock()
|
|
|
|
|
with mock.patch.object(menu.os.path, "isfile", return_value=False):
|
|
|
|
|
self.assertEqual(menu.bypass_menage(execute), 0)
|
|
|
|
|
execute.exec_command_live.assert_not_called()
|
|
|
|
|
|
|
|
|
|
def test_la_liste_est_lue_du_binaire(self):
|
|
|
|
|
"""Lui seul normalise une adresse et saute une ligne fautive : une
|
|
|
|
|
seconde lecture écrite ici dériverait de la sienne."""
|
|
|
|
|
from script.todo.qemu_cache_menu import QemuCacheMenuMixin as M
|
|
|
|
|
|
|
|
|
|
sortie = "52:54:00:aa:bb:cc vm-une\n52:54:00:11:22:33\n\n"
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(M, "_cache_lire", return_value=sortie),
|
|
|
|
|
mock.patch("os.path.isfile", return_value=True),
|
|
|
|
|
):
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
self.assertEqual(
|
|
|
|
|
M._cache_bypass_lire(),
|
|
|
|
|
[("52:54:00:aa:bb:cc", "vm-une"), ("52:54:00:11:22:33", "")],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLaSuppressionFaitLeMenage(unittest.TestCase):
|
|
|
|
|
def test_la_suppression_de_vm_appelle_le_menage(self):
|
|
|
|
|
"""Le seul endroit qui SAIT que la VM vient de disparaître."""
|
|
|
|
|
src = (RACINE / "script" / "todo" / "qemu_manage.py").read_text(
|
|
|
|
|
encoding="utf-8"
|
|
|
|
|
)
|
|
|
|
|
bloc = src[src.index("def _qemu_delete_vm(") :]
|
|
|
|
|
bloc = bloc[: bloc.index("\n def ")]
|
|
|
|
|
self.assertIn("bypass_menage(self.execute)", bloc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|