[FIX] déploiement et mesure : openSUSE, sudo, console, mémoire
Quatre défauts qu'une campagne complète sur les sept systèmes du catalogue a mis au jour. La famille zypper visait un faisceau de certificats qu'openSUSE n'écrit pas, et pip s'arrêtait là, en ligne comme hors ligne. sudo remet l'environnement à zéro : sans env_keep, une installation lancée par sudo rejette l'autorité du cache là où PAM ne relit pas /etc/environment. Le journal de console n'était gardé que pour la voie installateur, si bien qu'une image cloud bloquée avant ssh ne laissait rien à lire. Et une VM de 8 Gio restait allumée après sa mesure, épuisant la mémoire de l'hôte. --- EN --- Four faults a full campaign over the catalogue's seven systems brought out. The zypper family pointed at a certificate bundle openSUSE does not write, and pip stopped there, online as well as offline. sudo resets the environment: without env_keep, an install run by sudo rejects the cache authority where PAM does not reread /etc/environment. The console log was kept for the installer path only, so a cloud image stuck before ssh left nothing to read. And an 8 GiB VM stayed up after its measurement, exhausting the host's memory. Assisted-by: Claude Opus 5
This commit is contained in:
parent
f20e21b7d2
commit
15477d52ac
6 changed files with 236 additions and 18 deletions
|
|
@ -691,6 +691,27 @@ def dans_la_vm(adresse, commande, delai, journal, montrer=False):
|
||||||
return executer(ssh, delai, journal, montrer=montrer)
|
return executer(ssh, delai, journal, montrer=montrer)
|
||||||
|
|
||||||
|
|
||||||
|
def eteindre(nom, journal, dry_run=False):
|
||||||
|
"""Éteint une VM dont la mesure est faite, sans l'effacer.
|
||||||
|
|
||||||
|
Une VM de la charge « erplibre » tient 8 Gio de mémoire. Laissée allumée
|
||||||
|
pendant que la suivante s'installe, puis la troisième, elles s'additionnent,
|
||||||
|
et l'hôte manque de mémoire en pleine série. Éteinte, elle garde son disque
|
||||||
|
pour qui voudrait l'inspecter, et « --detruire » la retrouve par son nom.
|
||||||
|
Un échec d'extinction est dit et n'arrête rien : la mesure est déjà faite.
|
||||||
|
"""
|
||||||
|
cmd = f"sudo -n virsh -c qemu:///system destroy {shlex.quote(nom)}"
|
||||||
|
if dry_run:
|
||||||
|
dire(f" [à blanc] {cmd}", journal)
|
||||||
|
return
|
||||||
|
code, _ = executer(cmd, 120, journal)
|
||||||
|
if code:
|
||||||
|
dire(
|
||||||
|
f" ⚠ {nom} : extinction impossible ({code}), elle reste allumée",
|
||||||
|
journal,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def attendre_ssh(adresse, journal):
|
def attendre_ssh(adresse, journal):
|
||||||
for _essai in range(40):
|
for _essai in range(40):
|
||||||
code, _ = dans_la_vm(adresse, "true", 30, None)
|
code, _ = dans_la_vm(adresse, "true", 30, None)
|
||||||
|
|
@ -1310,6 +1331,9 @@ def _boucle(args, rapport, journal, acces, decalage):
|
||||||
}
|
}
|
||||||
ecrire_rapport(rapport)
|
ecrire_rapport(rapport)
|
||||||
dire(f" VM {rang} : paquets posés en {duree:.0f} s", journal)
|
dire(f" VM {rang} : paquets posés en {duree:.0f} s", journal)
|
||||||
|
# Sa mesure est dans le rapport : allumée, elle ne sert plus à rien et
|
||||||
|
# tient sa mémoire pendant que la suivante s'installe.
|
||||||
|
eteindre(nom, journal, args.dry_run)
|
||||||
|
|
||||||
ok = True
|
ok = True
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
|
|
||||||
|
|
@ -2507,10 +2507,14 @@ CACHE_TRUST = {
|
||||||
"update-ca-trust",
|
"update-ca-trust",
|
||||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||||
),
|
),
|
||||||
|
# openSUSE n'écrit aucun « /etc/ssl/certs/ca-certificates.crt » : son
|
||||||
|
# faisceau est « ca-bundle.pem ». Une variable qui vise un fichier absent
|
||||||
|
# fait échouer pip sur « Could not find a suitable TLS CA certificate
|
||||||
|
# bundle », en ligne comme hors ligne.
|
||||||
"zypper": (
|
"zypper": (
|
||||||
"/etc/pki/trust/anchors",
|
"/etc/pki/trust/anchors",
|
||||||
"update-ca-certificates",
|
"update-ca-certificates",
|
||||||
"/etc/ssl/certs/ca-certificates.crt",
|
"/etc/ssl/ca-bundle.pem",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2767,15 +2771,49 @@ def cache_commands(args: argparse.Namespace) -> list[str]:
|
||||||
f"sh -c 'grep -q ^{var}= /etc/environment"
|
f"sh -c 'grep -q ^{var}= /etc/environment"
|
||||||
f" || echo {var}={faisceau} >> /etc/environment'"
|
f" || echo {var}={faisceau} >> /etc/environment'"
|
||||||
)
|
)
|
||||||
|
gardees = list(CACHE_ENV_VARS)
|
||||||
if getattr(args, "offline", False):
|
if getattr(args, "offline", False):
|
||||||
for var, valeur in OFFLINE_ENV_VARS:
|
for var, valeur in OFFLINE_ENV_VARS:
|
||||||
commandes.append(
|
commandes.append(
|
||||||
f"sh -c 'grep -q ^{var}= /etc/environment"
|
f"sh -c 'grep -q ^{var}= /etc/environment"
|
||||||
f" || echo {var}={valeur} >> /etc/environment'"
|
f" || echo {var}={valeur} >> /etc/environment'"
|
||||||
)
|
)
|
||||||
|
gardees.append(var)
|
||||||
|
commandes.append(commande_sudoers(gardees))
|
||||||
return commandes
|
return commandes
|
||||||
|
|
||||||
|
|
||||||
|
CACHE_SUDOERS = "/etc/sudoers.d/erplibre-cache"
|
||||||
|
|
||||||
|
|
||||||
|
def commande_sudoers(variables, fichier: str = CACHE_SUDOERS) -> str:
|
||||||
|
"""La commande qui fait traverser « sudo » aux variables du cache.
|
||||||
|
|
||||||
|
sudo remet l'environnement à zéro. Seul un module PAM qui relit
|
||||||
|
/etc/environment pour sudo y ramène les variables, et toutes les
|
||||||
|
distributions ne le configurent pas : sans lui, « sudo npm install -g »
|
||||||
|
rejette l'autorité du cache sur « self-signed certificate in certificate
|
||||||
|
chain », quand le même npm sans sudo l'accepte. « env_keep » les garde
|
||||||
|
partout, pourvu que la session appelante les porte — ce que
|
||||||
|
cache_env_reload lui assure.
|
||||||
|
|
||||||
|
Le fichier est écrit sous un nom à point, que sudo ignore, vérifié par
|
||||||
|
« visudo -c », puis renommé : un fichier invalide dans sudoers.d rendrait
|
||||||
|
sudo inutilisable sur toute la machine. Un échec — visudo absent, pas de
|
||||||
|
répertoire sudoers.d — retire le temporaire et ne fait pas échouer la
|
||||||
|
commande. Une variable par ligne, sans guillemets : la commande passe
|
||||||
|
telle quelle dans un « runcmd » YAML comme dans un « sh -c » par ssh.
|
||||||
|
"""
|
||||||
|
dossier, nom = fichier.rsplit("/", 1)
|
||||||
|
tmp = f"{dossier}/.{nom}"
|
||||||
|
return (
|
||||||
|
f"sh -c 'for v in {' '.join(variables)};"
|
||||||
|
f" do echo Defaults env_keep += $v; done > {tmp}"
|
||||||
|
f" && chmod 0440 {tmp} && visudo -cf {tmp} && mv {tmp} {fichier}"
|
||||||
|
f" || rm -f {tmp}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def cache_runcmd(args: argparse.Namespace) -> list[str]:
|
def cache_runcmd(args: argparse.Namespace) -> list[str]:
|
||||||
"""Les mêmes gestes, emballés en éléments de « runcmd » pour cloud-init."""
|
"""Les mêmes gestes, emballés en éléments de « runcmd » pour cloud-init."""
|
||||||
return [f" - {c}" for c in cache_commands(args)]
|
return [f" - {c}" for c in cache_commands(args)]
|
||||||
|
|
@ -4022,16 +4060,12 @@ def virt_install(
|
||||||
"--network",
|
"--network",
|
||||||
args.network,
|
args.network,
|
||||||
"--console",
|
"--console",
|
||||||
# Journal de console pour la voie installateur. Une console « pty »
|
# Journal de console, pour toutes les voies. Une console « pty » seule
|
||||||
# seule ne gardE rien : quand d-i échoue, il l'écrit à l'écran d'une
|
# ne garde rien : quand d-i échoue, ou qu'une image cloud reste bloquée
|
||||||
# VM que personne ne regarde, et il ne reste RIEN à lire ensuite —
|
# avant son serveur ssh, la VM l'écrit à un écran que personne ne
|
||||||
# exactement « l'installation a échoué, pas de sortie pertinente ».
|
# regarde, et il ne reste RIEN à lire ensuite. Le fichier, lui, survit
|
||||||
# Le fichier, lui, survit à l'arrêt du domaine.
|
# à l'arrêt du domaine.
|
||||||
(
|
f"pty,target_type={console_target},log.file={console_log}",
|
||||||
f"pty,target_type={console_target},log.file={console_log}"
|
|
||||||
if installer
|
|
||||||
else f"pty,target_type={console_target}"
|
|
||||||
),
|
|
||||||
# Canal virtio de l'agent invité (org.qemu.guest_agent.0) : permet à
|
# Canal virtio de l'agent invité (org.qemu.guest_agent.0) : permet à
|
||||||
# virsh de piloter la VM SANS réseau (ex. étendre le FS invité après
|
# virsh de piloter la VM SANS réseau (ex. étendre le FS invité après
|
||||||
# un redimensionnement de disque). Inoffensif si l'agent est absent.
|
# un redimensionnement de disque). Inoffensif si l'agent est absent.
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,10 @@ func GuestTrustCommand(family string) (dir, cmd, bundle string, ok bool) {
|
||||||
"/etc/pki/tls/certs/ca-bundle.crt", true
|
"/etc/pki/tls/certs/ca-bundle.crt", true
|
||||||
case "zypper":
|
case "zypper":
|
||||||
// openSUSE range ses ancres ailleurs que la famille RHEL, tout en
|
// openSUSE range ses ancres ailleurs que la famille RHEL, tout en
|
||||||
// employant la même commande que Debian.
|
// employant la même commande que Debian ; son faisceau n'est pas celui
|
||||||
|
// de Debian, et une variable qui vise un fichier absent casse pip.
|
||||||
return "/etc/pki/trust/anchors", "update-ca-certificates",
|
return "/etc/pki/trust/anchors", "update-ca-certificates",
|
||||||
"/etc/ssl/certs/ca-certificates.crt", true
|
"/etc/ssl/ca-bundle.pem", true
|
||||||
}
|
}
|
||||||
return "", "", "", false
|
return "", "", "", false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ func TestCommandeDeConfiance(t *testing.T) {
|
||||||
},
|
},
|
||||||
"zypper": {
|
"zypper": {
|
||||||
"/etc/pki/trust/anchors", "update-ca-certificates",
|
"/etc/pki/trust/anchors", "update-ca-certificates",
|
||||||
"/etc/ssl/certs/ca-certificates.crt",
|
"/etc/ssl/ca-bundle.pem",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for famille, attendu := range cas {
|
for famille, attendu := range cas {
|
||||||
|
|
|
||||||
|
|
@ -366,5 +366,67 @@ class TestPlusieursSystemes(unittest.TestCase):
|
||||||
self.assertEqual(appels, [("ubuntu", DISTROS["ubuntu"][1])])
|
self.assertEqual(appels, [("ubuntu", DISTROS["ubuntu"][1])])
|
||||||
|
|
||||||
|
|
||||||
|
class TestUneVmMesureeEstEteinte(unittest.TestCase):
|
||||||
|
"""Deux VM de 8 Gio allumées ensemble, puis trois, épuisent la mémoire de
|
||||||
|
l'hôte en pleine série : chacune s'éteint dès que sa mesure est faite."""
|
||||||
|
|
||||||
|
def test_chaque_vm_s_eteint_apres_sa_propre_mesure(self):
|
||||||
|
import argparse
|
||||||
|
import tempfile
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
ordre = []
|
||||||
|
args = argparse.Namespace(
|
||||||
|
dry_run=False,
|
||||||
|
sans_cache=False,
|
||||||
|
hors_ligne=False,
|
||||||
|
distro="debian",
|
||||||
|
version="12",
|
||||||
|
charge="erplibre",
|
||||||
|
)
|
||||||
|
with tempfile.TemporaryDirectory() as rep:
|
||||||
|
rapport = {"_fichier": str(Path(rep) / "r.json"), "vms": []}
|
||||||
|
with mock.patch.object(QC, "dire"), mock.patch.object(
|
||||||
|
QC, "noter_uuid"
|
||||||
|
), mock.patch.object(
|
||||||
|
QC, "deployer", return_value="10.0.0.1"
|
||||||
|
), mock.patch.object(
|
||||||
|
QC, "attendre_ssh", return_value=True
|
||||||
|
), mock.patch.object(
|
||||||
|
QC,
|
||||||
|
"poser_les_paquets",
|
||||||
|
side_effect=lambda *a, **k: ordre.append("paquets") or True,
|
||||||
|
), mock.patch.object(
|
||||||
|
QC,
|
||||||
|
"eteindre",
|
||||||
|
side_effect=lambda nom, *a, **k: ordre.append(nom),
|
||||||
|
), mock.patch.object(
|
||||||
|
QC, "verdict", return_value=True
|
||||||
|
):
|
||||||
|
QC._boucle(args, rapport, None, "", 0)
|
||||||
|
base = QC.base_des_noms(args)
|
||||||
|
self.assertEqual(
|
||||||
|
ordre, ["paquets", f"{base}-1", "paquets", f"{base}-2"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_eteindre_n_efface_rien(self):
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
with mock.patch.object(QC, "dire") as dit:
|
||||||
|
QC.eteindre("vm-essai", None, dry_run=True)
|
||||||
|
annonce = " ".join(str(a) for c in dit.call_args_list for a in c.args)
|
||||||
|
self.assertIn("destroy vm-essai", annonce)
|
||||||
|
self.assertNotIn("undefine", annonce)
|
||||||
|
|
||||||
|
def test_a_blanc_rien_n_est_lance(self):
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
with mock.patch.object(QC, "dire"), mock.patch.object(
|
||||||
|
QC, "executer"
|
||||||
|
) as ex:
|
||||||
|
QC.eteindre("vm-essai", None, dry_run=True)
|
||||||
|
ex.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,15 @@ sys.path.insert(0, str(RACINE))
|
||||||
from script.qemu.deploy_qemu import ( # noqa: E402
|
from script.qemu.deploy_qemu import ( # noqa: E402
|
||||||
CACHE_CERT_NAME,
|
CACHE_CERT_NAME,
|
||||||
CACHE_ENV_VARS,
|
CACHE_ENV_VARS,
|
||||||
|
CACHE_SUDOERS,
|
||||||
CACHE_TRUST,
|
CACHE_TRUST,
|
||||||
OFFLINE_ENV_VARS,
|
OFFLINE_ENV_VARS,
|
||||||
|
cache_commands,
|
||||||
cache_env_reload,
|
cache_env_reload,
|
||||||
cache_family,
|
cache_family,
|
||||||
cache_files,
|
cache_files,
|
||||||
cache_commands,
|
|
||||||
cache_runcmd,
|
cache_runcmd,
|
||||||
|
commande_sudoers,
|
||||||
langue_des_messages,
|
langue_des_messages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -134,6 +136,10 @@ class TestRuncmd(unittest.TestCase):
|
||||||
|
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
self.lignes = cache_runcmd(faux_args(Path(self.tmp.name), "arch"))
|
self.lignes = cache_runcmd(faux_args(Path(self.tmp.name), "arch"))
|
||||||
|
# Les écritures de variables, sans la confiance ni le sudoers.
|
||||||
|
self.variables = [
|
||||||
|
l for l in self.lignes[1:] if "/etc/environment" in l
|
||||||
|
]
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.tmp.cleanup()
|
self.tmp.cleanup()
|
||||||
|
|
@ -159,13 +165,14 @@ class TestRuncmd(unittest.TestCase):
|
||||||
def test_les_variables_vont_dans_etc_environment(self):
|
def test_les_variables_vont_dans_etc_environment(self):
|
||||||
"""PAM lit /etc/environment pour TOUTE session ssh, non interactive
|
"""PAM lit /etc/environment pour TOUTE session ssh, non interactive
|
||||||
comprise : c'est la seule voie qui atteint une commande distante."""
|
comprise : c'est la seule voie qui atteint une commande distante."""
|
||||||
for ligne in self.lignes[1:]:
|
self.assertEqual(len(self.variables), len(CACHE_ENV_VARS))
|
||||||
self.assertIn("/etc/environment", ligne)
|
for var in CACHE_ENV_VARS:
|
||||||
|
self.assertTrue(any(f"{var}=" in l for l in self.variables), var)
|
||||||
|
|
||||||
def test_ecriture_idempotente(self):
|
def test_ecriture_idempotente(self):
|
||||||
"""runcmd ne tourne qu'une fois par instance, mais un opérateur peut
|
"""runcmd ne tourne qu'une fois par instance, mais un opérateur peut
|
||||||
rejouer la commande : elle ne doit pas empiler les doublons."""
|
rejouer la commande : elle ne doit pas empiler les doublons."""
|
||||||
for ligne in self.lignes[1:]:
|
for ligne in self.variables:
|
||||||
self.assertIn("grep -q", ligne)
|
self.assertIn("grep -q", ligne)
|
||||||
|
|
||||||
def test_aucune_commande_ne_peut_faire_echouer_le_boot(self):
|
def test_aucune_commande_ne_peut_faire_echouer_le_boot(self):
|
||||||
|
|
@ -258,6 +265,96 @@ class TestLeHorsLigneCoupeLAuditNpm(unittest.TestCase):
|
||||||
self.assertIn(var, cache_env_reload())
|
self.assertIn(var, cache_env_reload())
|
||||||
|
|
||||||
|
|
||||||
|
class TestLesVariablesTraversentSudo(unittest.TestCase):
|
||||||
|
"""sudo remet l'environnement à zéro : sans « env_keep », une installation
|
||||||
|
lancée par « sudo npm » rejette l'autorité du cache là où PAM ne relit pas
|
||||||
|
/etc/environment pour sudo."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
|
||||||
|
def commande(self, offline=False, distro="debian"):
|
||||||
|
args = faux_args(Path(self.tmp.name), distro)
|
||||||
|
args.offline = offline
|
||||||
|
lignes = [c for c in cache_commands(args) if "env_keep" in c]
|
||||||
|
self.assertEqual(len(lignes), 1, "une seule écriture du sudoers")
|
||||||
|
return lignes[0]
|
||||||
|
|
||||||
|
def test_chaque_variable_du_cache_est_gardee(self):
|
||||||
|
commande = self.commande()
|
||||||
|
for var in CACHE_ENV_VARS:
|
||||||
|
self.assertIn(var, commande)
|
||||||
|
self.assertNotIn("NPM_CONFIG_AUDIT", commande)
|
||||||
|
|
||||||
|
def test_hors_ligne_l_audit_coupe_traverse_aussi(self):
|
||||||
|
for var, _ in OFFLINE_ENV_VARS:
|
||||||
|
self.assertIn(var, self.commande(offline=True))
|
||||||
|
|
||||||
|
def test_vient_apres_les_variables(self):
|
||||||
|
"""La confiance reste la première commande ; le sudoers suit."""
|
||||||
|
args = faux_args(Path(self.tmp.name), "debian")
|
||||||
|
commandes = cache_commands(args)
|
||||||
|
self.assertIn("env_keep", commandes[-1])
|
||||||
|
|
||||||
|
def test_le_fichier_ecrit_est_celui_que_sudo_lit(self):
|
||||||
|
"""sudo ignore un nom de sudoers.d qui porte un point : c'est ce qui
|
||||||
|
rend le temporaire inoffensif, et interdit ce point au nom final."""
|
||||||
|
nom_final = CACHE_SUDOERS.rsplit("/", 1)[1]
|
||||||
|
self.assertNotIn(".", nom_final)
|
||||||
|
self.assertTrue(CACHE_SUDOERS.startswith("/etc/sudoers.d/"))
|
||||||
|
|
||||||
|
def test_le_fichier_est_verifie_avant_d_etre_pose(self):
|
||||||
|
commande = commande_sudoers(["A"], "/etc/sudoers.d/essai")
|
||||||
|
self.assertLess(
|
||||||
|
commande.index("visudo -cf"),
|
||||||
|
commande.index("mv /etc/sudoers.d/.essai /etc/sudoers.d/essai"),
|
||||||
|
)
|
||||||
|
self.assertIn("chmod 0440", commande)
|
||||||
|
self.assertTrue(
|
||||||
|
commande.rstrip("'").endswith("|| rm -f /etc/sudoers.d/.essai")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_le_contenu_ecrit_est_une_ligne_par_variable(self):
|
||||||
|
"""Joué dans un vrai shell, visudo remplacé : c'est le fichier produit
|
||||||
|
qui compte, et son absence quand la vérification échoue."""
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as rep:
|
||||||
|
bin_ = Path(rep) / "bin"
|
||||||
|
bin_.mkdir()
|
||||||
|
dossier = Path(rep) / "sudoers.d"
|
||||||
|
dossier.mkdir()
|
||||||
|
for verdict, attendu in (("0", True), ("1", False)):
|
||||||
|
faux = bin_ / "visudo"
|
||||||
|
faux.write_text(f"#!/bin/sh\nexit {verdict}\n")
|
||||||
|
faux.chmod(0o755)
|
||||||
|
fichier = dossier / "erplibre-cache"
|
||||||
|
commande = commande_sudoers(
|
||||||
|
["PIP_CERT", "NODE_EXTRA_CA_CERTS"], str(fichier)
|
||||||
|
)
|
||||||
|
res = subprocess.run(
|
||||||
|
["sh", "-c", commande],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env={"PATH": f"{bin_}:/usr/bin:/bin"},
|
||||||
|
)
|
||||||
|
with self.subTest(visudo=verdict):
|
||||||
|
self.assertEqual(res.returncode, 0, res.stderr)
|
||||||
|
self.assertEqual(fichier.exists(), attendu)
|
||||||
|
self.assertFalse((dossier / ".erplibre-cache").exists())
|
||||||
|
if attendu:
|
||||||
|
self.assertEqual(
|
||||||
|
fichier.read_text(),
|
||||||
|
"Defaults env_keep += PIP_CERT\n"
|
||||||
|
"Defaults env_keep += NODE_EXTRA_CA_CERTS\n",
|
||||||
|
)
|
||||||
|
fichier.unlink()
|
||||||
|
|
||||||
|
|
||||||
class TestLaLangueDesMessagesDuCache(unittest.TestCase):
|
class TestLaLangueDesMessagesDuCache(unittest.TestCase):
|
||||||
def test_l_option_du_deploiement_l_emporte(self):
|
def test_l_option_du_deploiement_l_emporte(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue