diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 6b43764..f94e512 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -200,6 +200,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Deployment › QEMU cache › Git mirrors** fills the base of the active Odoo version, or its extra modules, on their own — beside the full fill of every manifest, which takes hours. Each list shows how many repositories it declares and how many still lack a mirror. What a deployment clones is now read with the manifest merge's own rule and lists, so the extra modules, installed only on request, and the mobile project no longer count: the offline warning announced four missing mirrors that a default Odoo 18 install never clones - pip's PEP 658 metadata — the `.whl.metadata` file fetched before each wheel — is served from disk. The name ends in `.metadata`, which no rule knew, so each file was taken again on every install: 178 of them on a full ERPLibre install. Copies stored before this change are not reached again; the next online install refills them - The timezone a deployed VM inherits from its host is translated to its canonical name. Ubuntu 24.04 cloud images no longer carry the legacy aliases — `Canada/*`, `US/*`, `Asia/Calcutta` — moved to a `tzdata-legacy` package they do not install: cloud-init refused the zone, the VM stayed on UTC, and the only sign was cloud-init reporting an error, the offset showing up in timestamps long afterwards. The alias table is the host's own `tzdata.zi`, not a copy kept in the code +- The locale and the keyboard a deployed VM is given now apply on Debian, where both silently failed. A locale is generated from `/etc/locale.gen` and nowhere else, so `update-locale` refused one that was not there and the VM stayed on C.UTF-8; the keyboard module ends on a `console-setup` the genericcloud image does not carry, so `/etc/default/keyboard` — the file localed and X read — is written directly instead. Every Debian deployment used to print `cloud-init: status: error`, and a word that always shows warns of nothing @@ -242,6 +243,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Déploiement › Cache QEMU › Miroirs git** remplit à part la base de la version d'Odoo active, ou ses modules extra — à côté du remplissage de tous les manifestes, qui prend des heures. Chaque liste dit combien de dépôts elle déclare et combien n'ont pas encore de miroir. Ce qu'un déploiement clone se lit désormais avec la règle et les listes de la fusion des manifestes elle-même : les modules extra, installés seulement sur demande, et le projet mobile ne comptent plus, là où l'avertissement hors ligne annonçait quatre miroirs manquants qu'une installation Odoo 18 par défaut ne clone jamais - Les métadonnées PEP 658 de pip — le fichier `.whl.metadata` récupéré avant chaque roue — sont servies du disque. Le nom finit par `.metadata`, qu'aucune règle ne connaissait : chaque fichier repartait à chaque installation, 178 sur une installation complète d'ERPLibre. Les copies gardées avant ce changement ne sont plus atteintes ; la prochaine installation en ligne les remplit - Le fuseau horaire qu'une VM déployée hérite de son hôte est traduit en son nom canonique. Les images cloud d'Ubuntu 24.04 ne portent plus les alias historiques — `Canada/*`, `US/*`, `Asia/Calcutta` — déplacés dans un paquet `tzdata-legacy` qu'elles n'installent pas : cloud-init refusait le fuseau, la VM restait en UTC, et le seul signe était un cloud-init en erreur, le décalage n'apparaissant qu'aux horodatages longtemps après. La table des alias est le `tzdata.zi` de l'hôte, et non une copie figée dans le code +- Le locale et le clavier qu'une VM déployée reçoit s'appliquent désormais sur Debian, où les deux échouaient en silence. Un locale se génère à partir de `/etc/locale.gen` et de nulle part ailleurs : `update-locale` refusait celui qui n'y était pas et la VM restait en C.UTF-8 ; le module clavier finit par un `console-setup` que l'image genericcloud ne porte pas, alors `/etc/default/keyboard` — le fichier que localed et X relisent — est écrit directement. Chaque déploiement Debian imprimait `cloud-init: status: error`, et un mot qui s'affiche toujours n'avertit plus de rien ## Removed diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 4a33f44..ae6addd 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -111,6 +111,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Déploiement › Cache QEMU › Miroirs git** remplit à part la base de la version d'Odoo active, ou ses modules extra — à côté du remplissage de tous les manifestes, qui prend des heures. Chaque liste dit combien de dépôts elle déclare et combien n'ont pas encore de miroir. Ce qu'un déploiement clone se lit désormais avec la règle et les listes de la fusion des manifestes elle-même : les modules extra, installés seulement sur demande, et le projet mobile ne comptent plus, là où l'avertissement hors ligne annonçait quatre miroirs manquants qu'une installation Odoo 18 par défaut ne clone jamais - Les métadonnées PEP 658 de pip — le fichier `.whl.metadata` récupéré avant chaque roue — sont servies du disque. Le nom finit par `.metadata`, qu'aucune règle ne connaissait : chaque fichier repartait à chaque installation, 178 sur une installation complète d'ERPLibre. Les copies gardées avant ce changement ne sont plus atteintes ; la prochaine installation en ligne les remplit - Le fuseau horaire qu'une VM déployée hérite de son hôte est traduit en son nom canonique. Les images cloud d'Ubuntu 24.04 ne portent plus les alias historiques — `Canada/*`, `US/*`, `Asia/Calcutta` — déplacés dans un paquet `tzdata-legacy` qu'elles n'installent pas : cloud-init refusait le fuseau, la VM restait en UTC, et le seul signe était un cloud-init en erreur, le décalage n'apparaissant qu'aux horodatages longtemps après. La table des alias est le `tzdata.zi` de l'hôte, et non une copie figée dans le code +- Le locale et le clavier qu'une VM déployée reçoit s'appliquent désormais sur Debian, où les deux échouaient en silence. Un locale se génère à partir de `/etc/locale.gen` et de nulle part ailleurs : `update-locale` refusait celui qui n'y était pas et la VM restait en C.UTF-8 ; le module clavier finit par un `console-setup` que l'image genericcloud ne porte pas, alors `/etc/default/keyboard` — le fichier que localed et X relisent — est écrit directement. Chaque déploiement Debian imprimait `cloud-init: status: error`, et un mot qui s'affiche toujours n'avertit plus de rien ## Retiré diff --git a/CHANGELOG.md b/CHANGELOG.md index a9cf39d..942ea94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Deployment › QEMU cache › Git mirrors** fills the base of the active Odoo version, or its extra modules, on their own — beside the full fill of every manifest, which takes hours. Each list shows how many repositories it declares and how many still lack a mirror. What a deployment clones is now read with the manifest merge's own rule and lists, so the extra modules, installed only on request, and the mobile project no longer count: the offline warning announced four missing mirrors that a default Odoo 18 install never clones - pip's PEP 658 metadata — the `.whl.metadata` file fetched before each wheel — is served from disk. The name ends in `.metadata`, which no rule knew, so each file was taken again on every install: 178 of them on a full ERPLibre install. Copies stored before this change are not reached again; the next online install refills them - The timezone a deployed VM inherits from its host is translated to its canonical name. Ubuntu 24.04 cloud images no longer carry the legacy aliases — `Canada/*`, `US/*`, `Asia/Calcutta` — moved to a `tzdata-legacy` package they do not install: cloud-init refused the zone, the VM stayed on UTC, and the only sign was cloud-init reporting an error, the offset showing up in timestamps long afterwards. The alias table is the host's own `tzdata.zi`, not a copy kept in the code +- The locale and the keyboard a deployed VM is given now apply on Debian, where both silently failed. A locale is generated from `/etc/locale.gen` and nowhere else, so `update-locale` refused one that was not there and the VM stayed on C.UTF-8; the keyboard module ends on a `console-setup` the genericcloud image does not carry, so `/etc/default/keyboard` — the file localed and X read — is written directly instead. Every Debian deployment used to print `cloud-init: status: error`, and a word that always shows warns of nothing ## Removed diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 1558631..6801ceb 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -2563,6 +2563,89 @@ def build_gitconfig(name: str, email: str, editor: str) -> str: return "\n".join(lines) + "\n" if lines else "" +# Les images cloud dont le module « keyboard » de cloud-init ne peut pas +# aboutir : il finit par « systemctl restart console-setup », service qui +# n'existe que si le paquet console-setup est installé, et l'image +# genericcloud de Debian ne le porte pas. Le réglage EST écrit avant cet +# échec — dans /etc/default/keyboard, que localed et X relisent — mais +# cloud-init se déclare en erreur, et ce mot masque les vraies pannes dans le +# compte-rendu de déploiement. +DISTROS_CLAVIER_PAR_FICHIER = ("debian",) + +# Un locale se génère à partir de /etc/locale.gen et de nulle part ailleurs : +# « locale-gen xx_YY.UTF-8 » ne prend pas son argument pour une demande, il +# ne relit que ce fichier. Une famille qui n'a pas ce fichier — Fedora, la +# famille RHEL, openSUSE — embarque ses locales déjà générées. +LOCALE_A_GENERER = re.compile(r"^[a-z]{2,3}_[A-Z]{2}(\.|@|$)") + + +def locale_bootcmd_lines(locale: str) -> list[str]: + """« bootcmd » qui GÉNÈRE le locale avant que cloud-init ne l'applique. + + Sur Debian, « update-locale LANG=xx_YY.UTF-8 » refuse un locale absent de + ceux qui sont générés — « invalid locale settings » — et le module + « locale » se solde par une erreur : la VM reste en C.UTF-8, sans autre + signe que le compte-rendu de cloud-init, et le décalage ne se voit qu'aux + messages, longtemps après. + + « bootcmd » et non « runcmd » : il tourne à l'étape init, AVANT les + modules de configuration dont « locale » fait partie. runcmd viendrait + après, et n'aurait plus rien à rattraper. + + Il tourne aussi à CHAQUE démarrage, d'où le premier grep : sans lui, + /etc/locale.gen s'allonge d'une ligne identique par boot. + + La ligne est DÉCOMMENTÉE plutôt qu'ajoutée : le fichier porte déjà le + catalogue entier en commentaires, et l'ajouter en produirait un doublon — + cloud-init décommente ensuite la même, à l'étape d'après. Le doublon est + inoffensif mais il ment sur qui a écrit quoi. L'ajout reste le recours + pour un locale que le catalogue ne porte pas. + """ + if not locale or not LOCALE_A_GENERER.match(locale): + return [] + ligne = f"{locale} UTF-8" + corps = ( + "[ -f /etc/locale.gen ] || exit 0; " + f"grep -qxF '{ligne}' /etc/locale.gen && exit 0; " + f"sed -i 's/^#[[:space:]]*{ligne}$/{ligne}/' /etc/locale.gen; " + f"grep -qxF '{ligne}' /etc/locale.gen " + f"|| echo '{ligne}' >> /etc/locale.gen; locale-gen" + ) + return ["bootcmd:", f' - [ sh, -c, "{corps}" ]'] + + +def keyboard_lines(distro: str, layout: str, variant: str) -> list[str]: + """Bloc « keyboard » de cloud-init, ou rien là où il échouerait.""" + if distro in DISTROS_CLAVIER_PAR_FICHIER: + return [] + return ["keyboard:", f" layout: {layout}", f" variant: {variant}"] + + +def keyboard_files( + distro: str, layout: str, variant: str +) -> list[tuple[str, str, str, str]]: + """/etc/default/keyboard écrit directement, là où cloud-init échoue. + + C'est le fichier que le module de cloud-init aurait écrit, et celui que + localed et X relisent : le clavier d'une VM graphique est le même, + l'erreur de déploiement en moins. + + La disposition de la CONSOLE texte n'est perdue pour personne : sur cette + image, ni console-setup ni systemd-vconsole-setup n'existent, et aucun + des deux chemins ne l'appliquait. Ce que cloud-init y écrivait était une + valeur que rien ne lisait. + """ + if distro not in DISTROS_CLAVIER_PAR_FICHIER: + return [] + contenu = ( + "XKBMODEL=pc105\n" + f"XKBLAYOUT={layout}\n" + f"XKBVARIANT={variant}\n" + "XKBOPTIONS=\n" + ) + return [("/etc/default/keyboard", "0644", contenu, "")] + + def write_files_lines( entries: list[tuple[str, str, str, str]], ) -> list[str]: @@ -3109,16 +3192,25 @@ def build_cloud_config( getattr(args, "apt_mirror", None), fixe=bool(getattr(args, "cache_ca", None)), ) - lines += [ - "keyboard:", - f" layout: {args.keyboard_layout}", - f" variant: {args.keyboard_variant}", - ] + distro = getattr(args, "distro", "ubuntu") + lines += locale_bootcmd_lines(args.locale) + lines += keyboard_lines( + distro, args.keyboard_layout, args.keyboard_variant + ) # Guide de connexion et identité git : posés par cloud-init, donc présents # dès le PREMIER boot. C'est le point : ils sont là avant l'installation # d'ERPLibre, et encore là si elle échoue — le moment où l'on se connecte # justement à la main. - lines += write_files_lines(guide_files(args) + cache_files(args)) + # Trois sources pour un seul bloc « write_files » : le guide de + # connexion, le clavier là où cloud-init ne sait pas le poser, et les + # fichiers du cache. write_files_lines reçoit UNE liste et n'émet qu'une + # clé : les concaténer ne peut pas produire le doublon qui ferait perdre + # le premier bloc en silence. + lines += write_files_lines( + guide_files(args) + + keyboard_files(distro, args.keyboard_layout, args.keyboard_variant) + + cache_files(args) + ) # apt update/upgrade désactivés par défaut : sur un réseau lent/instable # ils font pendre cloud-init au 1er boot (et retardent la dispo SSH). SSH # est déjà présent dans les images cloud ; on l'active via runcmd sans apt. diff --git a/script/todo/qemu_install.py b/script/todo/qemu_install.py index 9212ce6..2ec60b3 100644 --- a/script/todo/qemu_install.py +++ b/script/todo/qemu_install.py @@ -2526,10 +2526,6 @@ class QemuInstallMixin: doit réussir quand même. """ nix = "/nix/var/nix/profiles/default/bin/nix" - # L'installateur écrit l'un OU l'autre selon sa version ; les deux - # portent le PATH et les variables du démon. - profils = "/etc/profile.d/nix.sh /etc/profile.d/nix-daemon.sh" - source_line = f'for f in {profils}; do [ -e "$f" ] && . "$f"; done' outil = "$HOME/.nix-profile/bin/nixos-anywhere" def pose(cmd, secondes): @@ -2558,10 +2554,13 @@ class QemuInstallMixin: + pose(self._QEMU_NIX_UPSTREAM, 900) + conf + installe - + ( - f"grep -qF {shlex.quote(source_line)} ~/.bashrc 2>/dev/null" - f" || echo {shlex.quote(source_line)} >> ~/.bashrc; " - ) + # Rien n'est ajouté à ~/.bashrc, et c'est délibéré : l'installateur + # écrit lui-même /etc/bash.bashrc, /etc/profile.d/nix.sh et les + # fichiers zsh et fish. Une ligne de plus dans le ~/.bashrc de + # l'utilisateur serait posée APRÈS le « return » que ce fichier + # exécute pour tout shell non interactif — donc jamais atteinte par + # « ssh hôte 'commande' », le seul cas qu'elle prétendait couvrir. + # # Le verdict porte sur le BINAIRE, pas sur le code de retour des # poses : toutes rendent 0 par construction. + f'[ -x "{outil}" ]' diff --git a/test/test_qemu_nixanywhere.py b/test/test_qemu_nixanywhere.py index 2b7a6e8..3e5e1fc 100644 --- a/test/test_qemu_nixanywhere.py +++ b/test/test_qemu_nixanywhere.py @@ -119,14 +119,16 @@ class LaCommandeDistante(unittest.TestCase): /etc/nix/nix.conf d'une ligne identique.""" self.assertIn("grep -qF 'experimental-features", self.cmd) - def test_the_rc_line_is_written_once(self): - """Même invariant sur ~/.bashrc.""" - self.assertIn("grep -qF 'for f in /etc/profile.d/nix.sh", self.cmd) + def test_nothing_is_added_to_the_user_rc_file(self): + """L'installateur écrit lui-même /etc/bash.bashrc et + /etc/profile.d/nix.sh : c'est de là que « ssh hôte \'commande\' » + tient son PATH. - def test_both_profile_scripts_are_sourced(self): - """L'installateur écrit l'un OU l'autre selon sa version.""" - self.assertIn("/etc/profile.d/nix.sh", self.cmd) - self.assertIn("/etc/profile.d/nix-daemon.sh", self.cmd) + Une ligne de plus dans le ~/.bashrc de l'utilisateur serait posée + APRÈS le « return » que ce fichier exécute pour tout shell non + interactif — donc jamais atteinte dans le seul cas qu'elle + prétendait couvrir, et sans objet dans les autres.""" + self.assertNotIn(".bashrc", self.cmd) def test_the_verdict_looks_at_the_binary(self): """Toutes les poses rendent 0 par construction : leur code de retour @@ -138,8 +140,8 @@ class LaCommandeDistante(unittest.TestCase): « set -e », une seule commande non gardée y suffirait. Le bloc est joué pour de vrai, avec les commandes qui SORTENT de la - machine remplacées par un échec — un test unitaire ne télécharge rien - et n'écrit rien hors de son répertoire temporaire.""" + machine remplacées par un échec — un test unitaire ne télécharge rien. + Le HOME temporaire reste VIDE : le bloc n'écrit chez personne.""" neutre = ( "curl(){ return 1; }; sudo(){ return 1; }; " "timeout(){ return 1; }; systemctl(){ return 1; }; " @@ -152,8 +154,7 @@ class LaCommandeDistante(unittest.TestCase): env={"PATH": os.environ["PATH"], "HOME": maison}, ) self.assertEqual(0, fini.returncode, fini.stderr) - rc = Path(maison, ".bashrc") - self.assertIn("/etc/profile.d/nix.sh", rc.read_text()) + self.assertEqual([], list(Path(maison).iterdir())) def test_it_is_valid_shell(self): """Une commande mal citée casse tout le bloc des outils, pas diff --git a/test/test_qemu_seed_locale_keyboard.py b/test/test_qemu_seed_locale_keyboard.py new file mode 100644 index 0000000..e5f901d --- /dev/null +++ b/test/test_qemu_seed_locale_keyboard.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +"""Le seed ne demande à l'invité que ce que son image sait honorer. + +Deux réglages régionaux échouaient sur toute VM Debian, et le seul signe en +était le mot « error » dans le compte-rendu de cloud-init que le déploiement +imprime — un mot qui, s'il s'affiche à chaque fois, n'avertit plus de rien. + +- le LOCALE : « update-locale LANG=xx_YY.UTF-8 » refuse un locale qui n'est + pas généré, et un locale ne se génère qu'à partir de /etc/locale.gen. La VM + restait en C.UTF-8 ; +- le CLAVIER : le module de cloud-init finit par « systemctl restart + console-setup », service absent de l'image genericcloud. Le réglage est + écrit AVANT cet échec, dans /etc/default/keyboard — c'est le fichier que + localed et X relisent —, donc le poser nous-mêmes ne perd rien d'autre que + la disposition de la console texte, qui demande ce paquet. + +Ces tests gardent aussi la contrainte qui domine toutes les autres ici : le +user-data reste un YAML valide. Une erreur de syntaxe fait rejeter la +configuration ENTIÈRE, sans message : la VM démarre sans compte ni clé. +""" + +import importlib.util +import sys +import unittest +from pathlib import Path + +sys.argv = ["todo.py"] + +RACINE = Path(__file__).resolve().parents[1] + +try: + import yaml +except ImportError: # pragma: no cover + yaml = None + + +def _deploy_qemu(): + chemin = RACINE / "script/qemu/deploy_qemu.py" + spec = importlib.util.spec_from_file_location("deploy_qemu", chemin) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +DQ = _deploy_qemu() + + +def _cloud_config(distro): + args = DQ.build_parser().parse_args( + ["--distro", distro, "--hostname", "x"] + ) + return DQ.build_cloud_config(args, None, ["ssh-ed25519 AAAA essai"]) + + +class LeLocale(unittest.TestCase): + def test_the_locale_is_generated_before_cloud_init_applies_it(self): + """« bootcmd » et non « runcmd » : le premier tourne à l'étape init, + avant les modules de configuration dont « locale » fait partie. Le + second viendrait après, et n'aurait plus rien à rattraper.""" + lignes = DQ.locale_bootcmd_lines("fr_CA.UTF-8") + self.assertEqual("bootcmd:", lignes[0]) + self.assertIn("locale-gen", lignes[1]) + + def test_the_line_is_added_to_locale_gen_only_once(self): + """bootcmd tourne à CHAQUE démarrage : sans le grep, /etc/locale.gen + s'allonge d'une ligne identique par boot.""" + self.assertIn( + "grep -qxF 'fr_CA.UTF-8 UTF-8'", + " ".join(DQ.locale_bootcmd_lines("fr_CA.UTF-8")), + ) + + def test_a_host_without_locale_gen_is_left_alone(self): + """Fedora, la famille RHEL et openSUSE embarquent leurs locales déjà + générées, et n'ont pas ce fichier.""" + self.assertIn( + "[ -f /etc/locale.gen ] || exit 0", + " ".join(DQ.locale_bootcmd_lines("fr_CA.UTF-8")), + ) + + def test_a_builtin_locale_asks_for_nothing(self): + """C.UTF-8 et POSIX sont dans la bibliothèque C, pas dans + locale.gen : les demander ferait échouer locale-gen, donc bootcmd, + donc cloud-init — l'erreur qu'on retire.""" + for locale in ("C.UTF-8", "POSIX", ""): + with self.subTest(locale=locale): + self.assertEqual([], DQ.locale_bootcmd_lines(locale)) + + +class LeClavier(unittest.TestCase): + def test_debian_gets_the_file_and_not_the_cloud_init_key(self): + """Le module de cloud-init n'y aboutit pas : console-setup manque.""" + self.assertEqual([], DQ.keyboard_lines("debian", "ca", "multix")) + fichiers = DQ.keyboard_files("debian", "ca", "multix") + self.assertEqual("/etc/default/keyboard", fichiers[0][0]) + self.assertIn("XKBLAYOUT=ca", fichiers[0][2]) + self.assertIn("XKBVARIANT=multix", fichiers[0][2]) + + def test_elsewhere_cloud_init_keeps_the_job(self): + """Une VM Ubuntu applique le clavier sans erreur : lui retirer le + bloc perdrait la disposition de sa console, elle qui l'obtient.""" + for distro in ("ubuntu", "fedora", "arch", "opensuse", "nixos"): + with self.subTest(distro=distro): + self.assertEqual( + ["keyboard:", " layout: ca", " variant: multix"], + DQ.keyboard_lines(distro, "ca", "multix"), + ) + self.assertEqual([], DQ.keyboard_files(distro, "ca", "multix")) + + def test_the_file_carries_a_model(self): + """localed lit les quatre champs ; un XKBMODEL vide laisse X deviner.""" + self.assertIn("XKBMODEL=", DQ.keyboard_files("debian", "ca", "")[0][2]) + + +class LeSeedEntier(unittest.TestCase): + """Ce que la configuration produite doit rester, distro par distro.""" + + def setUp(self): + if yaml is None: + self.skipTest("PyYAML absent") + + def test_the_user_data_stays_valid_yaml(self): + """Une erreur de syntaxe fait rejeter le user-data en ENTIER, sans + message : la VM démarre nue, sans compte ni clé SSH.""" + for distro in ("debian", "ubuntu", "fedora", "arch", "nixos"): + with self.subTest(distro=distro): + doc = yaml.safe_load(_cloud_config(distro)) + self.assertIsInstance(doc, dict) + self.assertIn("users", doc) + + def test_bootcmd_is_a_list_of_arguments_not_a_shell_string(self): + """La forme [sh, -c, "…"] met le corps hors de portée de YAML : + accolades, apostrophes et redirections y passent sans échappement.""" + doc = yaml.safe_load(_cloud_config("debian")) + self.assertIsInstance(doc["bootcmd"][0], list) + self.assertEqual(["sh", "-c"], doc["bootcmd"][0][:2]) + + def test_debian_asks_cloud_init_for_no_keyboard(self): + doc = yaml.safe_load(_cloud_config("debian")) + self.assertIsNone(doc.get("keyboard")) + chemins = [f["path"] for f in doc["write_files"]] + self.assertIn("/etc/default/keyboard", chemins) + + def test_ubuntu_keeps_asking(self): + doc = yaml.safe_load(_cloud_config("ubuntu")) + self.assertEqual("ca", doc["keyboard"]["layout"]) + chemins = [f["path"] for f in doc["write_files"]] + self.assertNotIn("/etc/default/keyboard", chemins) + + +if __name__ == "__main__": + unittest.main()