Compare commits
224 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad63236375 | |||
| a743c4462a | |||
| 55f6127a72 | |||
| bc41b35769 | |||
| 9ee54f00cd | |||
| 5d6a262489 | |||
| e8f7e8384a | |||
| d6ec01d289 | |||
| dcb1af4880 | |||
| 3e53cbd47d | |||
| 2997dfeb26 | |||
| 188dad9960 | |||
| 00eb0a3c7a | |||
| 03ed3a6a87 | |||
| f2da91b0fe | |||
| 32d25b5344 | |||
| 4205c0c0e3 | |||
| cd8c32b5b2 | |||
| e687b874d6 | |||
| b197cc01c5 | |||
| b8bfb0f8e0 | |||
| 7d96b56fb4 | |||
| d40e6aa011 | |||
| 25cc780005 | |||
| 7c1016552b | |||
| ce5fb38634 | |||
| b8582f5bc6 | |||
| b664128a05 | |||
| c1afa2f4d2 | |||
| 6d3a4c38cb | |||
| 2716127bff | |||
| 5f3afa6da2 | |||
| 11d9b88de9 | |||
| 1f0b5c021f | |||
| 74ee584ffa | |||
| fba3b8aed1 | |||
| a9ef5a0e8e | |||
| 96cc39431c | |||
| 79a07ac577 | |||
| 8ce4b93292 | |||
| 1e4cc4f471 | |||
| 1ec37defc3 | |||
| 045ccc1c85 | |||
| 4536c5bc40 | |||
| ab9f94cc28 | |||
| b8f2bd7343 | |||
| a6ecc8aa3c | |||
| 71aba63b03 | |||
| 5951b0374f | |||
| d84d729c77 | |||
| 52492302d3 | |||
| db1e9fa24f | |||
| 839ef08ea4 | |||
| a2b507c471 | |||
| b856548d64 | |||
| 671730b841 | |||
| 06687f5f7e | |||
| 12d1c0c8ae | |||
| 6506e5cf9f | |||
| 19c5f41751 | |||
| 233fdf2d2b | |||
| e8c60a2d46 | |||
| bcb77df2d3 | |||
| 808e37cd59 | |||
| 43d268e448 | |||
| 64c0bbc698 | |||
| 7904c72d71 | |||
| 1ebf2b353f | |||
| b99db0ea66 | |||
| fd3226a053 | |||
| bea9a82d75 | |||
| ae3745a22a | |||
| 8be034f25e | |||
| c9ec5b98b9 | |||
| 545193d293 | |||
| 9fb24e583a | |||
| 0b02e9fe43 | |||
| 4f0c3cd217 | |||
| c60b3f5cbd | |||
| e3c548e57b | |||
| 9b00de6d8b | |||
| 7e89fa354f | |||
| 0f50c524db | |||
| c9b51998a4 | |||
| 6ce56629f4 | |||
| c140b418d5 | |||
| 14fa807221 | |||
| babe69dbf7 | |||
| 59bf857558 | |||
| f7df2310b3 | |||
| e2b940a3af | |||
| 8278c38f46 | |||
| ab8bfec113 | |||
| 3f20497912 | |||
| a0c560b15f | |||
| ec88b5f1f0 | |||
| 4bebf5a397 | |||
| 22dcf96853 | |||
| 03f6205b9e | |||
| 4d2b7cea00 | |||
| edf066bb82 | |||
| e36795fb34 | |||
| c31dc3967d | |||
| d9b8372784 | |||
| b2a2db686e | |||
| 4c04145c31 | |||
| 99180d2abf | |||
| 421f61b84b | |||
| 320b88b83b | |||
| 6440cec2e1 | |||
| bc0e06137f | |||
| b542aa7dc2 | |||
| 7517cc814b | |||
| f2cb9be665 | |||
| 62ea6ebb7f | |||
| 1fd98e6ab6 | |||
| 6f70da98d5 | |||
| fdbec6d19d | |||
| b7522bb92d | |||
| 51a9e7b97c | |||
| 1e9e5abd1f | |||
| da1206bcb2 | |||
| a74b0c0c4c | |||
| 372aedb0eb | |||
| 52b79551dd | |||
| 4688e7c606 | |||
| 5a408f1061 | |||
| b49e107b83 | |||
| 7bd44d21a9 | |||
| 939e26ba83 | |||
| b395e7c3ed | |||
| 50617e3909 | |||
| bd94c277d3 | |||
| 0930f30cd3 | |||
| 88dc1d5409 | |||
| 8652c30507 | |||
| 3bf8fc2972 | |||
| 374373cc7c | |||
| 63e7b6af8c | |||
| 482a914ab8 | |||
| 37a999b911 | |||
| deddaeb6a9 | |||
| 137f4245ec | |||
| 7a001a9b84 | |||
| 69d87f93c3 | |||
| ea3f9c00cf | |||
| 18d3f4461c | |||
| 44d64fe87a | |||
| 3cb6f45269 | |||
| 7def76eb62 | |||
| 84e383a3b1 | |||
| 4a821226fb | |||
| 0830c895eb | |||
| d4d3d801c3 | |||
| 62bf80d9b1 | |||
| 8d8d0750b0 | |||
| 83a28ed9e8 | |||
| 83f71561f8 | |||
| 0aa84ed1e3 | |||
| 65a0d6f907 | |||
| 6ac7624950 | |||
| c0ceb6bea7 | |||
| d83d244805 | |||
| 2b7e20493a | |||
| 79c69fb6d5 | |||
| dc9dfa5779 | |||
| 21810a31a1 | |||
| c4f7d7dcbd | |||
| 990c81c0e7 | |||
| 19e3801d2c | |||
| 4b988bbe46 | |||
| 78e7cf55c6 | |||
| 9876a03ac7 | |||
| 2020e15b5e | |||
| ef78b9d315 | |||
| f5923d0fbb | |||
| ca796a67a4 | |||
| 84f63c326a | |||
| 3e2224b782 | |||
| 3b3efe5e43 | |||
| 9c8e7454f3 | |||
| 0d985ab449 | |||
| a202143c30 | |||
| 935919e07b | |||
| a42633c8ce | |||
| dc42bb715a | |||
| 6f087d2a9b | |||
| 2d549025ec | |||
| b91878f06e | |||
| 160103a595 | |||
| 18c40790c8 | |||
| c47302d5c4 | |||
| af523fc1c2 | |||
| 970a373400 | |||
| 612c1e455a | |||
| 89dc8e001f | |||
| 08caec3659 | |||
| 7e0e6d0dda | |||
| 01c23b0640 | |||
| f0a7e85e81 | |||
| 624c2d89e9 | |||
| e9f14c2af9 | |||
| eb64155f3c | |||
| 7c99ce9f29 | |||
| dd08281c6f | |||
| 0c3fedeb79 | |||
| 15a8229a01 | |||
| 3136bb694f | |||
| 4766fcb3b0 | |||
| e03befb28e | |||
| cd14e128ff | |||
| 212a9238c9 | |||
| 465e8d9c05 | |||
| d6275809de | |||
| 027224a87e | |||
| 32315a2dba | |||
| 795d4a0904 | |||
| 98c35e3aaf | |||
| b689c4ddf1 | |||
| c4bb115bd6 | |||
| ca76f11ba3 | |||
| 0066ef18b8 | |||
| c88c580fb4 | |||
| 498a5f5b94 |
579 changed files with 8155 additions and 230337 deletions
|
|
@ -8,158 +8,8 @@ configuration du dépôt — les lire plutôt que de supposer : `.flake8`,
|
||||||
Prettier (via npm) formate XML/JSON/YAML ; `.editorconfig` donne les
|
Prettier (via npm) formate XML/JSON/YAML ; `.editorconfig` donne les
|
||||||
indentations par type de fichier.
|
indentations par type de fichier.
|
||||||
|
|
||||||
## Commentaires
|
|
||||||
|
|
||||||
Un commentaire dit COMMENT le code fonctionne : ce que la fonction prend, ce
|
|
||||||
qu'elle rend, l'invariant qu'elle tient, ses effets de bord, la contrainte
|
|
||||||
technique qu'on ne devine pas en lisant la ligne d'à côté. Il doit se lire
|
|
||||||
dans dix ans sans rien savoir de la semaine où il a été écrit. La règle vaut
|
|
||||||
pour les docstrings autant que pour les lignes `#`.
|
|
||||||
|
|
||||||
**L'épreuve : le sujet et le temps.** Chaque phrase a le CODE pour sujet, au
|
|
||||||
présent de ce qu'il fait. Une phrase dont le sujet est un incident, une
|
|
||||||
machine, une date ou une personne est à couper, où qu'elle se trouve dans le
|
|
||||||
paragraphe. Le MODE DE DÉFAILLANCE que le code empêche est du fonctionnement
|
|
||||||
et reste — « une VM renommée se voit attribuer la passerelle ». L'INCIDENT où
|
|
||||||
on l'a observé est du récit et part — « vécu sur telle VM, annoncée à telle
|
|
||||||
adresse ».
|
|
||||||
|
|
||||||
**Les chiffres.** Une mesure qui établit un fait durable reste, dépouillée de
|
|
||||||
sa date, de son lieu et de son opérateur : une limite, un seuil, une valeur
|
|
||||||
que documente l'éditeur. Un relevé de ce qui répondait ce jour-là part.
|
|
||||||
|
|
||||||
**Rien d'identifiant, jamais** : nom d'un client ou d'une organisation tierce,
|
|
||||||
nom de base de données réelle, nom de VM ou d'hôte, adresse IP, courriel,
|
|
||||||
chemin portant un nom d'utilisateur, libellé ou chiffre tiré des données d'un
|
|
||||||
client. La seule exception est l'en-tête de copyright : le dépôt nomme son
|
|
||||||
propriétaire, pas ses clients. Généraliser plutôt que censurer — « sur une
|
|
||||||
base de production », « sur un hôte qui exige une authentification sudo
|
|
||||||
interactive » — dit la CLASSE de situation, qui est ce qui sert au lecteur.
|
|
||||||
|
|
||||||
**L'exemple qui illustre un interdit s'invente.** La règle a d'abord été
|
|
||||||
violée par ses propres tests : pour démontrer qu'une adresse et un chemin de
|
|
||||||
compte sont refusés, ils en portaient de vrais, pris dans le parc. Choisir un
|
|
||||||
cas réel « parce qu'il est parlant » est exactement le réflexe que la règle
|
|
||||||
combat, et un test le fige pour toujours. Une valeur inventée démontre aussi
|
|
||||||
bien ; vérifier qu'elle n'existe nulle part ailleurs dans le dépôt.
|
|
||||||
|
|
||||||
Le récit n'est pas perdu, il change de place : l'enquête, les mesures datées
|
|
||||||
et les impasses vivent dans `tasks/`, qui n'est pas versionné. Ni le fichier
|
|
||||||
ni le corps du commit ne les portent.
|
|
||||||
|
|
||||||
Cela vaut aussi pour l'existant, mais **au fur et à mesure** : on corrige les
|
|
||||||
commentaires du fichier qu'on touche, au moment où on le touche, et non en une
|
|
||||||
passe qui réécrirait le dépôt. Le hook `pre-commit` liste ce qui est à relire
|
|
||||||
dans les fichiers indexés, sans jamais bloquer le commit ; le même outil se
|
|
||||||
lance à la main :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
|
||||||
python3 script/analyse/check_comment_hygiene.py --staged
|
|
||||||
```
|
|
||||||
|
|
||||||
🔴 `identifiant` est une trouvaille, à retirer. 🟡 `récit` est un signal à
|
|
||||||
relire : l'outil ne sait pas si la phrase énonce un fait durable ou raconte
|
|
||||||
une journée, et ne tranche pas à votre place. 🟡 `nom` en est un autre, et sa
|
|
||||||
limite est plus dure : un nom d'hôte NU ne se distingue mécaniquement ni d'un
|
|
||||||
mot ordinaire ni du nom d'un logiciel, donc l'outil ne voit que la forme
|
|
||||||
pleinement qualifiée. Un miroir de paquets nommé dans un commentaire y
|
|
||||||
répond, et se confirme d'un coup d'œil ; l'absence de signal ne prouve rien
|
|
||||||
sur les noms.
|
|
||||||
|
|
||||||
Trois exemples pris dans ce dépôt, leurs noms propres masqués — une règle qui
|
|
||||||
interdit de nommer ne se cite pas elle-même en clair.
|
|
||||||
|
|
||||||
`qemu_manage.py` — la dernière phrase, « Vécu sur « <VM> », annoncée en
|
|
||||||
<adresse> au lieu de <adresse> », part en entier. Les deux qui la précèdent
|
|
||||||
disent déjà tout, une fois l'imparfait du récit passé au présent : « une VM
|
|
||||||
renommée, dont le bail porte encore l'ancien nom d'hôte, SE VOIT attribuer la
|
|
||||||
passerelle ».
|
|
||||||
|
|
||||||
`todo.py` — « recopier « <base_client>_neutralize_upgrade_18 » oblige à
|
|
||||||
regarder ce qu'on détruit » devient « recopier un nom long oblige à regarder
|
|
||||||
ce qu'on détruit, là où « o » se tape par réflexe ». L'exemple ne servait qu'à
|
|
||||||
illustrer « long ».
|
|
||||||
|
|
||||||
`qemu_install.py` — le relevé daté des miroirs, qui répondait et qui non tel
|
|
||||||
jour, part : c'est l'état d'une journée. « Aucun miroir ne réplique tout,
|
|
||||||
d'où plusieurs entrées plutôt qu'une » reste : c'est la raison d'être de la
|
|
||||||
liste, et elle est vraie demain.
|
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
- Branches : `develop` (développement), `master` (production)
|
- Branches : `develop` (développement), `master` (production)
|
||||||
- Pas de submodules Git — utilise **Google Repo** pour les addons
|
- Pas de submodules Git — utilise **Google Repo** pour les addons
|
||||||
- Manifests XML dans `manifest/` pour chaque version Odoo
|
- Manifests XML dans `manifest/` pour chaque version Odoo
|
||||||
- Format de commit : `[TYPE] scope: subject`, sujet **en anglais**, à
|
- Format de commit : `[TYPE] description` (ex: `[FIX]`, `[UPD]`, `[ADD]`, `[REM]`)
|
||||||
l'impératif, 72 caractères au plus. Tags réellement utilisés : `[UPD]`,
|
|
||||||
`[FIX]`, `[ADD]`, `[IMP]`, `[REF]`.
|
|
||||||
- **Nommer les fichiers à l'indexation, jamais `git add -A`** : `private/` et
|
|
||||||
`tasks/` ne sont pas suivis EXPRÈS, et un ratissage les commit. Il emporte
|
|
||||||
aussi ce qui est en cours ailleurs dans le checkout, sous un sujet qui ne le
|
|
||||||
couvre pas. `git add -p` quand un fichier porte deux sujets.
|
|
||||||
|
|
||||||
### Le sujet
|
|
||||||
|
|
||||||
Le sujet est lu cent fois pour une fois que le corps l'est — `git log
|
|
||||||
--oneline`, un blame, une note de version, un bisect. Il a une seule tâche :
|
|
||||||
dire **sur quoi porte le code**.
|
|
||||||
|
|
||||||
L'épreuve : le lire seul, sans diff ni corps. Sait-on quelle partie du système
|
|
||||||
est en jeu, et ce qui y est désormais différent ? Sinon il n'est pas fini.
|
|
||||||
|
|
||||||
Nommer la chose, puis ce qui change pour elle. Le symptôme, le message d'erreur
|
|
||||||
cité et la métaphore sont des PREUVES, et une preuve va dans le corps — un
|
|
||||||
sujet bâti sur elles se lit bien et n'apprend rien. La portée dit OÙ, les mots
|
|
||||||
après le deux-points doivent dire QUOI.
|
|
||||||
|
|
||||||
Le sujet résume le commit ENTIER, pas sa plus grosse pièce. S'il lui faut un
|
|
||||||
« et » entre deux choses sans rapport, c'étaient deux commits.
|
|
||||||
|
|
||||||
Si le travail n'entre décidément pas dans une phrase de 72 caractères, ne pas
|
|
||||||
en écrire une amputée : des **mots-clés qui résument**, séparés par des
|
|
||||||
virgules, en disent plus dans la même place — `[FIX] proxmox: pmxcfs down,
|
|
||||||
pvesm silent, diagnosis at the source`. C'est un repli, pas un défaut : la
|
|
||||||
phrase reste préférable quand elle tient.
|
|
||||||
|
|
||||||
Un garde-fou refuse le mécanique. Sur le sujet : tag absent, plus de 72
|
|
||||||
caractères, ouverture sur une citation. Sur le corps : plus de 10 lignes pour
|
|
||||||
une langue, une adresse IP, un courriel, un chemin de compte. Sur l'ordre des
|
|
||||||
langues : un marqueur `--- EN ---`, une section française qui ne s'ouvre pas
|
|
||||||
sur le sujet traduit sous le même tag. Que le sujet soit bien en anglais
|
|
||||||
reste à l'auteur. Ce qui reste un jugement — « ce corps raconte-t-il
|
|
||||||
l'enquête » — n'est vérifié par personne.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config core.hooksPath script/git/hooks # une fois par clone
|
|
||||||
git commit --no-verify # exception légitime
|
|
||||||
```
|
|
||||||
|
|
||||||
Le mode d'emploi complet, avec des exemples avant/après pris dans l'historique
|
|
||||||
de ce dépôt, est dans `conf/template_claude_commands_commit.md`.
|
|
||||||
|
|
||||||
### Tout commit assisté par IA
|
|
||||||
|
|
||||||
Trois exigences, sans exception — `AI_POLICY.md` en donne la raison :
|
|
||||||
|
|
||||||
- Trailer `Assisted-by: <modèle>`, une ligne par modèle. C'est **binaire** :
|
|
||||||
il y a eu IA ou non, aucun seuil à apprécier.
|
|
||||||
- **Jamais** d'IA dans `Co-authored-by:` — ce champ est réservé aux humains.
|
|
||||||
- Message **bilingue, l'anglais d'abord** : le sujet et le corps en anglais,
|
|
||||||
puis `--- FR ---`, puis le sujet traduit en français sous le même tag, puis
|
|
||||||
le corps traduit. `--- EN ---` n'existe plus : l'ordre ne varie jamais.
|
|
||||||
|
|
||||||
Court et direct : **8 lignes par langue**, 10 est un plafond. Le corps dit
|
|
||||||
pourquoi c'était nécessaire, puis s'arrête. Rien de ce que le diff montre
|
|
||||||
déjà ; on garde le mode de défaillance, le chiffre qui borne et la
|
|
||||||
vérification. Le bilinguisme achète la concision, il ne l'excuse pas.
|
|
||||||
|
|
||||||
Le corps obéit aux mêmes deux règles que les commentaires : **rien
|
|
||||||
d'identifiant**, et **le fonctionnement plutôt que l'enquête**. Le corps dit
|
|
||||||
ce que le code fait ou refuse DÉSORMAIS ; il ne raconte ni la séance, ni les
|
|
||||||
hypothèses écartées, ni qui s'est trompé. Une mesure se généralise à sa classe
|
|
||||||
de situation — « sur une base de production », jamais son nom.
|
|
||||||
|
|
||||||
Le mode d'emploi complet — résolution dynamique du modèle, gabarit, identité
|
|
||||||
git, taille des correctifs — est dans
|
|
||||||
`conf/template_claude_commands_commit.md`, déployable en `/commit` par
|
|
||||||
`TODO › Execute › GPT code › Claude configs`.
|
|
||||||
|
|
|
||||||
9
.claude/rules/06-code-generator.md
Normal file
9
.claude/rules/06-code-generator.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Code Generator
|
||||||
|
|
||||||
|
ERPLibre inclut un système de génération de modules Odoo :
|
||||||
|
- `script/code_generator/new_project.py` — Créer un nouveau module
|
||||||
|
- `script/code_generator/create_from_existing_module.py` — Cloner un module existant
|
||||||
|
- `addons/TechnoLibre_odoo-code-generator/` — Moteur de génération
|
||||||
|
- `addons/TechnoLibre_odoo-code-generator-template/` — Templates
|
||||||
|
|
||||||
|
Documentation : `doc/CODE_GENERATOR.md`
|
||||||
9
.claude/rules/08-deployment.md
Normal file
9
.claude/rules/08-deployment.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Déploiement
|
||||||
|
|
||||||
|
- **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6)
|
||||||
|
- **Systemd** : `script/systemd/` pour les services
|
||||||
|
- **Nginx** : `script/nginx/` pour le reverse proxy
|
||||||
|
- **SSL** : Certbot pour les certificats
|
||||||
|
- **DNS** : `script/deployment/update_dns_cloudflare.py`
|
||||||
|
|
||||||
|
Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker).
|
||||||
|
|
@ -45,8 +45,5 @@
|
||||||
2. **Verify Plan**: Check in before starting implementation.
|
2. **Verify Plan**: Check in before starting implementation.
|
||||||
3. **Track Progress**: Mark items complete as you go.
|
3. **Track Progress**: Mark items complete as you go.
|
||||||
4. **Explain Changes**: High-level summary at each step.
|
4. **Explain Changes**: High-level summary at each step.
|
||||||
5. **Document Results**: Add review section to `tasks/todo.md`. L'enquête,
|
5. **Document Results**: Add review section to `tasks/todo.md`
|
||||||
les mesures datées, les impasses et les traces d'exécution restent LÀ.
|
|
||||||
`tasks/` n'est pas versionné : il porte ce que ni le code ni le commit ne
|
|
||||||
doivent porter. Ne les fais pas remonter.
|
|
||||||
6. **Capture Lessons**: Update `tasks/lessons.md` after corrections
|
6. **Capture Lessons**: Update `tasks/lessons.md` after corrections
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||||
},
|
|
||||||
"enabledPlugins": {
|
|
||||||
"superpowers@claude-plugins-official": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
name: erplibre-code-generator
|
|
||||||
description: >-
|
|
||||||
Génération de modules Odoo dans ERPLibre : créer un module, cloner un
|
|
||||||
module existant, où vivent le moteur et les gabarits, et l'hygiène des
|
|
||||||
commentaires du code produit. À charger avant de générer ou de cloner
|
|
||||||
un module.
|
|
||||||
---
|
|
||||||
|
|
||||||
ERPLibre inclut un système de génération de modules Odoo :
|
|
||||||
- `script/code_generator/new_project.py` — Créer un nouveau module
|
|
||||||
- `script/code_generator/create_from_existing_module.py` — Cloner un module existant
|
|
||||||
- `addons/TechnoLibre_odoo-code-generator/` — Moteur de génération
|
|
||||||
- `addons/TechnoLibre_odoo-code-generator-template/` — Templates
|
|
||||||
|
|
||||||
Documentation : `doc/CODE_GENERATOR.md`
|
|
||||||
|
|
||||||
## Les commentaires du code produit
|
|
||||||
|
|
||||||
Le code généré porte des commentaires comme le reste, et la même règle : ils
|
|
||||||
disent COMMENT ça marche, ils ne portent rien d'identifiant et ils ne
|
|
||||||
racontent pas l'enquête. Voir `.claude/rules/04-code-conventions.md`.
|
|
||||||
|
|
||||||
Un module généré à partir d'une base existante hérite de ce qu'elle contient :
|
|
||||||
relire ses commentaires et ses docstrings avant de committer, un nom de client
|
|
||||||
ou de base y arrive tout seul.
|
|
||||||
|
|
||||||
## Le nettoyage au fur et à mesure
|
|
||||||
|
|
||||||
La règle et ses deux garde-fous sont dans `.claude/rules/04-code-conventions.md`. Une invocation lui manque, utile
|
|
||||||
sur du code généré en masse : elle ne relève que les trouvailles.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
|
||||||
```
|
|
||||||
|
|
@ -34,17 +34,7 @@ make db_clone_test_to_test2 # Cloner test -> test2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
### Unitaires python (rapides, sans base de données)
|
### Suite complète
|
||||||
```bash
|
|
||||||
make test_unit # tout ce qui tourne sans DB ni Odoo
|
|
||||||
make test_unit_file F=test/test_qemu_motd.py # un seul fichier
|
|
||||||
```
|
|
||||||
Ils lisent le code et exécutent les fragments de shell générés par `todo.py`,
|
|
||||||
avec `sudo`, `pgrep` et `pkill` bouchonnés — quelques secondes. Le lanceur
|
|
||||||
annonce sa dépendance à `mobile/erplibre_home_mobile` : absent, les tests du
|
|
||||||
transfert mobile se déclarent ignorés au lieu de passer en silence.
|
|
||||||
|
|
||||||
### Suite complète (base de données requise)
|
|
||||||
```bash
|
```bash
|
||||||
make test # Tests de base + format
|
make test # Tests de base + format
|
||||||
make test_full_fast # Tests complets en parallèle
|
make test_full_fast # Tests complets en parallèle
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
---
|
|
||||||
name: erplibre-deployment
|
|
||||||
description: >-
|
|
||||||
Déploiement ERPLibre : Docker, systemd, nginx, SSL, DNS, plateformes
|
|
||||||
supportées, et le choix de l'interpréteur Python (EL_PYTHON_PROVIDER)
|
|
||||||
comme du gestionnaire de paquets (EL_PIP_PROVIDER). À charger pour
|
|
||||||
déployer, installer ou changer de fournisseur Python.
|
|
||||||
---
|
|
||||||
|
|
||||||
- **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6)
|
|
||||||
- **Systemd** : `script/systemd/` pour les services
|
|
||||||
- **Nginx** : `script/nginx/` pour le reverse proxy
|
|
||||||
- **SSL** : Certbot pour les certificats
|
|
||||||
- **DNS** : `script/deployment/update_dns_cloudflare.py`
|
|
||||||
- **VPN** : `script/vpn/` — cinq pilotes (L2TP/IPsec PSK, WireGuard,
|
|
||||||
OpenVPN, OpenConnect, sshuttle), profils en JSON et secrets dans un
|
|
||||||
coffre KeePassXC. Mode d'emploi : `script/vpn/README.md`.
|
|
||||||
|
|
||||||
Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3,
|
|
||||||
Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Leap 16 et Tumbleweed,
|
|
||||||
Arch Linux,
|
|
||||||
macOS (mise ou pyenv),
|
|
||||||
Windows (WSL/Docker).
|
|
||||||
|
|
||||||
Ubuntu 20.04 et 22.04 sont abandonnées : pikepdf exige qpdf >= 12.2, compilé
|
|
||||||
en C++20, quand focal livre GCC 9.
|
|
||||||
|
|
||||||
## Interpréteur Python
|
|
||||||
|
|
||||||
`EL_PYTHON_PROVIDER` (dans `env_var.sh`) vaut `auto`, `mise` ou `pyenv`.
|
|
||||||
`auto` reprend un interpréteur DÉJÀ posé, quel que soit le fournisseur ; sinon
|
|
||||||
il préfère mise, qui pose un CPython précompilé, et retombe sur pyenv, qui le
|
|
||||||
compile.
|
|
||||||
|
|
||||||
Un seul fichier décide : `script/install/lib_python_provider.sh`. mise n'est
|
|
||||||
jamais installé automatiquement — `make install_mise` porte cette décision.
|
|
||||||
Pas de binaire mise pour s390x à ce jour : cette architecture reste sur pyenv.
|
|
||||||
|
|
||||||
## Le Python de l'outillage
|
|
||||||
|
|
||||||
`conf/python-erplibre-version` (3.14.7) donne l'interpréteur de
|
|
||||||
`.venv.erplibre`, le venv d'outillage, distinct du venv Odoo (3.12.10 pour
|
|
||||||
Odoo 18.0). C'est la seule version que `script/` vise : là où une distribution
|
|
||||||
ne la porte pas, pyenv la compile.
|
|
||||||
|
|
||||||
Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige
|
|
||||||
`>=3.12.10,<3.13`. Pour l'outillage, la majeure.mineure suffit : exiger le
|
|
||||||
patch écarterait le Python d'une distribution d'un cran en retard — NixOS 25.11
|
|
||||||
livre 3.14.2 — et ferait compiler CPython pour rien.
|
|
||||||
|
|
||||||
`install_erplibre.sh` passe par `install_venv.sh`, donc par
|
|
||||||
`EL_PYTHON_PROVIDER`. Un venv dont `bin/python` n'est pas compatible (même
|
|
||||||
majeure.mineure, patch au moins égal) est DÉTRUIT puis rebâti : ce qui y avait
|
|
||||||
été posé à la main part avec lui. Le venv d'Odoo, lui, n'est rebâti que s'il
|
|
||||||
est HORS SERVICE : une simple différence de version le laisse en place, parce
|
|
||||||
que le rebâtir refait une installation Poetry entière. Un répertoire sans
|
|
||||||
`pyvenv.cfg` n'est jamais effacé.
|
|
||||||
|
|
||||||
`make` / `make todo` hors venv : TODO se relance dans `.venv.erplibre` ; si le
|
|
||||||
venv manque, il propose `install_erplibre.sh` en terminal, ou nomme la
|
|
||||||
commande.
|
|
||||||
|
|
||||||
Le hook `pre-commit` relaie `script/analyse/check_python_version.py` : il
|
|
||||||
signale le source qui ne parse pas sous cette version, sans bloquer, et dit
|
|
||||||
quand aucun interpréteur de cette version n'était là pour vérifier.
|
|
||||||
|
|
||||||
L'image Docker de production bâtit `.venv.erplibre` sur le Python d'Odoo de son
|
|
||||||
image de base, et s'arrête si ce Python ne sait pas lire `script/`.
|
|
||||||
|
|
||||||
## Paquets Python
|
|
||||||
|
|
||||||
`EL_PIP_PROVIDER` (dans `env_var.sh`) vaut `auto`, `uv` ou `pip`. `auto` prend
|
|
||||||
uv s'il est présent et si le venv visé est en Python ≥ 3.8, sinon pip ; un
|
|
||||||
échec d'uv retombe sur pip. Un seul fichier décide :
|
|
||||||
`script/install/lib_pip_provider.sh`. uv n'est jamais installé
|
|
||||||
automatiquement — `make install_uv` porte cette décision.
|
|
||||||
|
|
||||||
Portée réelle : le venv d'outils et l'amorçage de Poetry. **`poetry install`
|
|
||||||
n'est pas concerné** — uv ne lit pas `poetry.lock` — et c'est pourtant l'étape
|
|
||||||
qui domine. Sur s390x le gain est quasi nul : une trentaine de paquets sans
|
|
||||||
roue se compilent, et uv n'enlève pas une seconde de gcc.
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.8.0
|
1.6.0
|
||||||
31
.github/dependabot.yml
vendored
31
.github/dependabot.yml
vendored
|
|
@ -1,30 +1,13 @@
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
# Security updates reuse this entry only when its directory matches the
|
|
||||||
# manifest's own; they scan requirement/ as "/requirement", so both are listed.
|
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
directories:
|
directory: "/"
|
||||||
- "/"
|
|
||||||
- "/requirement"
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
# Odoo versions kept only as migration sources: their pins stay frozen.
|
|
||||||
# Globs cover requirements, ignore_requirements, pyproject and poetry.lock.
|
|
||||||
exclude-paths:
|
exclude-paths:
|
||||||
- "**/*odoo12.0_*"
|
- "requirement/requirements.odoo12.0_python3.7.17.txt"
|
||||||
- "**/*odoo13.0_*"
|
- "requirement/requirements.odoo13.0_python3.7.17.txt"
|
||||||
- "**/*odoo14.0_*"
|
- "requirement/requirements.odoo14.0_python3.8.20.txt"
|
||||||
- "**/*odoo15.0_*"
|
- "requirement/requirements.odoo15.0_python3.8.20.txt"
|
||||||
- "**/*odoo16.0_*"
|
- "requirement/requirements.odoo16.0_python3.10.18.txt"
|
||||||
- "**/*odoo17.0_*"
|
- "requirement/requirements.odoo17.0_python3.10.18.txt"
|
||||||
ignore:
|
|
||||||
# meteostat 2.x caps pytz below 2024, which would freeze the timezone
|
|
||||||
# data of every Odoo instance; 1.x leaves pytz free.
|
|
||||||
- dependency-name: "meteostat"
|
|
||||||
update-types: ["version-update:semver-major"]
|
|
||||||
groups:
|
|
||||||
# Each aiobotocore accepts only a narrow botocore range, and boto3 pins
|
|
||||||
# botocore: bumped one by one, each pull request breaks the resolution.
|
|
||||||
# Version updates only; a security fix still arrives on its own.
|
|
||||||
aws:
|
|
||||||
patterns: ["aiobotocore", "botocore", "boto3"]
|
|
||||||
|
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -38,8 +38,6 @@ manifest/default.dev.xml
|
||||||
poetry.lock
|
poetry.lock
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
.erplibre-semver-version
|
.erplibre-semver-version
|
||||||
# State of what this checkout has installed.
|
|
||||||
.erplibre-state.json
|
|
||||||
.erplibre-version
|
.erplibre-version
|
||||||
.odoo-version
|
.odoo-version
|
||||||
.poetry-version
|
.poetry-version
|
||||||
|
|
@ -52,6 +50,3 @@ screencasts
|
||||||
private/todo/todo_override_private.json
|
private/todo/todo_override_private.json
|
||||||
.erplibre.error.txt
|
.erplibre.error.txt
|
||||||
/mobile/erplibre_home_mobile/
|
/mobile/erplibre_home_mobile/
|
||||||
|
|
||||||
# Working notes, kept out of tracked files by the code conventions.
|
|
||||||
tasks/
|
|
||||||
|
|
|
||||||
37
.ruff.toml
37
.ruff.toml
|
|
@ -1,37 +0,0 @@
|
||||||
# Formatage et tri des imports de l'OUTILLAGE ERPLibre — script/, test/,
|
|
||||||
# long_test/ et les scripts de la racine.
|
|
||||||
#
|
|
||||||
# ruff et non black : black 24.8.0 ne connaît aucune cible au-delà de py313,
|
|
||||||
# quand l'outillage tourne sur conf/python-erplibre-version, et son tri
|
|
||||||
# d'imports remplace isort sans second outil. C'est aussi ce que la norme OCA
|
|
||||||
# emploie depuis qu'elle a quitté black.
|
|
||||||
#
|
|
||||||
# Les modules Odoo n'obéissent PAS à ce fichier : chaque dépôt d'addons porte
|
|
||||||
# sa propre norme. script/maintenance/format_python.sh aiguille entre les deux.
|
|
||||||
#
|
|
||||||
# target-version borne ce que le formateur ÉCRIT, et non ce qui exécute le
|
|
||||||
# code. py310 et non la version du venv : PEP 758 autorise depuis 3.14
|
|
||||||
# « except A, B: » sans parenthèses, et ruff l'écrirait — or les hooks de
|
|
||||||
# script/git/hooks portent « #!/usr/bin/env python3 » et tournent donc sur le
|
|
||||||
# Python du système, qu'une distribution livre encore en 3.10.
|
|
||||||
target-version = "py310"
|
|
||||||
# 79, la largeur que le dépôt tient déjà. La norme OCA en retient 88, déclarée
|
|
||||||
# dans les dépôts d'addons qui la suivent.
|
|
||||||
line-length = 79
|
|
||||||
|
|
||||||
# Des dépôts SÉPARÉS, rapatriés par Google Repo : ils portent leur propre
|
|
||||||
# configuration, et les reformater écrirait dans l'historique d'autrui.
|
|
||||||
# force-exclude : sans lui, l'exclusion ne vaut que pour les fichiers que ruff
|
|
||||||
# DÉCOUVRE, et un chemin nommé en argument y échappe — or format_python.sh
|
|
||||||
# passe les fichiers un par un.
|
|
||||||
force-exclude = true
|
|
||||||
extend-exclude = [
|
|
||||||
"addons",
|
|
||||||
"odoo*",
|
|
||||||
"script/OCA_maintainer-tools",
|
|
||||||
"script/OCA_odoo-module-migrator",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint]
|
|
||||||
# Le tri des imports seul : le lint du dépôt reste décrit par .flake8.
|
|
||||||
select = ["I"]
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
<!---------------------------->
|
|
||||||
<!-- multilingual suffix: en, fr -->
|
|
||||||
<!-- no suffix: en -->
|
|
||||||
<!---------------------------->
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
|
|
||||||
# Generative AI / LLM Policy
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
ERPLibre adopts the [OCA Generative AI / LLM
|
|
||||||
Policy](https://github.com/dixmit/oca.github/blob/ai_policy/AI_POLICY.md).
|
|
||||||
The text below is a summary; the OCA document is the reference.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
ERPLibre adopte la [politique IA générative / LLM de
|
|
||||||
l'OCA](https://github.com/dixmit/oca.github/blob/ai_policy/AI_POLICY.md).
|
|
||||||
Le texte ci-dessous en est un résumé ; le document de l'OCA fait foi.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
## The short version
|
|
||||||
|
|
||||||
- Using AI tools to help you is fine.
|
|
||||||
- Handing over responsibility to them is not.
|
|
||||||
- Every contribution comes from a human who understands it and answers for
|
|
||||||
it, however it was produced.
|
|
||||||
- Any AI involvement means an `Assisted-by:` trailer. It is binary: either
|
|
||||||
there was AI involvement or there was not, with no threshold to judge.
|
|
||||||
- AI tools never go in `Co-authored-by:`.
|
|
||||||
- Unsupervised agentic tools are not permitted.
|
|
||||||
- If you cannot explain and defend every line, do not submit it.
|
|
||||||
- During review, engage with the feedback. Regenerating and resubmitting is
|
|
||||||
not an answer, and neither is "the AI wrote it".
|
|
||||||
- Do not post AI-generated review comments or summaries you have not
|
|
||||||
fact-checked yourself.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
## En bref
|
|
||||||
|
|
||||||
- Se faire aider par des outils d'IA ne pose pas de problème.
|
|
||||||
- Leur abandonner la responsabilité, si.
|
|
||||||
- Toute contribution vient d'un humain qui la comprend et en répond, quelle
|
|
||||||
qu'ait été sa fabrication.
|
|
||||||
- Tout recours à l'IA se déclare par un trailer `Assisted-by:`. C'est
|
|
||||||
binaire : il y a eu IA ou non, sans seuil à apprécier.
|
|
||||||
- Un outil d'IA ne figure jamais dans `Co-authored-by:`.
|
|
||||||
- Les outils agentiques non supervisés sont interdits.
|
|
||||||
- Si vous ne savez pas expliquer et défendre chaque ligne, ne soumettez pas.
|
|
||||||
- En revue, répondez au fond. Régénérer et resoumettre n'est pas une réponse,
|
|
||||||
« c'est l'IA qui l'a écrit » non plus.
|
|
||||||
- Ne publiez pas de commentaires ni de résumés générés par IA sans les avoir
|
|
||||||
vérifiés vous-même.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
## Declaring AI use
|
|
||||||
|
|
||||||
Add one `Assisted-by:` line per model, in the same shape as
|
|
||||||
`Co-authored-by:`, with no blank line between them:
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
## Déclarer l'usage de l'IA
|
|
||||||
|
|
||||||
Ajoutez une ligne `Assisted-by:` par modèle, sur le même modèle que
|
|
||||||
`Co-authored-by:`, sans ligne vide entre elles :
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
|
|
||||||
```text
|
|
||||||
Assisted-by: Claude Opus 4.6
|
|
||||||
Assisted-by: GitHub Copilot:gpt-5
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
The trailer says nothing about the quality of the work. It applies to every
|
|
||||||
level of use, from a piece of advice to fully autonomous coding.
|
|
||||||
|
|
||||||
`Co-authored-by:` must not name an AI tool: authorship of a work by a machine
|
|
||||||
is legally undefined. Disclosure is expected and welcome; it does not reduce
|
|
||||||
the contributor's responsibility one bit.
|
|
||||||
|
|
||||||
## Size and pace
|
|
||||||
|
|
||||||
Reviewer burden is roughly *quantity × rate*. A patch under 30 lines in a
|
|
||||||
single file is the reference point. A contribution over 500 lines needs prior
|
|
||||||
agreement with a maintainer. Contribute at a pace and size a volunteer can
|
|
||||||
actually absorb.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This policy covers contributions to ERPLibre. Anything ERPLibre sends
|
|
||||||
upstream to the OCA is governed directly by the OCA document, including its
|
|
||||||
metrics framework and its consequences.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
Le trailer ne dit rien de la qualité du travail. Il vaut pour tout niveau
|
|
||||||
d'usage, du simple conseil au codage entièrement autonome.
|
|
||||||
|
|
||||||
`Co-authored-by:` ne doit pas nommer un outil d'IA : la paternité d'une œuvre
|
|
||||||
par une machine est juridiquement indéfinie. La déclaration est attendue et
|
|
||||||
bienvenue ; elle ne diminue en rien la responsabilité du contributeur.
|
|
||||||
|
|
||||||
## Taille et rythme
|
|
||||||
|
|
||||||
La charge du relecteur vaut à peu près *quantité × fréquence*. Le repère est
|
|
||||||
un correctif de moins de 30 lignes dans un seul fichier. Au-delà de 500
|
|
||||||
lignes, l'accord préalable d'un mainteneur est nécessaire. Contribuez à un
|
|
||||||
rythme et dans un volume qu'un bénévole peut absorber.
|
|
||||||
|
|
||||||
## Portée
|
|
||||||
|
|
||||||
Cette politique couvre les contributions à ERPLibre. Ce qu'ERPLibre remonte
|
|
||||||
à l'OCA relève directement du document de l'OCA, cadre de métriques et
|
|
||||||
sanctions compris.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
Adapted from the OCA policy, itself based on the policy of the *attrs*
|
|
||||||
project. The OCA document was led by Stuart J Mackintosh, with significant
|
|
||||||
contribution from Enric Tobella Alomar, and reviewed by the OCA Governance
|
|
||||||
Working Group.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
## Crédits
|
|
||||||
|
|
||||||
Adapté de la politique de l'OCA, elle-même fondée sur celle du projet
|
|
||||||
*attrs*. Le document de l'OCA a été mené par Stuart J Mackintosh, avec une
|
|
||||||
contribution notable d'Enric Tobella Alomar, et revu par le Governance
|
|
||||||
Working Group de l'OCA.
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
# Generative AI / LLM Policy
|
|
||||||
|
|
||||||
|
|
||||||
ERPLibre adopte la [politique IA générative / LLM de
|
|
||||||
l'OCA](https://github.com/dixmit/oca.github/blob/ai_policy/AI_POLICY.md).
|
|
||||||
Le texte ci-dessous en est un résumé ; le document de l'OCA fait foi.
|
|
||||||
|
|
||||||
|
|
||||||
## En bref
|
|
||||||
|
|
||||||
- Se faire aider par des outils d'IA ne pose pas de problème.
|
|
||||||
- Leur abandonner la responsabilité, si.
|
|
||||||
- Toute contribution vient d'un humain qui la comprend et en répond, quelle
|
|
||||||
qu'ait été sa fabrication.
|
|
||||||
- Tout recours à l'IA se déclare par un trailer `Assisted-by:`. C'est
|
|
||||||
binaire : il y a eu IA ou non, sans seuil à apprécier.
|
|
||||||
- Un outil d'IA ne figure jamais dans `Co-authored-by:`.
|
|
||||||
- Les outils agentiques non supervisés sont interdits.
|
|
||||||
- Si vous ne savez pas expliquer et défendre chaque ligne, ne soumettez pas.
|
|
||||||
- En revue, répondez au fond. Régénérer et resoumettre n'est pas une réponse,
|
|
||||||
« c'est l'IA qui l'a écrit » non plus.
|
|
||||||
- Ne publiez pas de commentaires ni de résumés générés par IA sans les avoir
|
|
||||||
vérifiés vous-même.
|
|
||||||
|
|
||||||
|
|
||||||
## Déclarer l'usage de l'IA
|
|
||||||
|
|
||||||
Ajoutez une ligne `Assisted-by:` par modèle, sur le même modèle que
|
|
||||||
`Co-authored-by:`, sans ligne vide entre elles :
|
|
||||||
|
|
||||||
|
|
||||||
```text
|
|
||||||
Assisted-by: Claude Opus 4.6
|
|
||||||
Assisted-by: GitHub Copilot:gpt-5
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Le trailer ne dit rien de la qualité du travail. Il vaut pour tout niveau
|
|
||||||
d'usage, du simple conseil au codage entièrement autonome.
|
|
||||||
|
|
||||||
`Co-authored-by:` ne doit pas nommer un outil d'IA : la paternité d'une œuvre
|
|
||||||
par une machine est juridiquement indéfinie. La déclaration est attendue et
|
|
||||||
bienvenue ; elle ne diminue en rien la responsabilité du contributeur.
|
|
||||||
|
|
||||||
## Taille et rythme
|
|
||||||
|
|
||||||
La charge du relecteur vaut à peu près *quantité × fréquence*. Le repère est
|
|
||||||
un correctif de moins de 30 lignes dans un seul fichier. Au-delà de 500
|
|
||||||
lignes, l'accord préalable d'un mainteneur est nécessaire. Contribuez à un
|
|
||||||
rythme et dans un volume qu'un bénévole peut absorber.
|
|
||||||
|
|
||||||
## Portée
|
|
||||||
|
|
||||||
Cette politique couvre les contributions à ERPLibre. Ce qu'ERPLibre remonte
|
|
||||||
à l'OCA relève directement du document de l'OCA, cadre de métriques et
|
|
||||||
sanctions compris.
|
|
||||||
|
|
||||||
|
|
||||||
## Crédits
|
|
||||||
|
|
||||||
Adapté de la politique de l'OCA, elle-même fondée sur celle du projet
|
|
||||||
*attrs*. Le document de l'OCA a été mené par Stuart J Mackintosh, avec une
|
|
||||||
contribution notable d'Enric Tobella Alomar, et revu par le Governance
|
|
||||||
Working Group de l'OCA.
|
|
||||||
66
AI_POLICY.md
66
AI_POLICY.md
|
|
@ -1,66 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
# Generative AI / LLM Policy
|
|
||||||
|
|
||||||
|
|
||||||
ERPLibre adopts the [OCA Generative AI / LLM
|
|
||||||
Policy](https://github.com/dixmit/oca.github/blob/ai_policy/AI_POLICY.md).
|
|
||||||
The text below is a summary; the OCA document is the reference.
|
|
||||||
|
|
||||||
|
|
||||||
## The short version
|
|
||||||
|
|
||||||
- Using AI tools to help you is fine.
|
|
||||||
- Handing over responsibility to them is not.
|
|
||||||
- Every contribution comes from a human who understands it and answers for
|
|
||||||
it, however it was produced.
|
|
||||||
- Any AI involvement means an `Assisted-by:` trailer. It is binary: either
|
|
||||||
there was AI involvement or there was not, with no threshold to judge.
|
|
||||||
- AI tools never go in `Co-authored-by:`.
|
|
||||||
- Unsupervised agentic tools are not permitted.
|
|
||||||
- If you cannot explain and defend every line, do not submit it.
|
|
||||||
- During review, engage with the feedback. Regenerating and resubmitting is
|
|
||||||
not an answer, and neither is "the AI wrote it".
|
|
||||||
- Do not post AI-generated review comments or summaries you have not
|
|
||||||
fact-checked yourself.
|
|
||||||
|
|
||||||
|
|
||||||
## Declaring AI use
|
|
||||||
|
|
||||||
Add one `Assisted-by:` line per model, in the same shape as
|
|
||||||
`Co-authored-by:`, with no blank line between them:
|
|
||||||
|
|
||||||
|
|
||||||
```text
|
|
||||||
Assisted-by: Claude Opus 4.6
|
|
||||||
Assisted-by: GitHub Copilot:gpt-5
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The trailer says nothing about the quality of the work. It applies to every
|
|
||||||
level of use, from a piece of advice to fully autonomous coding.
|
|
||||||
|
|
||||||
`Co-authored-by:` must not name an AI tool: authorship of a work by a machine
|
|
||||||
is legally undefined. Disclosure is expected and welcome; it does not reduce
|
|
||||||
the contributor's responsibility one bit.
|
|
||||||
|
|
||||||
## Size and pace
|
|
||||||
|
|
||||||
Reviewer burden is roughly *quantity × rate*. A patch under 30 lines in a
|
|
||||||
single file is the reference point. A contribution over 500 lines needs prior
|
|
||||||
agreement with a maintainer. Contribute at a pace and size a volunteer can
|
|
||||||
actually absorb.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This policy covers contributions to ERPLibre. Anything ERPLibre sends
|
|
||||||
upstream to the OCA is governed directly by the OCA document, including its
|
|
||||||
metrics framework and its consequences.
|
|
||||||
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
Adapted from the OCA policy, itself based on the policy of the *attrs*
|
|
||||||
project. The OCA document was led by Stuart J Mackintosh, with significant
|
|
||||||
contribution from Enric Tobella Alomar, and reviewed by the OCA Governance
|
|
||||||
Working Group.
|
|
||||||
|
|
@ -25,526 +25,6 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Added
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Ajouté
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- Site presets for the VPN: one `.json` carries a site's gateway, protocol and connection group, and neither a username nor a secret, so it can be handed around. Read from `conf/vpn_presets/`, then from a git-ignored `private/vpn/presets/`, then from any directory listed in `vpn_preset_paths`; on the same identifier the latest wins, so a site fixes a shipped template without touching a tracked file
|
|
||||||
- Import a Cisco AnyConnect `.xml` profile from the menu, browsing the client's own directories or typing the path, and get a preset from its `HostName`, `HostAddress` and `UserGroup`
|
|
||||||
- OpenConnect tells apart the two mechanisms that designate a service on one concentrator: the connection group in the URL and the value picked from a dropdown. Confusing them hands over another service's login form, so correct credentials are refused with nothing naming the group
|
|
||||||
- Reach a gateway that demands an embedded browser for SAML, which stops OpenConnect on « No SSO handler »: the web step is delegated to an `openconnect-sso` helper that the installer offers to set up, and the tunnel is then brought up by the driver itself, so the interface name, the state files and the diagnosis stay with the profile
|
|
||||||
- Declare a concentrator that compares only the first characters of a password: it is announced before the secret is stored, and nothing is ever truncated
|
|
||||||
- The VPN installer looks for `vpnc-script` — a file, not a binary on the `PATH` — and names the package to install per distribution family, instead of letting the tunnel fail on an interface that never appears
|
|
||||||
- A command launched by the VPN runner gets `/dev/null` on standard input, so a captured-output command can no longer be stopped by a SIGTTOU and freeze the machine's package manager
|
|
||||||
- The VPN profile list marks which profiles carry a live tunnel, so connecting one already up asks first and disconnecting one already down says so instead of looking like a mistake. A profile is judged on its interface, not on the state file a tunnel killed without `down` leaves behind — a state left over used to be reported as mounted on the same screen that declared the process gone
|
|
||||||
- `Assistant › LLM` — ask a model server over several turns, local or remote. The history lives in memory and dies with the menu, `/save` being the only way to keep a trace; every command carries a leading slash, so a question pasted over several lines stays one turn. A third-party destination has to be retyped before the first send
|
|
||||||
- Server recognition across eleven ports and twelve families, identity read from the response BODY and never from the port: one port hosts up to three products, and one family re-serves another's whole native API
|
|
||||||
- Finding a server from six sources — the loopback, this machine's QEMU domains, the hosts of `~/.ssh/config`, an address or a network typed by hand, and the networks read over SSH on another machine. Anything wider than a `/24` is refused before enumeration, and two preferences bound the sweep: `assistant_sweep_workers`, `assistant_sweep_timeout`
|
|
||||||
- A gpt catalogue in `script/todo/assistant/gpt/`: one Markdown file per tool, whose declared requirements are matched against what the server announces. An unknown never greys a tool out — only a requirement contradicted by a field actually read does, with the figure that refuses it
|
|
||||||
- A declared READ-ONLY context per tool, files and allowlisted commands, shown and confirmed before the first send, capped in size and duration, and scanned for identifying data. The scan's honest limit is stated: it sees addresses, e-mails and account paths, not names
|
|
||||||
- `Execute › GPT code › Claude Code` — list the machine's sessions, ask one a question with read-only tools, or resume one in its own terminal. A copy is branched by default, since two writers on one session lose a branch
|
|
||||||
- `check_comment_hygiene.py` signals a fully qualified machine name in a comment, as a re-read signal and never a finding: a BARE host name is mechanically indistinguishable from an ordinary word, so the absence of a signal proves nothing about names. The copyright header, the RFC 2606 domains and any address carried by a URL are left alone
|
|
||||||
- Transform data, a menu entry that opens an external file — Excel, Access, CSV, XML or JSON — reports what it holds, then draws an anonymised copy of it. The original is never touched. Numbers are drawn from the measured extent of their own column rather than a fixed 0-1000 range, which turns a tax rate into 743 and a year into 12; they are drawn without replacement, since a hundred distinct keys in a range of a hundred otherwise yield sixty-six outputs and a fixture that no longer reimports. Text takes a French word from a local dictionary of 1366, assigned by a counter rather than a draw, and a portable mapping table gives the same client the same word across a whole batch
|
|
||||||
- Eleven structural labels are left alone on their NAME — `id`, `create_uid`, `sequence`, `active` and the rest, plus anything ending in `/id` or `/.id` — since a test set has to keep working. Every OTHER column is judged on its measured CONTENT, never on the strength of its name. In an import-ready export, `partner_id` holds the relation's display name and `display_name` is the data itself, so deciding on the label copied the most identifying columns of the file verbatim while announcing them as protected
|
|
||||||
- The anonymiser rereads the bytes it has just written and refuses the copy when a value it announced as replaced survives in it. A workbook holds data in a dozen places that are not cells: a pivot cache and an external link each carry an entire copy of the source, a chart caches its categories and its axis titles, a custom number format carries a label, a comment carries its author, a cell hyperlink carries a path. The guard depends on no list of those, so a place nobody thought to clean produces a refusal instead of a silence — and what stays by decision, a header row or a column left alone, is named on screen before anything is written
|
|
||||||
- Transform data anonymises an Odoo database too, the entry asking for the SOURCE rather than the format: an external file, a live database, or a backup zip taken from `image_db/`. A zip is never modified — it is restored into a database, anonymised there, then dumped into a NEW zip by Odoo's own backup, so the dump, the filestore and the manifest are written by the software that will read them back. A live database is modified ITSELF, which is said before the work and not after, and nothing is drawn when the write did not happen: declining otherwise handed back a zip of the untouched database, announced as anonymised. A register records which databases the entry produced, since the server does not
|
|
||||||
- An anonymised number can keep its digit count, on the file anonymiser and on `anonymize.py` alike (`--keep-digits`): 8839 then draws in 1000..9999, so the copy keeps columns of the same width, which an export read by eye or imported into a bounded field asks for. Each value keeps ITS own width and not its column's — 7 stays at one digit where 12 keeps two — and below the unit the rule does not apply, 0.15 having no digit before the point. The option trades one guarantee for another: the measured extent no longer bounds anything, so a rate or a year can leave its range, and the preview SAYS so rather than letting it be found at reimport. Uniqueness is what yields when a band fills up, a duplicate of the same width beating a value of a different one, and the band never exceeds what the type RECEIVING the draw holds, which is not always the column's: an Odoo `integer` over a column PostgreSQL did not create as an integer — what an upgraded base carries — is poured into `numeric`, which has no ceiling, where `integer` stops at 2 147 483 647 and a draw past it aborts the whole write, that write being one transaction
|
|
||||||
- The anonymiser asks four questions instead of eleven, seven of the eleven having had an obvious default. Answering them one by one to arrive at the same place makes prompts be passed by reflex, and a prompt passed by reflex consents to nothing; they now live behind « Advanced options? ». The short path DECLARES what it takes, in sentences rather than in yes/no, since those are decisions taken and not questions whose answer was lost. Macros and charts are asked on both paths: they are not visible in the cells
|
|
||||||
- `markdown-it-py` is declared as a functional dependency: it renders the language model's markdown to HTML, before sanitising, on the assistant page. It reached the environment transitively through `bandit` → `rich`, a lint tool, so removing a lint dependency removed a portal page's rendering engine
|
|
||||||
- A download cache shared by the QEMU VMs of a host, installed from **Deployment › QEMU cache**. Two VMs of the same distribution stop pulling the same hundreds of megabytes twice: a package file is served from disk, while an index is always taken from upstream, so a withdrawn package can never turn into a « failed retrieving file … 404 ». An index is stored all the same and only comes back out when upstream is unreachable, which is what makes an offline deployment possible. The cache never shrinks by itself: `--status` says what it occupies
|
|
||||||
- Interception is transparent and covers the whole host bridge, so a VM cannot opt out from the inside. Every VM trusts the cache's authority as long as the service runs. To take ONE machine out, tick « keep this VM out of the download cache » when deploying: its MAC address is fixed before creation and an exception is posted on the host. To take them all out, stop the service — its rules leave with it. A Proxmox host that is itself a VM here is no exception: the machines it carries come out behind its address, so the deployment poses the authority in each of them, by ssh, before installing anything
|
|
||||||
- The measurement is not limited to Arch: any catalogue system whose package family is known, and either a batch of packages or the real install of ERPLibre and Odoo 18. Measured on Ubuntu 24.04 with that real install, the second VM pulled **zero byte** of package from the network and finished 19 % faster. Verified on the seven systems of the catalogue
|
|
||||||
- Git is MIRRORED rather than cached: its protocol is a negotiation, the server computing its answer from what the client already holds, so no answer is reusable. A bare mirror per upstream repository is kept on the host and served locally, and a mirror already held serves with no network at all — measured, a repository cloned inside a VM while the cache's upstream was cut. Measured on a full ERPLibre install: the second VM pulled **zero** git request from upstream, where git had been four fifths of the traffic. **Deployment › QEMU cache › Git mirrors** fills them ahead from the manifests, so the first VM does not pay every clone
|
|
||||||
- An offline deployment also covers the base suite of a cloud image. Its apt REVALIDATES the index it ships instead of downloading it, upstream answers 304, and the cache had no body to keep — so that one suite, and only it, was missing offline while its `-updates` and `-security` neighbours came off the disk. A conditional request for something the cache does not hold now goes upstream without its condition, once, and a client that holds its own copy gets 304 rather than 504 when upstream is mute.
|
|
||||||
- What an offline deployment covers, exactly: packages, repository indexes and git. NOT what the cache is forbidden to decrypt — a client carrying its own trust store, npm and poetry among them, is tunnelled opaquely, and a tunnel carries nothing once the network is gone. A full ERPLibre install therefore still needs the network, though everything the cache holds is served from disk
|
|
||||||
- A mirror is COMPLETE where `repo sync` clones at depth one, so it costs tens of gigabytes. Below ten gigabytes free, no new mirror is created and the request goes back upstream. The diagnosis says what the objects and the mirrors each occupy
|
|
||||||
- **Deployment › QEMU cache › Age and cleanup** groups the cache by age of last use (day, week or month; objects and git repositories apart) and gives back what has not served for a chosen delay, or everything. A served object has its date renewed, so « old » means « no longer used ». Both cleanups say what would go before erasing anything, and entry 5 lists the mirrors heaviest first to remove one
|
|
||||||
- `long_test/qemu_cache.py` measures whether the cache really serves the second VM, and `--hors-ligne` cuts the upstream of the cache service alone to prove a third VM still builds from the stored index
|
|
||||||
- Before cutting, the form says whether the cache holds the base suite of each system asked for: F5 warns « cache holds nothing for ubuntu 26.04 » and a second F5 goes ahead anyway. The verdict is given only where a release is named unambiguously in the URL — the apt families — rather than reassuring wrongly elsewhere. The verdict is given component by component — `main`, `universe`, `restricted`, `multiverse`, on the base suite and on its `-updates` and `-security` companions — because a single stored URL used to silence it while a whole component was missing, and apt only said so twenty minutes later, by « Unable to locate package »
|
|
||||||
- **Deployment › QEMU cache › Copy it to another machine** carries the store to another ERPLibre host in a single stream — `tar`, compressed, over ssh, no intermediate file for tens of gigabytes — and hands the files to the service account on arrival, the same account rarely bearing the same number on two machines. Only the STORE travels: an object is keyed by its URL and never by the machine that fetched it, and a git mirror is a repository. The settings stay behind — bridge, subnet and authority belong to the host, and entry 1 poses them there
|
|
||||||
- The deployment form carries a **Network** section with « No internet connection »: for the whole deployment, install included, the cache service loses its way out AND the VMs lose theirs — ping, other ports, UDP, IPv6 — so a step that takes any path but the cache fails instead of quietly succeeding online. Names no longer resolve through the internet either: the host answers every name itself with an address the cache intercepts, so only what the cache holds can be reached; dnsmasq must be installed on the host. A VM behind the cache pulls from one fixed apt mirror: cloud-init's mirror search discards every mirror behind a resolver that answers every name, and would fall back on another mirror than the one the previous VMs filled the cache from. The cut refuses rather than drops, so an address the cache does not hold answers 504 at once instead of after a connection timeout; a kernel that cannot load the reject module gets the dropping set instead. Offered only where the cache runs, and the deployment refuses rather than run with the upstream still up, a VM built that way succeeding for the wrong reason. **Proxmox VE** carries the same box, offered where its host is itself a VM of this bridge: its guests come out behind its address, so the local cut covers them and nothing is posted on the remote host. A Proxmox host that does not live here is never offered it — nothing here can cut its way out
|
|
||||||
- A Proxmox VE deployment can ask for **3D acceleration**. The VM is created with an accelerated screen (`--vga virtio-gl`) rather than the serial console as its display, and the account joins the GPU groups INSIDE the guest: the render node belongs to `root:render`, so without them every GL application falls back to software rendering while the VIRGL negotiation reports success, and nothing says so. The box is offered only where the remote host has a render node AND the three libraries Proxmox loads for that screen — VIRGL, GL and EGL. It names the missing ones and refuses to start the machine, once its disk is written and its configuration posted, so a host carrying GL without EGL would fail at the very last step. Where a piece is missing the form says which, and the package to install on the host: a box that vanishes without a word reads as a regression. A button installs them without leaving the screen — the form hands the terminal back so sudo can ask for a password and apt can be watched — then reads the host again rather than taking apt's word for it, and the box appears in place of the message. The serial port stays posted, so `qm terminal` keeps working
|
|
||||||
- Offline, the cache replays what an online pass saw, not only 200 bodies: redirects, definitive refusals (404/410) and HEAD answers of volatile URLs are kept without a body, under keys of their own, and served ONLY when upstream is mute. A TUF client probing the next version of its root gets the 404 it expects instead of a 504, so mise installs Python offline with its Sigstore verification intact; GNOME extensions, Claude Code's installer and rtk's version lookup follow their redirects offline
|
|
||||||
- The offline cut ends when the last installation ends, not when the monitor closes: a root unit, handed the lift at launch, waits for every installation's exit marker and survives the monitor closed early, todo.py killed or the terminal gone — 12 h at most. The monitor is therefore required while offline. A second offline deployment is refused while one runs, and an online deployment started meanwhile is told it would run offline
|
|
||||||
- F5 also reads the previous offline runs of the same VM and warns « at least N addresses were missing », minus what the store holds now (`erplibre_go_qemu_cache --detient`, read-only, no root). **Deployment › QEMU cache › Fill what offline runs lacked** replays them online, through the cache
|
|
||||||
- The install log names the commit the VM runs; offline, the recap says, branch by branch, which commit the cache's mirror will give
|
|
||||||
- `long_test/qemu_cache.py --distro tous` (or a comma list) chains one campaign per catalogue system, destroys each system's VMs before the next, and ends on a table of verdict, durations and upstream bytes. A failure does not stop the series
|
|
||||||
- The QEMU download cache can clean itself up every day: by age (`EL_PURGE_AGE`, e.g. `90j`) and by size ceiling (`EL_MAX_SIZE`, e.g. `50G`, the least recently served going first, objects and git mirrors alike). Both are off by default and set from **Deployment › QEMU cache › Automatic cleanup**, which previews what would go; `--purge-to-size` runs the ceiling by hand. A reinstall keeps the values chosen
|
|
||||||
- NixOS 25.11 as a deployable system, alongside the eight others. It is the only one no distribution publishes a cloud image for: the image is rebuilt by a third party, so its release is pinned, its sha256 verified at every download, and its origin printed before anything is created. cloud-init receives no network configuration there — its networkd renderer takes the key of a block as an interface NAME, where netplan honours a `match:` — and the account is created with `/bin/sh`, sshd refusing an account whose shell does not exist
|
|
||||||
- ERPLibre installed on NixOS, its dependencies DECLARED in `conf/nixos/erplibre.nix` and applied by `nixos-rebuild`, where the four other families install them one command at a time. `services.envfs` answers `/usr/bin/env` and `programs.nix-ld` gives manylinux wheels the dynamic loader they ask for; the headers of what has no wheel are linked into the system profile, and its libraries reach the loader too, a module compiled on the machine carrying no RPATH. The install reaches it from the menu like any other distribution: the bootstrap poses git, make and python3 — absent from that image — in the user profile for the clone, and « make install_os » redeclares them for the system. Checked on a VM: the 362-package lock installs and Odoo answers over HTTP
|
|
||||||
- A `nix + nixos-anywhere` option on the OTHER distributions, the reverse of picking NixOS: it leaves an ordinary VM able to install NixOS onto any machine reachable over SSH. Nix is called by absolute path, the remote shell's PATH being frozen before the installer drops the binary, and the flake features are repeated on the command line, writing `nix.conf` needing a sudo that may fail
|
|
||||||
- NixOS deploys on a **Proxmox** host as it does on QEMU/KVM, which took three fixes no code reading could have found. Its image has no BIOS boot sector: a VM created in SeaBIOS reports « running » with a silent console, so the catalogue now marks which images need UEFI — one marker per distribution, since Debian 13 boots in SeaBIOS on that same host. `--ciuser` and `--sshkeys` defer to the image's DEFAULT account, which NixOS declares as another name: the repository's own cloud-config now travels as a snippet for every distribution, carrying an explicit `users:` block. And the cloud-init drive moves to the SCSI bus — an image built for virtio alone has no ATA driver and never sees an IDE drive, so cloud-init hunted network datasources and the VM came up with no account at all. Checked end to end: NixOS answers with `/bin/sh`, Debian 13 with `/bin/bash`, sudo on both
|
|
||||||
- The third-party image is verified against the sum the repository pins for it on the Proxmox path too, where a bare `wget` used to be enough. One accessor carries that sum for both paths, and the check runs even on a cached image — the case aimed at is a file substituted or truncated between deployments, which a presence test cannot see
|
|
||||||
- One entry of the download cache can be removed, by URL. A checksum that does not match invalidated nothing: the store kept serving the same bytes, and re-downloading changed nothing since the store is what answers. Neither existing purge reaches it — `--purge` erases everything, and `--purge-older-than` skips what is recent while every service resets that date, so a poisoned object that keeps being served never ages. `--oublie` is symmetric to `--detient`: same input lines, same key functions, same refusals, and a present object that resists is reported as a refusal rather than a forget. Both checksum failures now name it, with the exact URL
|
|
||||||
- The install log carries what the host decided before launching: the download cache authority placed or refused, the bypass, the mirror. Those lines were said on a console that scrolls away, while the file reopened after a failure held only the symptom — on a guest with no trust store, a refused certificate, hundreds of derivations to build and six hundred lines of errors, without a word on the cause
|
|
||||||
- One entry of the download cache can be forgotten from the menu, under « Age and cleanup ». The binary could already do it; the menu offered only the two bulk purges, neither of which reaches a single object — « erase what has not served » never reaches one the service rejuvenates each time it serves it, and « erase everything » costs the whole cache for one file. `--detient` runs first and is the preview: same line, same key, nothing modified
|
|
||||||
- `erplibre_go_qemu_cache --recle` stores a cache's objects again under the current key, without downloading anything, and merges the copies a mirror carried under several paths. Objects written under the former key rule stay on disk but become UNREACHABLE, so the service asks upstream for them again and the space they hold serves no one: on a store of 12 764 objects, 5 419 were in that case — 9.11 GiB — and merging the duplicates returned about 3.37 GiB. The service must be stopped, the body being renamed before its meta, and `--dry-run` only counts what would move. A status-only entry is left alone, its key carrying the host rather than the path
|
|
||||||
- The `pre-commit` hook runs `check_python_version.py` on staged files: it reports source that does not parse under the Python of `conf/python-erplibre-version`, without blocking the commit, and says when no such interpreter was there to check. Neither black nor flake8 sees that fault — black's target bounds what it writes, never what it accepts
|
|
||||||
- `Deploy › Local › [4]` opens a SOCKS proxy over SSH — `ssh -D`, port 1080 by default — so the browser reaches, FROM the remote machine, an interface listening only on its loopback or a host of its network. The address comes from `~/.ssh/config` or by hand; an alias is passed to ssh as is, so its `ProxyJump` still applies and a nested VM stays reachable. The Firefox settings print before the tunnel opens, the command only returning on Ctrl+C
|
|
||||||
- `make format_test` formats `test/` and `long_test/`, which no target covered: 75 files out of 210 followed no standard, and only a file a diff reported was ever touched
|
|
||||||
- `script/reverse_proxy/main.py` — a development reverse proxy for Odoo 18 without nginx: one address sends pages to the web port and `/websocket` to the bus port, with the `X-Forwarded-*` headers of `proxy_mode` set by the proxy alone (`--trust-forwarded` extends a chain instead, behind another proxy). Only the request head is read, so an upgraded WebSocket stays open and a gzip or chunked response passes byte for byte. It serves HTTPS with `--tls-cert`/`--tls-key`, logs one line per request (route, status, duration; `--quiet` silences it), says at start which Odoo port does not answer, and names that port in a 502. A request head waits 30 s (408) and the connection to Odoo 10 s (504), never an open WebSocket. Listens on `127.0.0.1:8080` by default, `--listen 0.0.0.0` opens it to the network; one request per connection, production keeps nginx
|
|
||||||
- `script/reverse_proxy/local_cert.py` issues a local authority, imported once in the browser, and a server certificate it signs for localhost, the host name and its addresses, under `~/.erplibre/reverse_proxy_tls/` with keys in 0600; the authority is kept when the certificate is issued again
|
|
||||||
- `TODO › Execute › Network › Odoo reverse proxy` starts it with the web and bus ports read from `config.conf`, asks for this machine only or the whole network and for HTTP or HTTPS, issues the local certificate on first HTTPS use, and warns when `proxy_mode` is off or `workers` is 0 — without a worker no bus port listens and `/websocket` fails. `Network › Local TLS certificates` issues it again with extra names
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Des préréglages de site pour le VPN : un `.json` porte la passerelle d'un site, son protocole et son groupe de connexion, et ni identifiant ni secret, si bien qu'il peut circuler. Lus depuis `conf/vpn_presets/`, puis depuis un `private/vpn/presets/` ignoré par git, puis depuis tout répertoire listé dans `vpn_preset_paths` ; sur un même identifiant le plus tardif gagne, et un site corrige un gabarit livré sans toucher de fichier suivi
|
|
||||||
- Importer un profil Cisco AnyConnect `.xml` depuis le menu, en parcourant les répertoires du client ou en tapant le chemin, et en tirer un préréglage de ses balises `HostName`, `HostAddress` et `UserGroup`
|
|
||||||
- OpenConnect distingue les deux mécanismes qui désignent un service sur un même concentrateur : le groupe de connexion dans l'URL et la valeur choisie dans un menu déroulant. Les confondre donne le formulaire d'un autre service, et des identifiants justes sont refusés sans que rien ne nomme le groupe
|
|
||||||
- Joindre une passerelle qui exige un navigateur intégré pour le SAML, ce qui arrête OpenConnect sur « No SSO handler » : l'étape web est déléguée à un greffon `openconnect-sso` que l'installateur propose de poser, et le tunnel est ensuite monté par le pilote lui-même, si bien que le nom d'interface, les fichiers d'état et le diagnostic restent au profil
|
|
||||||
- Déclarer un concentrateur qui ne compare que les premiers caractères d'un mot de passe : il est annoncé avant le dépôt du secret, et rien n'est jamais tronqué
|
|
||||||
- L'installateur VPN cherche `vpnc-script` — un fichier, et non un binaire du `PATH` — et nomme le paquet à poser par famille de distribution, au lieu de laisser le tunnel échouer sur une interface qui n'apparaît jamais
|
|
||||||
- Une commande lancée par l'exécuteur VPN reçoit `/dev/null` sur son entrée standard, si bien qu'une commande à sortie capturée ne peut plus être arrêtée par un SIGTTOU et figer le gestionnaire de paquets de la machine
|
|
||||||
- La liste des profils VPN marque ceux qui portent un tunnel vivant, si bien que connecter un profil déjà monté demande confirmation et que déconnecter un profil déjà tombé le dit au lieu de ressembler à une erreur. Un profil est jugé sur son interface et non sur le fichier d'état qu'un tunnel tué sans `down` laisse derrière lui — un état laissé était annoncé monté sur l'écran même qui déclarait le processus mort
|
|
||||||
- `Assistant › LLM` — interroger un serveur de modèle sur plusieurs tours, local ou distant. L'historique vit en mémoire et meurt avec le menu, `/save` étant le seul moyen d'en garder une trace ; toute commande porte une barre oblique initiale, donc une question collée sur plusieurs lignes reste un seul tour. Une destination tierce doit être retapée avant le premier envoi
|
|
||||||
- Reconnaissance du serveur sur onze ports et douze familles, l'identité étant lue dans le CORPS de la réponse et jamais dans le port : un port héberge jusqu'à trois produits, et une famille réémet l'API native d'une autre en entier
|
|
||||||
- Recherche d'un serveur depuis six sources — la boucle locale, les domaines QEMU de la machine, les hôtes de `~/.ssh/config`, une adresse ou un réseau saisi, et les réseaux lus en SSH sur une autre machine. Plus large qu'un `/24` est refusé avant énumération, et deux préférences bornent le balayage : `assistant_sweep_workers`, `assistant_sweep_timeout`
|
|
||||||
- Un catalogue d'outils gpt dans `script/todo/assistant/gpt/` : un fichier Markdown par outil, dont les exigences déclarées sont confrontées à ce que le serveur annonce. L'inconnu ne grise jamais un outil — seule une exigence contredite par un champ réellement lu le fait, avec le chiffre qui la refuse
|
|
||||||
- Un contexte LECTURE SEULE déclaré par outil, fichiers et commandes autorisées, montré et confirmé avant le premier envoi, borné en taille et en durée, et balayé à la recherche de données identifiantes. La limite du balayage est dite : il voit les adresses, les courriels et les chemins de compte, pas les noms
|
|
||||||
- `Exécution › GPT code › Claude Code` — lister les sessions de la machine, en interroger une avec des outils en lecture seule, ou la reprendre dans son propre terminal. Une copie est branchée par défaut, deux écritures sur une même session perdant une branche
|
|
||||||
- `check_comment_hygiene.py` signale un nom de machine pleinement qualifié dans un commentaire, en signal à relire et jamais en trouvaille : un nom d'hôte NU ne se distingue mécaniquement pas d'un mot ordinaire, donc l'absence de signal ne prouve rien sur les noms. L'en-tête de copyright, les domaines de la RFC 2606 et toute adresse portée par une URL sont laissés tranquilles
|
|
||||||
- Transform data, une entrée de menu qui ouvre un fichier externe — Excel, Access, CSV, XML ou JSON —, rapporte ce qu'il porte, puis en tire une copie anonymisée. L'original n'est jamais touché. Les nombres sont tirés dans l'étendue mesurée de leur propre colonne plutôt que dans un intervalle fixe de 0 à 1000, qui rend un taux de TVA à 743 et une année à 12 ; ils sont tirés sans remise, cent clés distinctes dans une étendue de cent ne rendant sinon que soixante-six sorties, et une fixture qui ne se réimporte plus. Le texte prend un mot français dans un dictionnaire local de 1366, attribué par un compteur et non par un tirage, et une table de correspondance portable donne au même client le même mot sur tout un lot
|
|
||||||
- Onze étiquettes structurelles sont laissées intactes sur leur NOM — `id`, `create_uid`, `sequence`, `active` et les autres, plus tout ce qui finit en `/id` ou `/.id` —, un jeu de test devant continuer de fonctionner. Toute AUTRE colonne est jugée sur son CONTENU mesuré, jamais sur la seule foi de son nom. Dans un export import-compatible, `partner_id` porte le nom affiché de la relation et `display_name` est la donnée elle-même : décider sur l'étiquette recopiait textuellement les colonnes les plus identifiantes du fichier en les annonçant comme protégées
|
|
||||||
- L'anonymiseur relit les octets qu'il vient d'écrire et refuse la copie dès qu'une valeur annoncée comme remplacée y subsiste. Un classeur porte de la donnée dans une douzaine d'endroits qui ne sont pas des cellules : un cache de tableau croisé et un lien externe en portent chacun une copie entière, un graphique met ses catégories et ses titres d'axes en cache, un format de nombre personnalisé porte un libellé, un commentaire porte son auteur, un hyperlien de cellule porte un chemin. Le filet ne dépend d'aucune liste de ces endroits, si bien qu'un endroit que personne n'a pensé à nettoyer produit un refus et non un silence — et ce qui reste par décision, une ligne d'en-tête ou une colonne laissée intacte, est nommé à l'écran avant que rien ne soit écrit
|
|
||||||
- Transform data anonymise aussi une base Odoo, l'entrée demandant la SOURCE plutôt que le format : un fichier externe, une base vivante, ou un zip de sauvegarde pris dans `image_db/`. Un zip n'est jamais modifié — il est restauré dans une base, anonymisé là, puis vidé dans un zip NEUF par la sauvegarde d'Odoo lui-même, si bien que le dump, le filestore et le manifeste sont écrits par le logiciel qui les relira. Une base vivante est modifiée ELLE-MÊME, ce qui est dit avant le travail et non après, et rien n'est tiré lorsque l'écriture n'a pas eu lieu : un renoncement rendait sinon un zip de la base intacte, annoncé comme anonymisé. Un registre inscrit les bases que l'entrée a produites, le serveur ne le disant pas
|
|
||||||
- Un nombre anonymisé peut garder son nombre de chiffres, sur l'anonymiseur de fichiers comme sur `anonymize.py` (`--keep-digits`) : 8839 tire alors dans 1000..9999, si bien que la copie garde des colonnes de la même largeur, ce qu'un export relu à l'œil ou importé dans un champ borné demande. Chaque valeur garde SA largeur et non celle de sa colonne — 7 reste à un chiffre là où 12 en garde deux — et sous l'unité la règle ne s'applique pas, 0,15 n'ayant pas de chiffre avant la virgule. L'option échange une garantie contre une autre : l'étendue mesurée ne borne plus rien, donc un taux ou une année peut sortir de sa plage, et l'aperçu le DIT au lieu de le laisser découvrir à la réimportation. C'est l'unicité qui cède quand une bande se remplit, un doublon de la même largeur valant mieux qu'une valeur d'une autre, et la bande ne dépasse jamais ce que tient le type qui ACCUEILLE le tirage, lequel n'est pas toujours celui de la colonne : un `integer` d'Odoo posé sur une colonne que PostgreSQL n'a pas créée entière — ce qu'une base montée de version porte — se coule en `numeric`, qui n'a pas de plafond, là où `integer` s'arrête à 2 147 483 647 et où un tirage au-delà fait retomber toute l'écriture, celle-ci tenant en une transaction
|
|
||||||
- L'anonymiseur pose quatre questions au lieu de onze, sept des onze ayant eu un défaut évident. Les répondre une par une pour arriver au même endroit fait passer les invites par réflexe, et une invite qu'on passe par réflexe ne consent à rien : elles vivent désormais derrière « Options avancées ? ». Le chemin court DIT ce qu'il prend, en phrases et non en oui/non, puisque ce sont des décisions prises et non des questions dont on aurait perdu la réponse. Macros et graphiques sont demandés dans les deux chemins : ils ne se voient pas dans les cellules
|
|
||||||
- `markdown-it-py` est déclaré en dépendance fonctionnelle : il rend en HTML le markdown du modèle de langage, avant assainissement, sur la page de l'assistant. Il arrivait dans l'environnement en transitif par `bandit` → `rich`, un outil de lint, si bien que retirer une dépendance de lint retirait le moteur de rendu d'une page portail
|
|
||||||
- Un cache de téléchargement partagé par les VM QEMU d'un hôte, installé depuis **Déploiement › Cache QEMU**. Deux VM de la même distribution cessent de tirer deux fois les mêmes centaines de mégaoctets : un fichier de paquet est servi du disque, tandis qu'un index est toujours repris à l'amont, si bien qu'un paquet retiré ne peut jamais devenir un « failed retrieving file … 404 ». L'index est stocké quand même et ne ressort que si l'amont est injoignable, ce qui rend un déploiement hors ligne possible. Le cache ne diminue jamais de lui-même : `--status` dit ce qu'il occupe
|
|
||||||
- L'interception est transparente et vaut pour tout le pont de l'hôte : une VM ne peut pas s'y soustraire de l'intérieur. Toutes approuvent l'autorité du cache tant que le service tourne. Pour en soustraire UNE, cocher « soustraire cette VM au cache » au déploiement : son adresse MAC est fixée avant la création et une exception est posée sur l'hôte. Pour les soustraire toutes, arrêter le service — ses règles partent avec lui. Un hôte Proxmox qui est lui-même une VM d'ici n'y échappe pas : les machines qu'il porte sortent derrière son adresse, et le déploiement pose donc l'autorité dans chacune, par ssh, avant d'installer quoi que ce soit
|
|
||||||
- La mesure ne se limite plus à Arch : tout système du catalogue dont la famille de paquets est connue, et au choix un lot de paquets ou l'installation réelle d'ERPLibre et d'Odoo 18. Mesuré sur Ubuntu 24.04 avec cette installation réelle, la seconde VM n'a tiré **aucun octet** de paquet du réseau et a fini 19 % plus vite. Vérifié sur les sept systèmes du catalogue
|
|
||||||
- Git est mis en MIROIR plutôt que caché : son protocole est une négociation, le serveur calculant sa réponse d'après ce que le client détient déjà, si bien qu'aucune réponse ne se réutilise. Un miroir nu par dépôt amont est tenu sur l'hôte et servi localement, et un miroir déjà détenu sert sans aucun réseau — mesuré, un dépôt cloné dans une VM alors que l'amont du cache était coupé. Mesuré sur une installation complète d'ERPLibre : la seconde VM n'a tiré **aucune** requête git de l'amont, là où git pesait quatre cinquièmes du trafic. **Déploiement › Cache QEMU › Miroirs git** les remplit d'avance depuis les manifestes, pour que la première VM ne paie pas tous les clonages
|
|
||||||
- Un déploiement hors ligne couvre aussi la suite de base d'une image cloud. Son apt REVALIDE l'index qu'elle livre au lieu de le télécharger, l'amont rend 304, et le cache n'avait donc aucun corps à garder — cette suite-là, et elle seule, manquait hors ligne pendant que ses voisines `-updates` et `-security` sortaient du disque. Une requête conditionnelle sur ce que le cache ne détient pas part désormais sans sa condition, une fois ; et un client qui détient sa propre copie reçoit 304 plutôt que 504 quand l'amont est muet.
|
|
||||||
- Ce qu'un déploiement hors ligne couvre, exactement : les paquets, les index de dépôts et git. PAS ce que le cache n'a pas le droit de déchiffrer — un client qui porte son propre magasin de confiance, npm et poetry en sont, passe en tunnel opaque, et un tunnel ne porte rien une fois le réseau coupé. Une installation complète d'ERPLibre demande donc toujours le réseau, même si tout ce que le cache détient est servi du disque
|
|
||||||
- Un miroir est COMPLET là où `repo sync` clone en profondeur un : il coûte donc des dizaines de gigaoctets. Sous dix gigaoctets libres, aucun miroir neuf n'est créé et la requête repart vers l'amont. Le diagnostic dit ce qu'occupent les objets et les miroirs, séparément
|
|
||||||
- **Déploiement › Cache QEMU › Âge et nettoyage** groupe le cache par âge du dernier usage (jour, semaine ou mois ; objets et dépôts git séparément) et rend ce qui n'a plus servi depuis un délai choisi, ou tout. Un objet servi voit sa date remise à jour : « vieux » veut donc dire « n'a plus servi ». Les deux nettoyages disent ce qui partirait avant d'effacer quoi que ce soit, et l'entrée 5 liste les miroirs du plus lourd au plus léger pour en effacer un
|
|
||||||
- `long_test/qemu_cache.py` mesure si le cache sert vraiment la seconde VM, et `--hors-ligne` coupe l'amont du seul service du cache pour prouver qu'une troisième se bâtit encore sur l'index stocké
|
|
||||||
- Avant de couper, le formulaire dit si le cache détient la suite de base de chaque système demandé : F5 prévient « le cache ne détient rien pour ubuntu 26.04 » et un second F5 passe outre. Le verdict n'est rendu que là où une version se nomme sans ambiguïté dans l'URL — les familles apt — plutôt que de rassurer à tort ailleurs. Le verdict est rendu composant par composant — `main`, `universe`, `restricted`, `multiverse`, sur la suite de base et sur ses compagnes `-updates` et `-security` —, car une seule URL en réserve le rendait muet quand un composant entier manquait, et apt ne le disait que vingt minutes plus tard, par « Unable to locate package »
|
|
||||||
- **Déploiement › Cache QEMU › L'emporter sur une autre machine** porte le magasin vers un autre hôte ERPLibre en un seul flux — `tar`, compressé, par ssh, sans fichier intermédiaire pour des dizaines de gigaoctets — et rend les fichiers au compte du service à l'arrivée, le même compte portant rarement le même numéro sur deux machines. Seul le MAGASIN voyage : un objet est rangé sous son URL et jamais sous la machine qui l'a pris, et un miroir git est un dépôt. Les réglages restent — pont, sous-réseau et autorité appartiennent à l'hôte, et l'entrée 1 les pose là-bas
|
|
||||||
- Le formulaire de déploiement porte une section **Réseau** avec « Sans connexion internet » : pour tout le déploiement, installation comprise, le service du cache perd sa sortie ET les VM perdent la leur — ping, autres ports, UDP, IPv6 —, si bien qu'un pas qui prendrait un autre chemin que le cache échoue au lieu de réussir en ligne sans rien dire. Les noms ne se résolvent plus par l'internet non plus : l'hôte répond lui-même à tout nom une adresse que le cache intercepte, si bien que seul ce que le cache détient est joignable ; dnsmasq doit être installé sur l'hôte. Une VM derrière le cache tire d'un seul miroir apt, fixe : la recherche de miroir de cloud-init écarte tout miroir derrière un résolveur qui répond à tout nom, et retomberait sur un autre miroir que celui dont les VM précédentes ont rempli le cache. La coupure refuse au lieu de jeter : une adresse que le cache ne détient pas répond 504 sur-le-champ, et non après un délai de connexion ; un noyau qui ne peut pas charger le module du refus reçoit le jeu qui jette. Offerte seulement là où le cache tourne, et le déploiement refuse plutôt que de partir avec l'amont debout, une VM bâtie ainsi réussissant pour la mauvaise raison. **Proxmox VE** porte la même case, offerte là où son hôte est lui-même une VM de ce pont : ses invités sortent derrière son adresse, si bien que la coupure locale les couvre et que rien n'est posé sur l'hôte distant. Un hôte Proxmox qui ne vit pas ici ne la reçoit jamais — rien ici ne sait couper sa sortie
|
|
||||||
- Un déploiement Proxmox VE peut demander l'**accélération 3D**. La VM est créée avec un écran accéléré (`--vga virtio-gl`) plutôt que la console série comme affichage, et le compte entre dans les groupes du GPU DANS l'invité : le nœud de rendu appartient à « root:render », si bien que sans eux toute application GL retombe en rendu logiciel alors que la négociation VIRGL a réussi, et rien ne le signale. La case n'est offerte que là où l'hôte distant a le nœud de rendu ET les trois bibliothèques que Proxmox charge pour cet écran — VIRGL, GL et EGL. Il nomme celles qui manquent et refuse de démarrer la machine, une fois son disque écrit et sa configuration posée : un hôte portant GL sans EGL échouerait donc à la toute dernière étape. Là où une pièce manque, le formulaire dit laquelle et quel paquet poser sur l'hôte — une case qui disparaît sans un mot se lit comme une régression. Un bouton les pose sans quitter l'écran — le formulaire rend le terminal pour que sudo puisse demander un mot de passe et qu'on voie apt travailler —, puis relit l'hôte au lieu de croire apt sur parole, et la case prend la place du message. Le port série reste posé, donc « qm terminal » fonctionne toujours
|
|
||||||
- Hors ligne, le cache rejoue ce qu'un passage en ligne a vu, et non plus les seuls corps en 200 : redirections, refus définitifs (404/410) et réponses HEAD des adresses volatiles sont gardés sans corps, sous des clés à eux, et servis SEULEMENT quand l'amont est muet. Un client TUF qui sonde la version suivante de sa racine reçoit le 404 qu'il attend au lieu d'un 504 : mise pose Python hors ligne, vérification Sigstore intacte ; les extensions GNOME, l'installateur de Claude Code et la version de rtk suivent leurs redirections hors ligne
|
|
||||||
- La coupure hors ligne tombe avec la dernière installation, et non à la fermeture du suivi : une unité root, qui reçoit la levée au lancement, attend le marqueur de fin de chaque installation et survit au suivi fermé tôt, à todo.py tué ou au terminal perdu — 12 h au plus. Le suivi est donc obligatoire hors ligne. Un second déploiement hors ligne est refusé pendant qu'un premier tourne, et un déploiement en ligne lancé entre-temps est prévenu qu'il tournerait hors ligne
|
|
||||||
- F5 lit aussi les essais hors ligne précédents de la même VM et prévient « au moins N adresses ont manqué », moins ce que le magasin détient désormais (`erplibre_go_qemu_cache --detient`, en lecture seule, sans root). **Déploiement › Cache QEMU › Combler ce qui a manqué hors ligne** les rejoue en ligne, à travers le cache
|
|
||||||
- Le journal d'installation nomme le commit que la VM exécute ; hors ligne, le récapitulatif dit, branche par branche, quel commit le miroir du cache donnera
|
|
||||||
- `long_test/qemu_cache.py --distro tous` (ou une liste séparée par des virgules) enchaîne une campagne par système du catalogue, défait les VM de chaque système avant le suivant, et finit sur un tableau des verdicts, durées et octets d'amont. Un échec n'arrête pas la série
|
|
||||||
- Le cache de téléchargement QEMU peut se nettoyer chaque jour : par âge (`EL_PURGE_AGE`, ex. `90j`) et par plafond de taille (`EL_MAX_SIZE`, ex. `50G`, le moins récemment servi partant d'abord, objets et miroirs git confondus). Les deux sont désactivés par défaut et se règlent depuis **Déploiement › Cache QEMU › Nettoyage automatique**, qui montre ce qui partirait ; `--purge-to-size` applique le plafond à la main. Une réinstallation garde les valeurs choisies
|
|
||||||
- NixOS 25.11 parmi les systèmes déployables, à côté des huit autres. C'est le seul dont aucune distribution ne publie d'image cloud : l'image est rebâtie par un tiers, donc sa version est épinglée, sa somme sha256 vérifiée à chaque téléchargement, et son origine dite avant que rien ne soit créé. cloud-init n'y reçoit aucune configuration réseau — son moteur networkd prend la clé d'un bloc pour un NOM d'interface, là où netplan honore un `match:` — et le compte y est créé avec `/bin/sh`, sshd refusant un compte dont le shell n'existe pas
|
|
||||||
- ERPLibre installé sur NixOS, ses dépendances DÉCLARÉES dans `conf/nixos/erplibre.nix` puis appliquées par `nixos-rebuild`, là où les quatre autres familles les installent commande par commande. `services.envfs` répond à `/usr/bin/env` et `programs.nix-ld` donne aux roues manylinux le chargeur dynamique qu'elles réclament ; les en-têtes de ce qui n'a pas de roue sont liés au profil du système, et leurs bibliothèques vont aussi au chargeur, un module compilé sur la machine ne portant pas de RPATH. L'installation l'atteint depuis le menu comme toute autre distribution : l'amorçage pose git, make et python3 — absents de cette image — dans le profil de l'utilisateur pour la durée du clone, et « make install_os » les redéclare pour le système. Vérifié sur une VM : le verrou de 362 paquets s'installe et Odoo répond en HTTP
|
|
||||||
- Une option `nix + nixos-anywhere` sur les AUTRES distributions, l'inverse de choisir NixOS : elle laisse une VM ordinaire capable d'installer NixOS sur toute machine joignable en SSH. Nix est appelé par son chemin absolu, le PATH du shell distant étant figé avant que l'installateur ne pose le binaire, et les fonctions des flakes sont redonnées sur la ligne de commande, l'écriture de `nix.conf` réclamant un sudo qui peut manquer
|
|
||||||
- NixOS se déploie sur un hôte **Proxmox** comme sur QEMU/KVM, ce qui a demandé trois correctifs qu'aucune lecture de code n'aurait trouvés. Son image n'a pas de secteur d'amorçage BIOS : une VM créée en SeaBIOS se déclare « running » avec une console muette, donc le catalogue marque désormais les images qui exigent l'UEFI — un marqueur par distribution, Debian 13 démarrant en SeaBIOS sur ce même hôte. `--ciuser` et `--sshkeys` s'en remettent au compte par DÉFAUT de l'image, que NixOS nomme autrement : le cloud-config du dépôt part maintenant comme extrait pour toutes les distributions, avec son bloc `users:` explicite. Et le lecteur cloud-init passe sur le bus SCSI — une image bâtie pour virtio seul n'a pas de pilote ATA et ne voit jamais un lecteur IDE, si bien que cloud-init cherchait des sources réseau et que la VM arrivait sans aucun compte. Vérifié de bout en bout : NixOS rend `/bin/sh`, Debian 13 `/bin/bash`, sudo aux deux
|
|
||||||
- L'image tierce est vérifiée contre la somme que le dépôt épingle pour elle sur le chemin Proxmox aussi, où un `wget` nu suffisait. Un seul accesseur porte cette somme pour les deux chemins, et le contrôle vaut même pour une image déjà en cache — le cas visé est un fichier substitué ou tronqué entre deux déploiements, qu'un test de présence ne voit pas
|
|
||||||
- Une entrée du cache de téléchargement peut être retirée, par son URL. Une somme qui ne correspond pas n'invalidait rien : le magasin continuait de servir les mêmes octets, et retélécharger ne changeait rien puisque c'est lui qui répond. Aucune des deux purges ne l'atteint — `--purge` efface tout, et `--purge-older-than` saute ce qui est récent alors que chaque service remet cette date, si bien qu'un objet empoisonné qui sert ne vieillit jamais. `--oublie` est le symétrique de `--detient` : mêmes lignes en entrée, mêmes fonctions de clé, mêmes refus, et un objet présent qui résiste est dit refusé plutôt qu'oublié. Les deux échecs de somme le nomment désormais, avec l'URL exacte
|
|
||||||
- Le journal d'installation porte ce que l'hôte a décidé avant de lancer : l'autorité du cache de téléchargement posée ou refusée, l'exception, le miroir. Ces lignes se disaient sur une console qui défile, pendant que le fichier qu'on rouvre après un échec ne portait que le symptôme — sur un invité sans magasin de confiance, un certificat refusé, des centaines de dérivations à construire et six cents lignes d'erreurs, sans un mot sur la cause
|
|
||||||
- Une entrée du cache de téléchargement s'oublie depuis le menu, sous « Âge et nettoyage ». Le binaire savait déjà le faire ; le menu n'offrait que les deux purges en gros, dont aucune ne vise un objet — « effacer ce qui n'a plus servi » n'atteint jamais celui que le service rajeunit chaque fois qu'il le rend, et « tout effacer » coûte le cache entier pour un fichier. `--detient` passe d'abord et fait l'aperçu : même ligne, même clé, sans rien modifier
|
|
||||||
- `erplibre_go_qemu_cache --recle` range à nouveau les objets d'un cache sous la clé courante, sans rien retélécharger, et fond les copies qu'un miroir portait sous plusieurs chemins. Les objets écrits sous l'ancienne règle de clé restent sur le disque mais deviennent INTROUVABLES, si bien que le service les redemande à l'amont et que la place qu'ils tiennent ne sert plus personne : sur un magasin de 12 764 objets, 5 419 étaient dans ce cas — 9,11 Gio — et la fusion des doublons a rendu environ 3,37 Gio. Le service doit être arrêté, le corps étant renommé avant son méta, et `--dry-run` ne fait que compter ce qui bougerait. Un statut seul n'est pas touché, sa clé portant l'hôte et non le chemin
|
|
||||||
- Le hook `pre-commit` lance `check_python_version.py` sur les fichiers indexés : il signale le source qui ne parse pas sous le Python de `conf/python-erplibre-version`, sans bloquer le commit, et dit quand aucun interpréteur de cette version n'était là pour vérifier. Ni black ni flake8 ne voient ce défaut — la cible de black borne ce qu'il écrit, jamais ce qu'il accepte
|
|
||||||
- `Déploiement › Local › [4]` ouvre un proxy SOCKS par SSH — `ssh -D`, port 1080 par défaut — pour que le navigateur atteigne, DEPUIS la machine distante, une interface qui n'écoute que sur sa boucle locale ou un hôte de son réseau. L'adresse vient de `~/.ssh/config` ou de la saisie ; un alias part tel quel à ssh, si bien que son `ProxyJump` s'applique encore et qu'une VM imbriquée reste joignable. Le réglage de Firefox s'affiche avant l'ouverture du tunnel, la commande ne rendant la main qu'au Ctrl+C
|
|
||||||
- `make format_test` formate `test/` et `long_test/`, qu'aucune cible ne couvrait : 75 fichiers sur 210 ne suivaient aucune norme, et seul un fichier signalé par un diff était touché
|
|
||||||
- `script/reverse_proxy/main.py` — un mandataire inverse de développement pour Odoo 18, sans nginx : une seule adresse envoie les pages au port web et `/websocket` au port du bus, les en-têtes `X-Forwarded-*` de `proxy_mode` n'étant posés que par le mandataire (`--trust-forwarded` prolonge plutôt une chaîne, derrière un autre mandataire). Seule la tête de la requête est lue : une WebSocket montée reste ouverte et une réponse gzip ou en morceaux passe octet pour octet. Il sert en HTTPS avec `--tls-cert`/`--tls-key`, écrit une ligne par requête (route, statut, durée ; `--quiet` la coupe), dit au démarrage quel port d'Odoo ne répond pas, et nomme ce port dans un 502. La tête d'une requête attend 30 s (408) et la connexion à Odoo 10 s (504), jamais une WebSocket ouverte. Écoute par défaut sur `127.0.0.1:8080`, `--listen 0.0.0.0` l'ouvre au réseau ; une requête par connexion, la production garde nginx
|
|
||||||
- `script/reverse_proxy/local_cert.py` émet une autorité locale, importée une fois dans le navigateur, et un certificat serveur qu'elle signe pour localhost, le nom d'hôte et ses adresses, sous `~/.erplibre/reverse_proxy_tls/` avec des clés en 0600 ; l'autorité est gardée quand le certificat est réémis
|
|
||||||
- `TODO › Execute › Network › Mandataire inverse Odoo` le lance avec les ports web et bus lus dans `config.conf`, demande cette machine seule ou tout le réseau et HTTP ou HTTPS, émet le certificat local au premier usage en HTTPS, et prévient quand `proxy_mode` est éteint ou que `workers` vaut 0 — sans worker, aucun port de bus n'écoute et `/websocket` échoue. `Network › Certificats TLS locaux` le réémet avec d'autres noms
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Changed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Modifié
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- `Assistant › [1]` no longer sends every question to a single remote API on a fixed model: it asks whichever server is configured, and falls back to the remote one only when no local server answers
|
|
||||||
- The comment hygiene check reads Go comments, not only `#` ones: `//` outside a string, the raw string between backticks, and `/* … */` blocks
|
|
||||||
- Debian and Ubuntu `by-hash` index files are served from disk, their name being the digest of their content; `…/releases/latest/download/…` is no longer pinned to the first version seen
|
|
||||||
- An upstream that refuses or drops connections is remembered for 20 s: a request with a stored answer is served at once instead of waiting its connect timeout, which made up most of an offline install's time; a request with nothing stored still tries upstream. A git mirror skips its refresh while its forge is unreachable
|
|
||||||
- Mirror prefetch runs under the cache's service account, never as root
|
|
||||||
- The long-test menu asks before creating real machines, and asks again, in words of its own, before `--detruire` removes machines with their disks. The command is shown first, which is what makes the question answerable; a dry run or a performance report creates nothing and asks nothing
|
|
||||||
- Every entry of the Proxmox VE menu carries an icon, the same picture meaning the same action as in the other menus of the tool
|
|
||||||
- The QEMU cache binary speaks English or French: service journal, `--status`, `--age`, option help and the error served to a VM. The language comes from `--lang`, then `EL_LANG`, then French; the installer writes `EL_LANG` to the service settings and the TODO menu passes its own. Rules, verdict codes and JSON keys are never translated
|
|
||||||
- A repository index the cache already holds is revalidated with its ETag rather than downloaded again: upstream still judges every request, and a « 304 » serves the stored body from disk. On a full ERPLibre install the pip indexes, npm metadata and repo bundle had been about 110 MB per VM, taken whole each time. An index stored without its host — shared by every mirror of a rotating list — and an answer carrying no ETag are taken whole as before; the access log names the new outcome `revalidated`
|
|
||||||
- A registry page served under `Vary: Accept` keeps one copy per representation. npm asks for the same `/npm` page abridged, then complete, then abridged again; kept under one key they replaced each other and the 31 MB were fetched on every install. Each representation is now revalidated and, offline, served on its own; `--detient` still reads the page under its URL alone
|
|
||||||
- A VM deployed with the cache upstream cut — QEMU form, Proxmox VE, or `deploy_qemu.py --offline` — has npm's security audit turned off (`NPM_CONFIG_AUDIT=false`): it queries a remote service no cache can replay, and failed on every offline install. An online VM keeps its audit
|
|
||||||
- Verifying a downloaded image no longer needs `--verify`: it runs by default for every distribution that publishes a sum, and `--no-verify` is what skips it — to be kept for offline runs, where a substituted image would otherwise pass unremarked
|
|
||||||
- `--bios` is refused on an image with no BIOS boot sector, and says why. Forced there, it gave a VM reported « running » with a silent console — the very failure that flag exists to avoid elsewhere
|
|
||||||
- The tooling virtual environment `.venv.erplibre` runs Python 3.14.7, independently of the Odoo one (3.12.10 for Odoo 18.0). `install_erplibre.sh` builds it through `install_venv.sh` and `EL_PYTHON_PROVIDER` instead of the system `python3`
|
|
||||||
- `.venv.erplibre` on an incompatible Python is DELETED and rebuilt; whatever was installed in it by hand goes with it. Odoo's venv is kept when it merely differs in version, and rebuilt only when it is unusable: rebuilding it redoes a whole Poetry install. A directory without `pyvenv.cfg` is never deleted
|
|
||||||
- `make` and `make todo` run `./todo.sh`, a launcher whose name says a menu opens and not an installation; it hands every argument to `install.sh`, which picks an interpreter able to READ the code before running it: `.venv.erplibre` on the right version, else a recent enough system `python3`, else the install, which runs only on a yes typed in a terminal (`o`, `oui`, `y`, `yes`) since it can delete `.venv.erplibre`. A system older than `conf/python-erplibre-version` would otherwise stop on a syntax error raised before any guard could name the command to type. TODO then relaunches itself in `.venv.erplibre`, or offers to run `install_erplibre.sh` in a terminal
|
|
||||||
- The production Docker image builds `.venv.erplibre` on Odoo's Python and stops when that Python cannot parse `script/`
|
|
||||||
- Debian 11 is dropped from the deployment catalogue: its LTS ended, and its security suite is neither served nor archived — the index it still publishes names packages whose pool no longer holds the file, so apt stops before installing git. Debian 13 takes its place, its cloud image always being the latest point release
|
|
||||||
- Every container image is built on bookworm, whatever the Odoo version. The base is `python:<version>-slim-<suite>`: the interpreter comes from the official image, never from Debian, and the bookworm variants exist down to 3.7.17. The wkhtmltopdf build follows the suite — bullseye's requires libssl1.1, absent from bookworm
|
|
||||||
- The PATCH bounds only Odoo's venv, whose pyproject requires `>=3.12.10,<3.13`. For the tooling one, the major.minor is enough: requiring the patch turned away a distribution's Python one step behind — NixOS 25.11 ships 3.14.2 where conf asks 3.14.7 — and made pyenv COMPILE CPython for a difference nothing needs
|
|
||||||
- `make format` picks the formatter from each file's context: an Odoo module keeps isort and black on `py37`, the series still supported going down that far, while this repository's own tooling goes through ruff, configured once in `.ruff.toml`. ruff follows CPython's versions, where black 24.8.0 stops at `py313`, and its import sorting replaces isort; it is also what the OCA standard uses since it left black
|
|
||||||
- The repositories that Google Repo checks out under `script/` are excluded from that formatting, a named path included: reformatting them would write in someone else's history. `target-version` stays at `py310` there, because the git hooks carry `#!/usr/bin/env python3` and a distribution still ships 3.10 — from 3.14 on, ruff would write `except A, B:` without parentheses
|
|
||||||
- The list of Claude Code commands in `TODO › Execute › GPT code › Claude configs` compares each ERPLibre template in `conf/` with its copy in `~/.claude/commands/`: a command not installed is shown, an outdated copy is marked with its count of added and removed lines, and a command that comes from elsewhere is labelled as such. On a yes it prints the diff, then redeploys the outdated copies, keeping the git name and e-mail `/commit` carried
|
|
||||||
- A commit message opens on an English subject and an English body; `--- FR ---` then opens the French section, which starts with the subject translated under the same tag. The `commit-msg` hook refuses a `--- EN ---` marker and a French section without that title, and checks the title like a subject. `/commit` and `/git_prepare_merge` follow the same order
|
|
||||||
- TODO menus: the language is set only from Configuration, the duplicate entry in Execute is gone, and Fork moves from the main menu to Configuration. The main menu now numbers Telemetry 4 and Configuration 5. The language chooser shows a flag per language
|
|
||||||
- Odoo 18 dependencies refreshed. `openai` is pinned to 2.x, whose 3.x requires an `idna` that Odoo 18 forbids; `fsspec` is pinned beside `s3fs`, which demands it at its own exact version, so the two move together; `meteostat` returns to 1.x, every 2.x capping `pytz` below 2024. PyMuPDF stays excluded on s390x, now declared in the requirements so a regeneration keeps it. Major bumps of `ujson` 6, `plotly` 7, `python-slugify` 9 and `sqlalchemy` 2.1 are not yet tested
|
|
||||||
- Dependabot ignores the major versions of `meteostat`
|
|
||||||
- Odoo 18 moves to pandas 3.0.6, cryptography 50 with pyopenssl 26.4, Pillow 12.3 and botocore/boto3 1.43.75 with aiobotocore 3.9.1, the highest botocore its narrow range accepts. The seven modules that import pandas run their pandas calls unchanged; `freq='d'` in a Cybro attendance dashboard now warns and will break with pandas 4
|
|
||||||
- Dependabot groups `aiobotocore`, `botocore` and `boto3` into one pull request, since each aiobotocore accepts only a narrow botocore range; security fixes still arrive on their own
|
|
||||||
- `TODO › Transform data` reads Excel with openpyxl 3.1.5 and xlsxwriter 3.2.9; the leak test that guards openpyxl's exact pin passes on them
|
|
||||||
- factur-x requires 6.8 outside s390x, the version already locked, so a regeneration can no longer fall back to an untested 4.x or 5.x
|
|
||||||
- The interface chooser of the QEMU deployment and of the Odoo migration, and its preferences in `TODO › Configuration`, mark the TUI form with 📋 and the line by line questions with 💬
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- `Assistant › [1]` n'envoie plus chaque question à une seule API distante sur un modèle figé : elle interroge le serveur configuré, et ne retombe sur le distant que lorsqu'aucun serveur local ne répond
|
|
||||||
- Le contrôle d'hygiène des commentaires lit le Go, et non les seuls `#` : `//` hors d'une chaîne, la chaîne brute entre accents graves, et les blocs `/* … */`
|
|
||||||
- Les index `by-hash` de Debian et d'Ubuntu sont servis du disque, leur nom étant l'empreinte de leur contenu ; `…/releases/latest/download/…` n'est plus figé sur la première version vue
|
|
||||||
- Un amont qui refuse ou ignore les connexions est retenu 20 s : une requête qui a une réponse gardée est servie aussitôt au lieu d'attendre son délai d'établissement, qui faisait l'essentiel du temps d'une installation hors ligne ; une requête sans rien en réserve tente toujours l'amont. Un miroir git saute son rafraîchissement tant que sa forge est injoignable
|
|
||||||
- Le pré-remplissage des miroirs tourne sous le compte du service du cache, jamais en root
|
|
||||||
- Le menu des tests longs demande avant de créer de vraies machines, et redemande, avec des mots à lui, avant que `--detruire` efface des machines avec leurs disques. La commande est montrée d'abord, c'est elle qui rend la question répondable ; un plan à blanc ou un rapport de performance ne crée rien et ne demande rien
|
|
||||||
- Chaque entrée du menu Proxmox VE porte une icône, la même image voulant dire la même action que dans les autres menus de l'outil
|
|
||||||
- Le binaire du cache QEMU parle anglais ou français : journal du service, `--status`, `--age`, aide des options et erreur servie à une VM. La langue vient de `--lang`, puis d'`EL_LANG`, puis du français ; l'installateur écrit `EL_LANG` dans les réglages du service et le menu TODO passe la sienne. Règles, codes de verdict et clés JSON ne se traduisent jamais
|
|
||||||
- Un index de dépôt que le cache détient déjà est revalidé par son ETag au lieu d'être retéléchargé : l'amont juge toujours chaque requête, et un « 304 » sert le corps gardé depuis le disque. Sur une installation complète d'ERPLibre, les index pip, les métadonnées npm et le bundle de repo pesaient environ 110 Mo par VM, repris en entier à chaque fois. Un index rangé sans son hôte — partagé par tous les miroirs d'une liste qui tourne — et une réponse sans ETag sont repris en entier comme avant ; le journal d'accès nomme la nouvelle issue `revalidated`
|
|
||||||
- Une page de registre servie sous `Vary: Accept` garde une copie par représentation. npm demande la même page `/npm` abrégée, puis complète, puis de nouveau abrégée ; rangées sous une seule clé, elles se remplaçaient et les 31 Mo repartaient à chaque installation. Chaque représentation est désormais revalidée et, hors ligne, servie à part ; `--detient` lit toujours la page sous sa seule URL
|
|
||||||
- Une VM déployée l'amont du cache coupé — formulaire QEMU, Proxmox VE, ou `deploy_qemu.py --offline` — a l'audit de sécurité de npm désactivé (`NPM_CONFIG_AUDIT=false`) : il interroge un service qu'aucun cache ne rejoue, et échouait à chaque installation hors ligne. Une VM en ligne garde son audit
|
|
||||||
- Vérifier une image téléchargée ne demande plus `--verify` : c'est le défaut pour toute distribution qui publie une somme, et `--no-verify` est ce qui la saute — à réserver aux essais hors ligne, où une image substituée passerait autrement sans un mot
|
|
||||||
- `--bios` est refusé sur une image sans secteur d'amorçage BIOS, et dit pourquoi. Forcé là, il donnait une VM « running » à console muette — la panne même que ce drapeau évite ailleurs
|
|
||||||
- L'environnement virtuel d'outillage `.venv.erplibre` tourne en Python 3.14.7, indépendamment de celui d'Odoo (3.12.10 pour Odoo 18.0). `install_erplibre.sh` le bâtit par `install_venv.sh` et `EL_PYTHON_PROVIDER` plutôt qu'avec le `python3` du système
|
|
||||||
- `.venv.erplibre` sur un Python incompatible est SUPPRIMÉ puis rebâti ; ce qu'on y avait posé à la main part avec lui. Le venv d'Odoo est conservé quand seule sa version diffère, et rebâti seulement s'il est hors service : le rebâtir refait une installation Poetry entière. Un répertoire sans `pyvenv.cfg` n'est jamais effacé
|
|
||||||
- `make` et `make todo` lancent `./todo.sh`, dont le nom annonce un menu et non une installation ; il passe chaque argument à `install.sh`, qui choisit un interpréteur capable de LIRE le code avant de le lancer : `.venv.erplibre` s'il porte la bonne version, sinon le `python3` du système s'il est assez récent, sinon l'installation, qui ne part que sur un oui tapé au terminal (`o`, `oui`, `y`, `yes`) puisqu'elle peut supprimer `.venv.erplibre`. Un système plus ancien que `conf/python-erplibre-version` s'arrêterait autrement sur une erreur de syntaxe levée avant qu'aucun garde puisse nommer la commande à taper. TODO se relance ensuite dans `.venv.erplibre`, ou propose de lancer `install_erplibre.sh` en terminal
|
|
||||||
- L'image Docker de production bâtit `.venv.erplibre` sur le Python d'Odoo et s'arrête quand ce Python ne sait pas lire `script/`
|
|
||||||
- Debian 11 quitte le catalogue de déploiement : son LTS est terminé, et sa suite de sécurité n'est ni servie ni archivée — l'index qu'elle publie encore nomme des paquets dont le pool ne porte plus le fichier, et apt s'arrête avant d'installer git. Debian 13 la remplace, son image cloud étant toujours le dernier point de version
|
|
||||||
- Toutes les images de conteneur reposent sur bookworm, quelle que soit la version d'Odoo. La base est `python:<version>-slim-<version Debian>` : l'interpréteur vient de l'image officielle, jamais de Debian, et les variantes bookworm existent jusqu'à 3.7.17. Le build de wkhtmltopdf suit la version — celui de bullseye réclame libssl1.1, absente de bookworm
|
|
||||||
- Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige `>=3.12.10,<3.13`. Pour celui de l'outillage, la majeure.mineure suffit : exiger le patch écartait le Python d'une distribution d'un cran en retard — NixOS 25.11 livre 3.14.2 quand conf demande 3.14.7 — et faisait COMPILER CPython à pyenv pour une différence que rien ne réclame
|
|
||||||
- `make format` choisit le formateur d'après le contexte de chaque fichier : un module Odoo garde isort et black en `py37`, la série la plus ancienne encore supportée, quand l'outillage de ce dépôt passe par ruff, réglé une fois dans `.ruff.toml`. ruff suit les versions de CPython, là où black 24.8.0 s'arrête à `py313`, et son tri d'imports remplace isort ; c'est aussi ce qu'emploie la norme OCA depuis qu'elle a quitté black
|
|
||||||
- Les dépôts que Google Repo rapatrie sous `script/` sont écartés de ce formatage, chemin nommé compris : les reformater écrirait dans l'historique d'autrui. `target-version` y reste à `py310`, parce que les hooks git portent `#!/usr/bin/env python3` et qu'une distribution livre encore 3.10 — à partir de 3.14, ruff écrirait `except A, B:` sans parenthèses
|
|
||||||
- La liste des commandes Claude Code de `TODO › Execute › GPT code › Claude configs` compare chaque gabarit d'ERPLibre de `conf/` à sa copie de `~/.claude/commands/` : une commande non installée est affichée, une copie périmée est marquée de son compte de lignes ajoutées et retirées, et une commande venue d'ailleurs est signalée comme telle. Sur un oui, elle affiche le diff, puis redéploie les copies périmées en gardant le nom et le courriel git que portait `/commit`
|
|
||||||
- Un message de commit s'ouvre sur un sujet et un corps en anglais ; `--- FR ---` ouvre ensuite la section française, qui commence par le sujet traduit sous le même tag. Le hook `commit-msg` refuse un marqueur `--- EN ---` et une section française sans ce titre, et juge ce titre comme un sujet. `/commit` et `/git_prepare_merge` suivent le même ordre
|
|
||||||
- Menus de TODO : la langue se règle seulement depuis Configuration, l'entrée en double dans Execute disparaît, et Fork quitte le menu principal pour Configuration. Le menu principal numérote désormais Télémétrie 4 et Configuration 5. Le choix de la langue montre un drapeau par langue
|
|
||||||
- Dépendances d'Odoo 18 rafraîchies. `openai` est épinglé en 2.x, dont la 3.x exige un `idna` qu'Odoo 18 interdit ; `fsspec` est épinglé à côté de `s3fs`, qui l'exige à sa propre version exacte, si bien que les deux se montent ensemble ; `meteostat` revient en 1.x, toute 2.x plafonnant `pytz` sous 2024. PyMuPDF reste écarté sur s390x, désormais déclaré dans les requirements pour qu'une régénération le garde. Les montées majeures de `ujson` 6, `plotly` 7, `python-slugify` 9 et `sqlalchemy` 2.1 ne sont pas encore testées
|
|
||||||
- Dependabot ignore les versions majeures de `meteostat`
|
|
||||||
- Odoo 18 passe à pandas 3.0.6, cryptography 50 avec pyopenssl 26.4, Pillow 12.3 et botocore/boto3 1.43.75 avec aiobotocore 3.9.1, le plus haut botocore que sa plage étroite accepte. Les sept modules qui importent pandas exécutent leurs appels pandas sans changement ; `freq='d'` dans un tableau de bord de présence Cybro avertit désormais et cassera avec pandas 4
|
|
||||||
- Dependabot réunit `aiobotocore`, `botocore` et `boto3` dans une seule demande de fusion, chaque aiobotocore n'acceptant qu'une plage étroite de botocore ; les correctifs de sécurité arrivent toujours seuls
|
|
||||||
- `TODO › Transform data` lit Excel avec openpyxl 3.1.5 et xlsxwriter 3.2.9 ; le test de fuite qui garde l'épingle exacte d'openpyxl passe sur eux
|
|
||||||
- factur-x exige 6.8 hors s390x, la version déjà verrouillée : une régénération ne peut plus retomber sur une 4.x ou 5.x non testée
|
|
||||||
- Le choix d'interface du déploiement QEMU et de la migration Odoo, et ses préférences dans `TODO › Configuration`, marquent le formulaire TUI d'un 📋 et les questions ligne par ligne d'un 💬
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Fixed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Corrigé
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- Reading the dnsmasq leases no longer opens a root password prompt: the files are read directly, which suffices on a standard install where they are 0644, and only then is `sudo -n` tried, which fails instead of asking. Displaying a VM list called that path once per VM, and waiting on a VM called it every three seconds for ten minutes
|
|
||||||
- `--max_process` runs again on Python 3.10 and later: `loop=` left `asyncio.wait` in 3.10 and `asyncio.get_event_loop()` raises outside a running loop since 3.14, so the pool was not even constructible while the help still advertised the flag
|
|
||||||
- A prompt no longer writes its colon twice — the most-seen menu of the software asked « Command:: », and seven remote-deployment prompts showed a colon followed by another
|
|
||||||
- Eleven submenus now leave their segment in the breadcrumb, and navigation telemetry stops filing them as commands under their raw method name
|
|
||||||
- The three readers of `~/.ssh/config` agree on what a machine name is: an alias declared with a lowercase `host` is seen, a tab separates as legally as a space, and a negated `!name` pattern is no longer taken for a machine to connect to
|
|
||||||
- Three translation keys declared twice are gone, and a check refuses the next one: a repeated key silently overwrites the previous, which had already cost a menu label
|
|
||||||
- `anonymize.py` abstains from a unique numeric column and NAMES it in the report. A draw guarantees nothing on a unique column, and a number has no way out where text has one — appending the id carries uniqueness for text, but would change a number's magnitude. Two rows drawing the same number failed the UPDATE and, the whole run being one transaction, took the entire anonymisation with them. Without the report naming it, an identifying column stays in the clear with nothing saying so
|
|
||||||
- Restoring a backup no longer raises `AttributeError` before running the command: `_monitoring_restore` read `self._execute` where `TODO` sets `self.execute`, and both entries leading there — the local backup and the remote one — were broken. A check refuses the next one: every `self.X` that `TODO` READS must be set by TODO or by one of its bases. Searching the name across `script/todo/` did not see the fault, another object of the package setting `_execute`; it has to be searched in the classes TODO INHERITS from
|
|
||||||
- The Selenium scripts run on a Python built without `tkinter` — any server with no graphical toolkit package. That module is only needed by the vault's file picker, so it is now optional: with no `tkinter` and no configured KDBX path, opening the vault logs an error and returns, instead of breaking the import of every browser-automation script
|
|
||||||
- Dark mode works again in a private window on a current Firefox. The « Run in Private Windows » permission is granted when the addon is installed, through the `allowPrivateBrowsing` field, rather than clicked through the `about:addons` interface: Firefox refuses navigation to `about:addons` from the content context, and the chrome context demands `-remote-allow-system-access`, which geckodriver rejects through capabilities, so both routes to that checkbox are closed. A geckodriver that ignores the field installs without the permission instead of killing the session
|
|
||||||
- The « - Default » label appears again at the version and environment menus: both reads asked for a capitalised key the version file never writes, and a missing key returns nothing without a word
|
|
||||||
- The cache's « nothing in store » message is inert as a shell script, every line being a comment. Fed to an installer built on `curl … | bash`, it used to become a cascade of « command not found » that hid the real cause. Such a download now also asks curl to fail on an HTTP error rather than execute the error page
|
|
||||||
- The wait for a VM to be ready now covers the guest-agent install too. That one is launched as a DETACHED unit so cloud-init returns in seconds, and it runs an `apt-get update`: `cloud-init status --wait` said « done » while the package lock was still held, the next step burnt through its retries, and the install then ran on an index never refreshed — « Unable to locate package », a message that blames the repository rather than the lock. An update that never succeeds now says so on the spot
|
|
||||||
- A desktop install no longer waits minutes on the apt lock: the apt-daily SERVICE is stopped and not only its timer, a timer being disabled without interrupting the apt-get it already started; and the retry comes back every two seconds rather than every ten, `DPkg::Lock::Timeout` not covering the list lock at all
|
|
||||||
- Fedora VMs boot again: the firmware loads and starts their loader, then freezes without writing a byte — no console, no DHCP lease, a machine "running" that does nothing. Fedora is booted in legacy BIOS, where the same image starts its kernel; `--bios` still wins when asked
|
|
||||||
- A VM receives a hostname it can accept — an underscore, which a libvirt domain name tolerates, made it keep its image's generic name — and a timezone its own distribution knows, a legacy alias having left it in UTC
|
|
||||||
- starship installs in a VM: its installer runs as root, bounded by a root timeout, so it never reaches the `sudo -v` that sudo-rs refuses; its shell hook no longer prints « command not found », nor fails a sourced rc, when starship is absent
|
|
||||||
- Each optional tool says whether it was installed, and a GNOME extension whose download failed is no longer reported as unavailable for this GNOME
|
|
||||||
- mise, pyenv and GNOME extensions are downloaded, then run: without pipefail, `curl | sh` could neither report a failed download nor reach its fallback
|
|
||||||
- The guest-agent unit no longer ends in failure after a successful install
|
|
||||||
- The cache answers 508 to a request that targets the cache itself, instead of calling itself until it runs out of descriptors
|
|
||||||
- The cache diagnosis no longer reports a stopped service as running
|
|
||||||
- The wait for `apt-get update` is bounded by a deadline rather than by a number of attempts. An attempt fails in under a second on a held lock, but takes minutes when the cache answers 504 on every index it does not hold: sixty attempts were then worth hours of silence where five minutes were promised, and the install went on to fail on unmet dependencies
|
|
||||||
- A Proxmox guest is pinned to the same apt mirror as the rest of the fleet, written over ssh before anything downloads. The store keys its indexes by HOST, so a VM left on the default repositories of its image found none of what the cache had been filled with — offline, every one of those indexes was missing
|
|
||||||
- The cache installer accepts a bridge and a subnet given by hand. It probed libvirt first and died on « network default not found », so the workaround its own header advertised — `EL_BRIDGE` and `EL_SUBNET` — could never be reached. A machine whose libvirt network is not started, or which carries its bridge otherwise, can now lay the cache down by naming it; and when the probe does run and fails, it names both ways out
|
|
||||||
- **Deployment › QEMU cache › Install or reinstall** no longer announces « installed and started » when the installer failed. It caught exceptions only: a non-zero exit — a missing libvirt network, a build that gave way — printed the success line right under the error itself, along with the path of an authority that does not exist
|
|
||||||
- A Proxmox guest receives its guide, its timezone, its apt mirror and the cache authority again. All four go by ssh and used to start as soon as an address was known, while cloud-init was still creating accounts and keys: they failed together, and the VM was born in UTC, guideless, without the authority and on its image's repositories. The deployment now waits for the machine to answer ssh — five minutes at most — and says so when it never does, rather than failing four times in a row
|
|
||||||
- The cache no longer serves a repository index newer than the signature that announces it. Offline, each object came out with its own date: an index refreshed on Saturday under a Friday `InRelease` made apt fail on « File has unexpected size » or « Hash Sum mismatch », and the install stopped on unmet dependencies — a message that blames the repository, never the cache. The comparison is made on the upstream `Last-Modified`, never on the storage date, which is renewed on every hit
|
|
||||||
- The refusal to copy the cache to another machine names the gesture that lifts it, and that gesture is not entry 1 — entry 1 installs the cache HERE, so following it reinstalled the host that already had one while the target stayed empty. Three situations were reported as a single « no cache installed »: an ssh link that never ran the probe, a target carrying no cache, and a target whose cache lacks its service account. Each has its own message now, and the missing-cache one lists the steps to run ON the target, the two ways past an installer that reads the « default » libvirt network included — start libvirt, or name the bridge, a stopped libvirt making it die on a network that exists. The steps also name the branch to put the target on, read from this host: the installer is a file of the repository, so a machine left on another branch answers « no such file », which looks nothing like a missing cache. A fourth case is checked before a single byte leaves: sudo asking for a password on the target, which no terminal can answer since the store itself occupies ssh's standard input — the message gives the ticket to obtain there first. The arrival is asked for the privilege ONCE, `tar` and `chown` under a single invocation. Where sudo there wants a password, the entry offers two ways out instead of failing: the one-off sudoers line that allows it, or a two-step mode — the store is sent into the target's own account, which needs no privilege at all, and a printed command extracts it from a terminal there, where a password can be typed. That mode needs twice the store on the target, checked before a byte leaves, a cache holding only already-compressed packages and git archives. The guide carries the same section
|
|
||||||
- The Proxmox VE deployment form checks the cache before cutting the network, as the libvirt one already did: what the store lacks, no cut VM will read, and the failure used to land an hour later, at the desktop step — a message that blames the repository, never the cache. F5 again means going ahead anyway. Both forms now share a single verdict instead of two copies of it, which would have drifted apart at the first adjustment
|
|
||||||
- The cache keeps its repository indexes when a mirror list rotates. An index published under the hash of its content — `by-hash/SHA256/…` — was stored under a key carrying the host, so the same bytes served by a second mirror were fetched again; offline they were simply missing, and the install failed on files the store already held, with a message that blames the repository. Such an object is now keyed by its path alone, its name BEING the checksum of its content
|
|
||||||
- The cache diagnosis no longer reports « no redirection rule is posted » when it simply could not read them. On a host whose sudo asks for a password, `sudo -n nft` returns nothing, and that silence was read as an absence of rules — sending the operator to reinstall a cache that was redirecting correctly. The reading now carries a third state, « cannot tell », marked with a dot rather than a cross, exactly as the upstream-cut reading already did
|
|
||||||
- A lifted cut gives the upstreams their chance back at once. The service remembers, for a short while, which upstreams just failed to connect, so that an offline install does not pay the connection delay hundreds of times; nothing told it the cut was over, and the first requests after the lift fell back on the store while the network was already back. The lift now touches a witness file inside the store, which that memory consults — there is no channel at all to the running service
|
|
||||||
- Before cutting, the form also warns about what no index can reveal: a package the deployment lays down OUTSIDE the watched thread — the guest agent, installed by a detached unit whose failure surfaces nowhere — and the git repositories declared by the manifests that have no mirror yet. A held suite index was enough to call the cache complete while not one byte of that package had ever crossed it; and a git negotiation is never stored, so a repository without a mirror simply cannot be cloned once the network is gone
|
|
||||||
- The mirror warning counts only the repositories of the Odoo version being deployed. It used to add up every manifest in the repository — the deprecated one included — and announced 170 missing mirrors where a real Odoo 18 deployment meets four: an alarm that fires for nothing is one that stops being read. Filling the mirrors still takes every version ahead, which is its purpose
|
|
||||||
- A host banned from decryption on a burst of transport errors gets another chance. Three failed handshakes in a row put it in an opaque tunnel, and a tunnel never consults the store: a distribution mirror condemned by a few corrupted records sent all its traffic back upstream, including the hundreds of objects already held for it, until the service was restarted. A TLS alert still bans for good — the client looked at our certificate and refused it — but a repeated cut is only a suspicion, and it reopens after ten minutes
|
|
||||||
- **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
|
|
||||||
- `make` looks for bash instead of assuming `/bin/bash`. That path does not exist on NixOS, where the shell lives in the store, and make stopped before running any recipe — including the one that installs what creates that path. Elsewhere the resolved shell is the same one as before
|
|
||||||
- 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
|
|
||||||
- A guest with no per-file trust anchor is taken out of the download cache instead of being intercepted without one. Interception is transparent and covers the whole bridge, so a VM given no authority still fails every HTTPS download on « self-signed certificate in certificate chain » — and on a declarative system placing the authority comes too late, the first rebuild being the first download. On a Proxmox host it is the HOST that is exempted: a nested guest leaves masqueraded behind it and the bridge never sees its own address. Measured from inside the guest: code 000 and SSL verification 19, then 200 and 0. Without it the package manager fell back to building 564 derivations, whose sources failed for the same reason
|
|
||||||
- NixOS learns the download cache's authority WITHOUT a rebuild, so it is no longer taken out of the cache — which used to close offline deployment to it, the store being the only source there and an exempted VM having none. It has no per-file trust anchor and /etc is generated read-only, while a declaration would come too late, the first rebuild being the first download. The authority is therefore POINTED AT, consumer by consumer, through the environment — and the environment is lost at three boundaries the four imperative families never meet. nix-daemon is socket-activated and sees no session: a drop-in under /run/systemd/system, a tmpfs writable where /etc is not, reaches it. sudo wipes it, and root's nix talks straight to the local store rather than the daemon, downloading on its own: `Defaults env_keep` carries the variables across, visudo being reached through the system profile since cloud-init's PATH there holds no sudo. And the ssh session opens one second before cloud-init writes the bundle, so the exports live inside the wait for it rather than at the head of the command. The bundle concatenates the system authorities with the cache's, giving the cache's alone would stop trusting everything else. Checked on a fresh VM with upstream CUT: nix-shell realises from the store; and with the cache intercepting, a complete install ends with no certificate refusal
|
|
||||||
- Odoo answers from outside a NixOS VM. It listened on 0.0.0.0:8069 and replied locally, but NixOS enables a firewall by default where none of the four other cloud images does: the host received nothing — not a refusal, silence until the timeout — and the monitor declared Odoo absent on a machine where it was running. Measured from the host: 000 after 12 s, then 303 in 9 ms
|
|
||||||
- ERPLibre runs as a service on NixOS. The install ended by writing a unit into `/etc/systemd/system`, generated from the store and mounted read-only: it returned 1 at its last step, after the clone, the venv and an Odoo start had all succeeded. The unit is now declared by the module; its interpreter comes from the store, `/bin` being an envfs FUSE mount that systemd does not see when it resolves the executable; and its PATH carries bash, whose absence stopped `run.sh` before Odoo
|
|
||||||
- What a declarative system must declare, and the four others receive free from their cloud image: xmlsec1, without which Odoo refuses to install auth_saml, a module of the addons path; parallel and shfmt, called by bare name; growpart, absent from the whole system while the disk grow is written « … || true » and returned 0 without growing anything; and the guest agent, which came from the image rather than the repository, its unit PATH lacking findmnt so that guest-exec died with 127 on its first line
|
|
||||||
- The connection guide is displayed on NixOS. Deployment writes `/etc/motd` everywhere and relies on pam_motd to show it — true of the four cloud images, false here, where sshd reports « printmotd no » and the PAM stack holds no pam_motd: the guide was written, complete, and nobody read it. It also gains a NixOS block naming the trap it exists for — `/etc/nixos/erplibre.nix` is rewritten by `make install_os`, and declarations added there vanish without a word
|
|
||||||
- « make db_drop_all » no longer announces databases as dropped that were not. It built a parallel command, discarded its exit status and printed the list; the case is reachable as soon as parallel is missing from the PATH, and the operator moves on believing their databases are gone
|
|
||||||
- A download cache mirror refused for lack of space names its threshold and its measurement. It echoed a field every ordinary caller leaves at zero — « less than 0 B free on disk » announces no threshold and does not say what was measured
|
|
||||||
- A downloaded image is checked against the sum its publisher ships, for every distribution that publishes one and WITHOUT asking. The check existed behind a flag and for Ubuntu only, so the other images arrived with nothing looking at them. Six now enter, read off the repositories rather than guessed: Debian publishes sha512 where everything else is sha256, the RHEL families name the file « CHECKSUM », Rocky writes the BSD form, and Arch and openSUSE ship a sum per image. An unreachable sums file no longer stops a deployment — that is an availability failure — while a mismatch still stops everything and removes the image
|
|
||||||
- The locale a deployed VM is given applies on NixOS. cloud-init applies it through locale-gen and update-locale, absent there, and the VM kept the distribution's default — « fr_CA.UTF-8 » asked for, « en_US.UTF-8 » obtained. Both it and the timezone are declared by the module now, and neither is imposed on a NixOS one already had
|
|
||||||
- The connection guide fits an 80-column terminal even on a VM carrying its tools and a desktop, where it rendered 103 columns wide: the frame overflowed, the terminal wrapped wherever it liked, and the two-column alignment — the only thing making the guide readable at a glance — was lost. A gloss now wraps under its column, and moves below its command when that command leaves no room. The layout carries the rule, not the length of the texts, which would have held only until the next tool
|
|
||||||
- A VM deployed offline gets its cache variables before its installation starts. « cloud-init status --wait » returns as soon as cloud-init declares itself in error — an accessory module suffices — while its final stage is still writing the authority, `/etc/environment` and the sudoers file; a session opened in that second lived without them, and an install run by sudo then rejected the cache certificate. The deployment now waits for the cloud-final unit, and only its « activating » state, that unit being a oneshot that stays active once finished
|
|
||||||
- Repository metadata an RPM distribution names after the hash of its content is served from disk like a package, and a range request on a file the cache does not hold fetches the whole file in the background, once per key. Such an index of tens of megabytes was taken whole on every install — about 110 MB per VM on a RHEL family system — and dnf, which fetches its zchunk metadata by ranges, got a « 504 » with upstream cut, no range being stored
|
|
||||||
- A VM deployed offline no longer waits for a time synchronisation that cannot come. An image that enables `systemd-time-wait-sync` holds `time-sync.target` until the first NTP answer, and cloud-init's final stage is ordered after it: with no reachable time server, that stage never ran, nor the ssh host keys it generates, and the VM reached its login prompt without ever answering ssh
|
|
||||||
- The download cache no longer re-fetches a package it already holds because a mirror files it under another path. A mirror prefixes the path as it pleases — « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » — and the whole path gave two keys for the same bytes: over a log of 7099 delivered names, 1124 lived under several paths and 3.18 GiB went back upstream for nothing. Only the last six segments count now, empty ones falling with them. Six is the smallest collision-free bound: a Debian path carries exactly six, so five would serve Ubuntu's package for Debian's, which bears the same name for other bytes. Pacman packages stop at four, their paths being shorter than the common bound, which drops the mirror's prefix and keeps the repository name. A store filled before this change is brought over by `--recle`
|
|
||||||
- A kept image that has gone stale is fetched once more instead of ending the deployment. A distribution's « latest » directory moves with each point release and the published sum stops describing the image on disk, with no byte corrupted: the check deleted it and exited, losing a whole campaign — three VMs — to a staleness one download repairs. A second mismatch is on freshly downloaded bytes, stops everything and deletes the image; called with no mirror list, and for unreachable sums, nothing changes
|
|
||||||
- `long_test/qemu_cache.py` no longer fails a campaign where the cache served everything. A URL counts as « already seen » only if the first VM obtained its bytes: one package lives under two paths depending on the mirror, and the first VM can get a « 504 » on one — upstream judged mute — then be served from disk by the other, so nothing was stored under the first path and the second VM's honest download was counted a fault. A missing status counts as delivered, older logs not always writing it, and the new-files line names both of its causes instead of blaming Arch whatever the system measured
|
|
||||||
- An interrupted image download resumes on the same mirror, three attempts, by a `Range` asking for the rest, instead of throwing away what it received: a VM image weighs half a gigabyte, and a truncated `.part` used to send the reader back to a hand-typed `curl -C -`. A server ignoring the Range returns the whole file, and the transfer restarts from zero rather than doubling the bytes already there
|
|
||||||
- The cache's certificate variables name a bundle only when the file exists. A recent Fedora lacks `/etc/pki/tls/certs/ca-bundle.crt`, and pointing pip at a missing path made it refuse EVERY download, including what has nothing to do with the cache. None found writes no variable, and pip keeps its own certificate set
|
|
||||||
- `make version` names both Pythons, Odoo's and the tooling's. A bare label suggested the repository had one, and the figure shown was not that of the venv the reader works in
|
|
||||||
- `make` no longer launches a `.venv.erplibre` built on another machine, such as a checkout mounted over the network, which died on « No module named 'encodings' ». `install.sh` reads the interpreter's version by running code: `python -V` answers before the standard library loads, so it vouched for an interpreter that could not start. Such a venv is now reported as built elsewhere, with the command that rebuilds it
|
|
||||||
- Dependabot no longer opens pull requests against the frozen requirements of Odoo 12 to 17: its security updates scan `requirement/` as its own directory, which the exclusions did not cover
|
|
||||||
- `poetry_update.py` stops on a missing `pyproject.toml` with the command that creates it, `make switch_odoo_XX` for the active version, and offers to run it then restart when launched from a terminal
|
|
||||||
- `poetry_update.py` runs Poetry in the Odoo venv even from a shell under `.venv.erplibre`, whose Python made it fail on « InvalidCurrentPythonVersionError »
|
|
||||||
- `poetry_update.py` skips requirements and manifests under a `doc`, `docs`, `example` or `examples` directory: an example file declaring a loose `>=` no longer moves a dependency for the whole environment
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- La lecture des baux dnsmasq n'ouvre plus d'invite de mot de passe root : les fichiers sont lus en direct, ce qui suffit sur une installation standard où ils sont en 0644, et `sudo -n` n'est tenté qu'ensuite, qui échoue au lieu de demander. L'affichage d'une liste de VM appelait ce chemin une fois par VM, et l'attente d'une VM toutes les trois secondes pendant dix minutes
|
|
||||||
- `--max_process` repart sur Python 3.10 et plus : `loop=` a quitté `asyncio.wait` en 3.10 et `asyncio.get_event_loop()` lève hors d'une loop en marche depuis 3.14, si bien que le pool n'était même plus instanciable alors que l'aide annonçait toujours l'option
|
|
||||||
- Une invite n'écrit plus son deux-points deux fois — le menu le plus vu du logiciel demandait « Commande :: », et sept invites de déploiement à distance affichaient un deux-points suivi d'un autre
|
|
||||||
- Onze sous-menus laissent désormais leur segment dans le fil d'Ariane, et la télémétrie de navigation cesse de les classer comme des commandes sous leur nom de méthode brut
|
|
||||||
- Les trois lecteurs de `~/.ssh/config` s'accordent sur ce qu'est un nom de machine : un alias déclaré par un `host` en minuscules est vu, une tabulation sépare aussi légalement qu'un espace, et un motif nié `!nom` n'est plus pris pour une machine à joindre
|
|
||||||
- Trois clés de traduction déclarées deux fois ont disparu, et un contrôle refuse la suivante : une clé répétée écrase la précédente en silence, ce qui avait déjà coûté une étiquette de menu
|
|
||||||
- `anonymize.py` s'abstient sur une colonne numérique unique et la NOMME dans le rapport. Un tirage ne garantit rien sur une colonne unique, et un nombre n'a pas l'issue qu'a le texte — coller l'id porte l'unicité pour du texte, mais changerait la grandeur d'un nombre. Deux lignes tirant le même nombre faisaient échouer l'UPDATE et, transaction unique oblige, emportaient TOUTE l'anonymisation. Sans que le rapport la nomme, une colonne identifiante reste en clair sans que rien ne le dise
|
|
||||||
- Restaurer une sauvegarde ne lève plus `AttributeError` avant de lancer la commande : `_monitoring_restore` lisait `self._execute` là où `TODO` pose `self.execute`, et les deux entrées qui y mènent — la sauvegarde locale et la distante — étaient cassées. Un contrôle refuse le suivant : tout `self.X` que `TODO` LIT doit être posé par TODO ou par une de ses bases. Chercher le nom dans tout `script/todo/` ne voyait pas la faute, un autre objet du paquet posant bien `_execute` ; il faut le chercher dans les classes dont TODO HÉRITE
|
|
||||||
- Les scripts Selenium tournent sur un Python bâti sans `tkinter` — tout serveur dépourvu de paquet d'interface graphique. Ce module ne sert qu'au sélecteur de fichier du coffre, il est donc désormais optionnel : sans `tkinter` et sans chemin KDBX configuré, l'ouverture du coffre journalise une erreur et rend la main, au lieu de casser l'import de tous les scripts de pilotage de navigateur
|
|
||||||
- Le mode sombre repart en fenêtre privée sur un Firefox récent. La permission « Exécuter dans les fenêtres privées » est accordée à l'installation de l'extension, par le champ `allowPrivateBrowsing`, au lieu d'être cochée dans l'interface `about:addons` : Firefox refuse la navigation vers `about:addons` depuis le contexte contenu, et le contexte chrome exige `-remote-allow-system-access`, que geckodriver refuse via les capabilities, si bien que les deux voies vers cette case sont fermées. Un geckodriver qui ignore le champ installe sans la permission au lieu d'interrompre la session
|
|
||||||
- L'étiquette « - Default » reparaît aux menus des versions et des environnements : les deux lectures demandaient une clé à majuscule que le fichier des versions n'écrit pas, et une clé absente ne rend rien sans rien dire
|
|
||||||
- Le message « rien en réserve » du cache est inerte pour un interpréteur de commandes, chaque ligne étant un commentaire. Livré à un installateur bâti sur `curl … | bash`, il devenait une cascade de « command not found » qui masquait la cause. Un tel téléchargement demande en outre à curl d'échouer sur une erreur HTTP plutôt que d'exécuter la page d'erreur
|
|
||||||
- L'attente qu'une VM soit prête couvre aussi la pose de l'agent invité. Celle-ci part en unité DÉTACHÉE pour que cloud-init rende la main en quelques secondes, et elle fait un `apt-get update` : « cloud-init status --wait » disait « done » alors que le verrou des paquets était encore tenu, l'étape suivante épuisait ses reprises, puis installait sur un index jamais rafraîchi — « Impossible de trouver le paquet », un message qui accuse le dépôt et non le verrou. Un update qui n'aboutit pas le dit désormais sur le champ
|
|
||||||
- Une installation de bureau n'attend plus des minutes sur le verrou apt : le SERVICE apt-daily est arrêté et non son seul minuteur, un minuteur désactivé n'interrompant pas l'apt-get qu'il a déjà lancé ; et la reprise repasse toutes les deux secondes au lieu de dix, « DPkg::Lock::Timeout » ne couvrant pas ce verrou-là
|
|
||||||
- Les VM Fedora démarrent de nouveau : le micrologiciel charge et démarre leur chargeur, puis se fige sans écrire un octet — pas de console, pas de bail DHCP, une machine « en cours d'exécution » qui ne fait rien. Fedora est amorcée en BIOS hérité, où la même image démarre son noyau ; `--bios` garde le dernier mot
|
|
||||||
- Une VM reçoit un nom d'hôte qu'elle accepte — un souligné, que le nom de domaine libvirt tolère, lui faisait garder le nom générique de son image — et un fuseau que sa distribution connaît, un alias hérité la laissant en UTC
|
|
||||||
- starship s'installe dans une VM : son installateur tourne en root, borné par un délai root, et n'atteint jamais le `sudo -v` que sudo-rs refuse ; son crochet de shell n'écrit plus « command not found », ni ne fait échouer un rc sourcé, quand starship est absent
|
|
||||||
- Chaque outil optionnel dit s'il a été posé, et une extension GNOME dont le téléchargement a échoué n'est plus déclarée indisponible pour ce GNOME
|
|
||||||
- mise, pyenv et les extensions GNOME sont téléchargés, puis exécutés : sans pipefail, `curl | sh` ne pouvait ni signaler un téléchargement raté ni atteindre son repli
|
|
||||||
- L'unité de l'agent invité ne finit plus en échec après une pose réussie
|
|
||||||
- Le cache répond 508 à une requête qui le vise lui-même, au lieu de s'appeler jusqu'à épuiser ses descripteurs
|
|
||||||
- Le diagnostic du cache ne donne plus un service arrêté pour actif
|
|
||||||
- L'attente d'`apt-get update` est bornée par une échéance et non par un nombre d'essais. Un essai échoue en moins d'une seconde sur un verrou tenu, mais dure des minutes quand le cache rend 504 sur chaque index qu'il ne détient pas : soixante essais valaient alors des heures de silence là où cinq minutes étaient promises, et l'installation échouait ensuite sur des dépendances introuvables
|
|
||||||
- Un invité Proxmox est fixé sur le même miroir apt que le reste du parc, écrit par ssh avant tout téléchargement. Le magasin range ses index par HÔTE : une VM restée sur les dépôts par défaut de son image ne retrouvait rien de ce dont le cache avait été rempli — hors ligne, chacun de ces index manquait
|
|
||||||
- L'installateur du cache accepte un pont et un sous-réseau donnés à la main. Il sondait libvirt d'abord et mourait sur « réseau default introuvable » : le contournement annoncé dans son propre en-tête — `EL_BRIDGE` et `EL_SUBNET` — était donc hors d'atteinte. Une machine dont le réseau libvirt n'est pas démarré, ou qui porte son pont autrement, peut désormais poser le cache en le nommant ; et quand la sonde tourne et échoue, elle nomme les deux issues
|
|
||||||
- **Déploiement › Cache QEMU › Installer ou réinstaller** n'annonce plus « installé et démarré » quand l'installateur a échoué. Il n'attrapait que les exceptions : un code de sortie non nul — réseau libvirt absent, compilation qui cède — imprimait la ligne de réussite juste sous l'erreur elle-même, avec le chemin d'une autorité qui n'existe pas
|
|
||||||
- Un invité Proxmox reçoit de nouveau son guide, son fuseau, son miroir apt et l'autorité du cache. Ces quatre gestes passent par ssh et partaient dès qu'une adresse était connue, pendant que cloud-init posait encore les comptes et les clés : ils échouaient ensemble, et la VM naissait en UTC, sans guide, sans autorité et sur les dépôts de son image. Le déploiement attend désormais que la machine réponde en ssh — cinq minutes au plus — et le dit quand elle ne répond jamais, au lieu d'échouer quatre fois de suite
|
|
||||||
- Le cache ne sert plus un index de dépôt plus récent que la signature qui l'annonce. Hors ligne, chaque objet sortait avec sa propre date : un index rafraîchi samedi sous un `InRelease` de vendredi faisait échouer apt sur « File has unexpected size » ou « Hash Sum mismatch », et l'installation s'arrêtait sur des dépendances introuvables — un message qui accuse le dépôt, jamais le cache. La comparaison porte sur le `Last-Modified` de l'amont, jamais sur la date de stockage, renouvelée à chaque service
|
|
||||||
- Le refus d'emporter le cache sur une autre machine nomme le geste qui le lève, et ce geste n'est pas l'entrée 1 — elle pose le cache ICI, si bien que la suivre faisait réinstaller l'hôte qui en avait déjà un pendant que l'arrivée restait sans rien. Trois situations étaient rendues par un seul « pas de cache » : un lien ssh qui n'a jamais exécuté la sonde, une arrivée sans cache, une arrivée dont le cache n'a pas son compte de service. Chacune a désormais son message, et celle du cache absent énumère les gestes à faire SUR l'arrivée, avec les deux issues d'un installateur qui lit le réseau libvirt « default » — lever libvirt, ou nommer le pont, un libvirt arrêté le faisant mourir sur un réseau qui existe. Les gestes nomment aussi la branche à donner à l'arrivée, lue sur cet hôte : l'installateur est un fichier du dépôt, si bien qu'une machine restée sur une autre branche répond « fichier introuvable », ce qui ne ressemble en rien à un cache absent. Un quatrième cas est éprouvé avant qu'un seul octet ne parte : un sudo qui réclame un mot de passe à l'arrivée, auquel aucun terminal ne peut répondre puisque le magasin occupe lui-même l'entrée standard de ssh — le message donne le ticket à y obtenir d'abord. Le privilège n'est demandé qu'UNE fois à l'arrivée, « tar » et « chown » sous une seule invocation. Quand le sudo de là-bas réclame un mot de passe, l'entrée offre deux issues au lieu d'échouer : la ligne sudoers à poser une fois, ou un mode en deux temps — le magasin part dans le compte de l'arrivée, qui n'exige aucun privilège, et une commande affichée l'extrait depuis un terminal de là-bas, où un mot de passe se tape. Ce mode réclame deux fois le magasin à l'arrivée, vérifié avant qu'un octet ne parte, un cache ne contenant que des paquets et des archives git déjà comprimés. Le guide porte la même section
|
|
||||||
- Le formulaire de déploiement Proxmox VE éprouve le cache avant de couper le réseau, comme celui de libvirt le faisait déjà : ce que le magasin n'a pas, aucune VM coupée ne le lira, et l'échec tombait une heure plus tard, à la pose du bureau — un message qui accuse le dépôt, jamais le cache. F5 à nouveau vaut passage outre. Les deux formulaires partagent désormais un seul verdict au lieu de deux copies, qui auraient divergé au premier ajustement
|
|
||||||
- Le cache garde ses index de dépôt quand une liste de miroirs tourne. Un index publié sous l'empreinte de son contenu — « by-hash/SHA256/… » — était rangé sous une clé portant l'hôte : les mêmes octets servis par un second miroir étaient repris à l'amont, et hors ligne ils manquaient tout simplement — l'installation échouait sur des fichiers que le magasin détenait, avec un message qui accuse le dépôt. Un tel objet est désormais rangé sous son seul chemin, son nom ÉTANT la somme de son contenu
|
|
||||||
- Le diagnostic du cache n'annonce plus « aucune règle de détournement n'est posée » quand il n'a simplement pas pu les lire. Sur un hôte dont le sudo réclame un mot de passe, « sudo -n nft » ne rend rien, et ce silence était lu comme une absence de règle — ce qui envoyait réinstaller un cache qui détournait correctement. La lecture porte désormais un troisième état, « impossible de savoir », marqué d'un point et non d'une croix, comme le faisait déjà la lecture de la coupure d'amont
|
|
||||||
- Une coupure levée rend aussitôt leur chance aux amonts. Le service retient un court moment ceux dont la connexion vient d'échouer, pour qu'une installation hors ligne ne paie pas le délai d'établissement des centaines de fois ; rien ne lui disait que la coupure était finie, et les premières requêtes d'après la levée se rabattaient sur le magasin alors que le réseau était déjà revenu. La levée touche désormais un témoin dans le magasin, que cette mémoire consulte — il n'existe aucun canal vers le service en marche
|
|
||||||
- Avant de couper, le formulaire avertit aussi de ce qu'aucun index ne peut révéler : un paquet que le déploiement pose HORS du fil observé — l'agent invité, posé par une unité détachée dont l'échec ne remonte nulle part — et les dépôts git déclarés par les manifestes qui n'ont pas encore de miroir. Un index de suite en réserve suffisait à faire passer le cache pour complet alors qu'aucun octet de ce paquet ne l'avait jamais traversé ; et une négociation git ne se garde jamais, si bien qu'un dépôt sans miroir ne peut tout simplement pas être cloné une fois le réseau coupé
|
|
||||||
- L'avertissement sur les miroirs ne compte que les dépôts de la version d'Odoo déployée. Il additionnait tous les manifestes du dépôt — le déprécié compris — et annonçait 170 miroirs manquants là où un vrai déploiement Odoo 18 en rencontre quatre : une alarme qui sonne pour rien est une alarme qu'on cesse de lire. Le remplissage des miroirs, lui, prend toujours de l'avance pour toutes les versions, ce qui est son rôle
|
|
||||||
- Un hôte banni du déchiffrement sur une rafale d'erreurs de transport retrouve sa chance. Trois poignées de main manquées d'affilée le passaient en tunnel opaque, et un tunnel ne consulte jamais le magasin : un miroir de distribution condamné par quelques enregistrements corrompus renvoyait tout son trafic à l'amont — y compris les centaines d'objets déjà détenus pour lui — jusqu'au redémarrage du service. Une alerte TLS bannit toujours définitivement, le client ayant regardé notre certificat et l'ayant refusé ; mais une coupure répétée n'est qu'un soupçon, et elle se rouvre au bout de dix minutes
|
|
||||||
- **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
|
|
||||||
- `make` cherche bash au lieu de présumer `/bin/bash`. Ce chemin n'existe pas sur NixOS, où le shell vit dans le store, et make s'arrêtait avant d'exécuter la moindre recette — y compris celle qui installe de quoi créer ce chemin. Ailleurs, le shell résolu est celui d'avant
|
|
||||||
- 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
|
|
||||||
- Un invité sans ancre de confiance par fichier est soustrait au cache de téléchargement plutôt qu'intercepté sans elle. Le détournement est transparent et vaut pour tout le pont : une VM à qui l'on ne donne pas l'autorité échoue quand même sur « self-signed certificate in certificate chain » — et sur un système déclaratif, poser l'autorité arrive trop tard, la première reconstruction étant le premier téléchargement. Sur un hôte Proxmox, c'est l'HÔTE qui est excepté : un invité imbriqué sort masqué derrière lui et le pont ne voit jamais sa propre adresse. Mesuré depuis l'invité : code 000 et vérification SSL 19, puis 200 et 0. Sans cela le gestionnaire de paquets se rabattait sur 564 dérivations à construire, dont les sources échouaient pour la même raison
|
|
||||||
- NixOS apprend l'autorité du cache de téléchargement SANS reconstruire, et n'en est donc plus soustrait — ce qui lui fermait le déploiement hors ligne, le magasin étant alors la seule source et une VM exceptée n'ayant plus rien. Il n'a pas d'ancre de confiance par fichier et /etc est généré en lecture seule, quand une déclaration arriverait trop tard, la première reconstruction étant le premier téléchargement. L'autorité y est donc POINTÉE, consommateur par consommateur, par l'environnement — et l'environnement se perd à trois frontières que les quatre familles impératives ne rencontrent jamais. nix-daemon est activé par socket et ne voit aucune session : un fragment sous /run/systemd/system, un tmpfs inscriptible là où /etc ne l'est pas, l'atteint. sudo l'efface, et le nix de root parle droit au magasin local plutôt qu'au démon, téléchargeant lui-même : « Defaults env_keep » fait traverser les variables, visudo étant atteint par le profil du système, le PATH qu'y donne cloud-init ne portant aucun sudo. Et la session ssh s'ouvre une seconde avant que cloud-init n'écrive le faisceau, si bien que les exports vivent DANS l'attente de celui-ci plutôt qu'en tête de commande. Le faisceau concatène les autorités du système et celle du cache, donner la seconde seule ferait cesser d'approuver tout le reste. Vérifié amont COUPÉ sur une VM neuve : nix-shell réalise depuis le magasin ; et cache interceptant, une installation complète se termine sans un refus de certificat
|
|
||||||
- Odoo répond depuis l'extérieur d'une VM NixOS. Il écoutait sur 0.0.0.0:8069 et répondait en local, mais NixOS active un pare-feu par défaut là où aucune des quatre autres images cloud n'en active : l'hôte ne recevait rien — pas un refus, un silence jusqu'au délai — et le suivi déclarait Odoo absent sur une machine où il tournait. Mesuré depuis l'hôte : 000 après 12 s, puis 303 en 9 ms
|
|
||||||
- ERPLibre tourne comme service sur NixOS. L'installation finissait par écrire une unité dans `/etc/systemd/system`, généré depuis le store et monté en lecture seule : elle rendait 1 à sa dernière étape, après que le clone, le venv et un démarrage d'Odoo avaient tous réussi. L'unité est désormais déclarée par le module ; son interpréteur vient du store, `/bin` étant un montage FUSE d'envfs que systemd ne voit pas quand il résout l'exécutable ; et son PATH porte bash, dont l'absence arrêtait `run.sh` avant Odoo
|
|
||||||
- Ce qu'un système déclaratif doit déclarer, et que les quatre autres reçoivent gratuitement de leur image cloud : xmlsec1, sans lequel Odoo refuse d'installer auth_saml, module du chemin des addons ; parallel et shfmt, appelés par leur nom nu ; growpart, absent de tout le système alors que l'agrandissement du disque s'écrit « … || true » et rendait 0 sans rien agrandir ; et l'agent invité, qui venait de l'image et non du dépôt, le PATH de son unité manquant findmnt si bien que guest-exec mourait en 127 dès sa première ligne
|
|
||||||
- Le guide de connexion s'affiche sur NixOS. Le déploiement écrit `/etc/motd` partout et compte sur pam_motd pour le montrer — vrai des quatre images cloud, faux ici, où sshd rend « printmotd no » et où la pile PAM ne contient aucun pam_motd : le guide était écrit, complet, et personne ne le lisait. Il gagne aussi un bloc propre à NixOS, qui nomme le piège pour lequel il existe — `/etc/nixos/erplibre.nix` est réécrit par `make install_os`, et les déclarations qu'on y ajoute disparaissent sans un mot
|
|
||||||
- « make db_drop_all » n'annonce plus détruites des bases qui ne le sont pas. Il composait une commande parallel, jetait son code de retour et imprimait la liste ; le cas s'atteint dès que parallel manque du PATH, et l'opérateur passe à la suite en croyant ses bases parties
|
|
||||||
- Un miroir du cache de téléchargement refusé faute de place nomme son seuil et sa mesure. Il reprenait un champ que tout appelant ordinaire laisse à zéro — « moins de 0 o libres sur le disque » n'annonce aucun seuil et ne dit pas ce qui a été mesuré
|
|
||||||
- Une image téléchargée est vérifiée contre la somme que son éditeur publie, pour toute distribution qui en publie une et SANS le demander. La vérification existait sous un drapeau et pour Ubuntu seulement, si bien que les autres images arrivaient sans que rien ne les regarde. Six y entrent, relevées sur les dépôts plutôt que devinées : Debian publie du sha512 quand tout le reste est en sha256, les familles RHEL nomment le fichier « CHECKSUM », Rocky l'écrit en forme BSD, et Arch comme openSUSE posent une somme par image. Un fichier de sommes injoignable n'arrête plus un déploiement — c'est une panne de disponibilité — quand un écart arrête toujours tout et supprime l'image
|
|
||||||
- La locale qu'une VM déployée reçoit s'applique sur NixOS. cloud-init l'applique par locale-gen et update-locale, absents là-bas, et la VM gardait le défaut de la distribution — « fr_CA.UTF-8 » demandé, « en_US.UTF-8 » obtenu. Elle et le fuseau sont désormais déclarés par le module, et ni l'un ni l'autre n'est imposé à une NixOS qu'on avait déjà
|
|
||||||
- Le guide de connexion tient dans un terminal de 80 colonnes même sur une VM qui porte ses outils et un bureau, là où il en rendait 103 : le cadre débordait, le terminal repliait où il voulait, et l'alignement en deux colonnes — seule chose qui rend le guide lisible d'un coup d'œil — se perdait. La glose se replie désormais sous sa colonne, et passe sous sa commande quand celle-ci ne laisse plus de quoi écrire. C'est la mise en page qui porte la règle, non la longueur des textes, qui n'aurait tenu que jusqu'au prochain outil
|
|
||||||
- Une VM déployée hors ligne reçoit les variables du cache avant que son installation ne commence. « cloud-init status --wait » rend la main dès que cloud-init se déclare en erreur — un module accessoire y suffit — alors que son étape finale écrit encore l'autorité, `/etc/environment` et le fichier sudoers ; une session ouverte dans cette seconde-là vivait sans elles, et une installation lancée par sudo rejetait alors le certificat du cache. Le déploiement attend désormais l'unité cloud-final, et son seul état « activating », ce service étant un oneshot qui reste actif une fois fini
|
|
||||||
- Une métadonnée de dépôt qu'une distribution RPM nomme d'après l'empreinte de son contenu est servie du disque comme un paquet, et une demande de plage sur un fichier que le cache ne détient pas fait prendre le fichier entier en arrière-plan, une fois par clé. Un tel index de dizaines de mégaoctets était repris en entier à chaque installation — environ 110 Mo par VM sur un système de la famille RHEL — et dnf, qui prend ses métadonnées zchunk par plages, recevait un « 504 » l'amont coupé, aucune plage ne se gardant
|
|
||||||
- Une VM déployée hors ligne n'attend plus une synchronisation de l'heure qui ne peut pas venir. Une image qui active `systemd-time-wait-sync` retient `time-sync.target` jusqu'à la première réponse NTP, et l'étape finale de cloud-init est ordonnée après elle : sans serveur de temps joignable, cette étape ne démarrait jamais, ni les clés d'hôte ssh qu'elle génère, et la VM atteignait son invite de connexion sans jamais répondre en ssh
|
|
||||||
- Le cache de téléchargement ne reprend plus à l'amont un paquet qu'il détient déjà parce qu'un miroir le range sous un autre chemin. Un miroir préfixe le chemin à sa guise — « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » — et le chemin entier donnait deux clés pour les mêmes octets : sur un journal de 7099 noms livrés, 1124 vivaient sous plusieurs chemins et 3,18 Gio repartaient à l'amont pour rien. Seuls les six derniers segments comptent désormais, les segments vides tombant avec eux. Six est la plus petite borne sans collision : un chemin Debian en porte exactement six, si bien que cinq servirait le paquet d'Ubuntu pour celui de Debian, qui porte le même nom pour d'autres octets. Les paquets pacman s'arrêtent à quatre, leurs chemins étant plus courts que la borne commune, ce qui retire le préfixe du miroir et garde le nom du dépôt. Un magasin rempli avant ce changement se rattrape par `--recle`
|
|
||||||
- Une image gardée devenue périmée est reprise une fois au lieu d'arrêter le déploiement. Le répertoire « latest » d'une distribution avance à chaque version mineure et la somme publiée cesse de décrire l'image du disque, sans qu'un octet soit corrompu : la vérification la supprimait puis sortait, perdant une campagne entière — trois VM — pour une péremption qu'un seul téléchargement répare. Un second écart porte sur des octets fraîchement téléchargés, arrête tout et supprime l'image ; appelée sans liste de miroirs, et pour des sommes injoignables, rien ne change
|
|
||||||
- `long_test/qemu_cache.py` ne fait plus échouer une campagne où le cache a tout servi. Une URL n'est « déjà vue » que si la première VM en a obtenu les octets : un même paquet vit sous deux chemins selon le miroir, et la première VM peut recevoir « 504 » sur l'un — amont jugé muet — puis être servie du disque par l'autre, si bien que rien n'était rangé sous le premier chemin et que le téléchargement honnête de la seconde y était compté en faute. Un statut absent vaut livré, les journaux d'avant ne l'écrivant pas toujours, et la ligne des fichiers neufs nomme ses deux causes au lieu d'imputer à Arch quelle que soit la distribution mesurée
|
|
||||||
- Un téléchargement d'image coupé se reprend sur le même miroir, trois essais, par un `Range` qui demande la suite, au lieu de jeter ce qu'il a reçu : une image de VM pèse un demi-gigaoctet, et un `.part` tronqué renvoyait le lecteur à un `curl -C -` tapé à la main. Un serveur qui ignore le Range rend le fichier entier, et le transfert repart alors de zéro plutôt que de doubler les octets déjà là
|
|
||||||
- Les variables de certificat du cache ne désignent un faisceau que s'il existe. Une Fedora récente n'a pas `/etc/pki/tls/certs/ca-bundle.crt`, et viser un chemin absent faisait refuser à pip TOUT téléchargement, y compris ce qui n'a rien à voir avec le cache. Aucun trouvé n'écrit aucune variable, et pip garde son propre jeu de certificats
|
|
||||||
- `make version` nomme les deux Python, celui d'Odoo et celui de l'outillage. Un libellé nu laissait croire que le dépôt n'en a qu'un, et le chiffre affiché n'était pas celui du venv dans lequel on travaille
|
|
||||||
- `make` ne lance plus un `.venv.erplibre` bâti sur une autre machine, comme un checkout monté par le réseau, qui mourait sur « No module named 'encodings' ». `install.sh` lit la version de l'interpréteur en exécutant du code : `python -V` répond avant le chargement de la bibliothèque standard, et se portait donc garant d'un interpréteur incapable de démarrer. Un tel venv est désormais signalé comme bâti ailleurs, avec la commande qui le rebâtit
|
|
||||||
- Dependabot n'ouvre plus de demandes de fusion sur les requirements figés d'Odoo 12 à 17 : ses mises à jour de sécurité lisent `requirement/` comme un répertoire à part, que les exclusions ne couvraient pas
|
|
||||||
- `poetry_update.py` s'arrête sur un `pyproject.toml` absent en nommant la commande qui le crée, `make switch_odoo_XX` pour la version active, et propose de la lancer puis de se relancer quand il tourne dans un terminal
|
|
||||||
- `poetry_update.py` fait tourner Poetry dans le venv Odoo même depuis un shell sous `.venv.erplibre`, dont le Python le faisait échouer sur « InvalidCurrentPythonVersionError »
|
|
||||||
- `poetry_update.py` écarte les requirements et manifestes sous un répertoire `doc`, `docs`, `example` ou `examples` : un fichier d'exemple déclarant un `>=` lâche ne déplace plus une dépendance pour tout l'environnement
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Removed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Retiré
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- The `sshconf` dependency, declared and installed everywhere and imported nowhere
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- La dépendance `sshconf`, déclarée et installée partout et importée nulle part
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Security
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Sécurité
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- An API key and a bearer token are redacted too before a command is displayed, logged or reprinted: `OPENAI_API_KEY=` went out in the clear, and a header token escaped by construction, carrying neither an option name nor a variable name
|
|
||||||
- Following a redirect, the cache no longer forwards the client's credentials (Authorization, Cookie, Proxy-Authorization) to another host
|
|
||||||
- Odoo 18 installs `idna` 3.20 instead of the 3.6 its own requirements pin, which is affected by CVE-2024-3651
|
|
||||||
- Odoo 18 installs `requests` 2.32.4 instead of the 2.31.0 its own requirements pin, which is affected by CVE-2024-35195 and CVE-2024-47081
|
|
||||||
- The git mirror of the QEMU cache clones only over `http` and `https`: a client could name an `ssh://` or `git://` repository in its request and make the cache connect, with its service account keys, to a host of its choosing. Repositories fetched over HTTPS are mirrored as before; the binary reports 0.2.17
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Une clé d'API et un jeton Bearer sont caviardés eux aussi avant qu'une commande soit affichée, journalisée ou réimprimée : `OPENAI_API_KEY=` partait en clair, et un jeton d'en-tête échappait par construction, ne portant ni nom d'option ni nom de variable
|
|
||||||
- En suivant une redirection, le cache ne transmet plus les identifiants du client (Authorization, Cookie, Proxy-Authorization) à un autre hôte
|
|
||||||
- Odoo 18 installe `idna` 3.20 au lieu de la 3.6 qu'épinglent ses propres requirements, touchée par CVE-2024-3651
|
|
||||||
- Odoo 18 installe `requests` 2.32.4 au lieu du 2.31.0 qu'épinglent ses propres requirements, touché par CVE-2024-35195 et CVE-2024-47081
|
|
||||||
- Le miroir git du cache QEMU ne clone plus qu'en `http` et `https` : un client pouvait nommer un dépôt `ssh://` ou `git://` dans sa requête et faire se connecter le cache, avec les clés de son compte de service, à un hôte de son choix. Les dépôts servis en HTTPS sont mis en miroir comme avant ; le binaire annonce 0.2.17
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
|
|
||||||
## [1.8.0] - 2026-09-04
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
**Migration notes**
|
|
||||||
|
|
||||||
Recreating the virtual environment, the Python interpreter and the package
|
|
||||||
installer being chosen now. Use the installation guide from tool `make`.
|
|
||||||
Ubuntu 20.04 and 22.04 are no longer supported.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
**Notes de migration**
|
|
||||||
|
|
||||||
Recréer l'environnement virtuel, l'interpréteur Python et l'installateur de
|
|
||||||
paquets se choisissant désormais. Utiliser le guide d'installation depuis
|
|
||||||
l'outil `make`. Ubuntu 20.04 et 22.04 ne sont plus supportés.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Added
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Ajouté
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- Deploy ERPLibre VMs with QEMU/KVM from cloud images: Ubuntu, Debian, Fedora, AlmaLinux, Rocky, openSUSE, Arch, Linux Mint, and Debian on s390x. Hardware, branch and Odoo version are set per machine
|
|
||||||
- Proxmox VE as a deployment target, its installation including the reboot it needs
|
|
||||||
- A QEMU menu: network status and repair, 3D acceleration, a diagnostic report, file recovery from a VM that no longer boots, virt-viewer and a remote desktop tunnel
|
|
||||||
- An install dashboard and Textual forms: deploy, follow, update, restart or delete a VM without leaving the screen
|
|
||||||
- A mobile development VM: PyCharm, Android Studio, an Android emulator and an adb tunnel
|
|
||||||
- A VPN tool, five free technologies from the menu, the secrets in a KeePassXC vault and a diagnosis that names the failing stage
|
|
||||||
- Automated Odoo migration: the tool drives the whole run, goes back to a step, and repairs what a version bump leaves behind
|
|
||||||
- Migration review: a verdict per step, smoke tests on every public URL, and a filestore check
|
|
||||||
- A read-only analysis toolkit for an Odoo database, a backup zip included, with PostgreSQL index advice
|
|
||||||
- Anonymising a production copy without AI, and duplicating a database neutralised for good
|
|
||||||
- Development assistants installed inside a VM, and a Git and Shell menu that installs what a checkout needs
|
|
||||||
- A writing convention for what stays in git, held by a `pre-commit` and a `commit-msg` hook
|
|
||||||
- `long_test/` — tests that create real machines, nested QEMU and Proxmox included, kept out of the unit runner
|
|
||||||
- NTFY, Forgejo, a local git server, e-mail from the CLI, and SSH configuration with recursive ProxyJump
|
|
||||||
- The Python interpreter and the package installer are chosen, through EL_PYTHON_PROVIDER and EL_PIP_PROVIDER
|
|
||||||
- The OCA generative AI policy, Claude Code agents and commands, and the context an assistant is given, shown from the menu
|
|
||||||
- Unit tests with a bilingual test plan, navigation telemetry for TODO, and Odoo 18 reading STL files
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Déployer des VM ERPLibre en QEMU/KVM depuis des images cloud : Ubuntu, Debian, Fedora, AlmaLinux, Rocky, openSUSE, Arch, Linux Mint, et Debian sur s390x. Matériel, branche et version d'Odoo se règlent par machine
|
|
||||||
- Proxmox VE comme cible de déploiement, son installation comprenant le redémarrage qu'elle exige
|
|
||||||
- Un menu QEMU : état et réparation du réseau, accélération 3D, un rapport de diagnostic, récupération de fichiers sur une VM qui ne démarre plus, virt-viewer et un tunnel de bureau distant
|
|
||||||
- Un tableau de bord d'installation et des formulaires Textual : déployer, suivre, mettre à jour, redémarrer ou effacer une VM sans quitter l'écran
|
|
||||||
- Une VM de développement mobile : PyCharm, Android Studio, un émulateur Android et un tunnel adb
|
|
||||||
- Un outil VPN, cinq technologies libres au menu, les secrets dans un coffre KeePassXC et un diagnostic qui nomme l'étage fautif
|
|
||||||
- La migration Odoo automatisée : l'outil mène toute l'exécution, revient à une étape, et répare ce qu'un changement de version laisse derrière lui
|
|
||||||
- La revue de migration : un verdict par étape, des tests de fumée sur chaque URL publique, et un contrôle du filestore
|
|
||||||
- Une trousse d'analyse en lecture seule d'une base Odoo, archive de sauvegarde comprise, avec conseil d'index PostgreSQL
|
|
||||||
- L'anonymisation d'une copie de production sans IA, et la duplication d'une base neutralisée pour de bon
|
|
||||||
- Des assistants de développement posés dans une VM, et un menu Git et Shell qui installe ce qu'un checkout réclame
|
|
||||||
- Une convention d'écriture pour ce qui reste dans git, tenue par un hook `pre-commit` et un hook `commit-msg`
|
|
||||||
- `long_test/` — des tests qui créent de vraies machines, QEMU et Proxmox imbriqués compris, hors du lanceur unitaire
|
|
||||||
- NTFY, Forgejo, un serveur git local, le courriel depuis le CLI, et la configuration SSH avec ProxyJump récursif
|
|
||||||
- L'interpréteur Python et l'installateur de paquets se choisissent, par EL_PYTHON_PROVIDER et EL_PIP_PROVIDER
|
|
||||||
- La politique d'IA générative de l'OCA, les agents et commandes Claude Code, et le contexte donné à un assistant, montré depuis le menu
|
|
||||||
- Des tests unitaires avec un plan de test bilingue, la télémétrie de navigation de TODO, et Odoo 18 qui lit les fichiers STL
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Changed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Modifié
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- todo.py split into nine files, one per subject, with a shared base per form
|
|
||||||
- Every menu entry carries an icon, the menus are grouped into sections, and a countdown prompt gives 15 seconds to decide
|
|
||||||
- Branch, profile, type and timezone are chosen per VM rather than globally
|
|
||||||
- Installation covers Fedora, Debian, Ubuntu, Arch and openSUSE; repository sync and Poetry run in parallel, quiet unless EL_VERBOSE asks
|
|
||||||
- Node.js 22 for Capacitor 8, flanker for Odoo 18, CybroOdoo extras opt-in, and a Poetry dependency declinable per architecture
|
|
||||||
- A VM boots faster and picks the fastest reachable mirror, Canadian pacman mirrors coming first on Arch
|
|
||||||
- Staging names the files, never `git add -A`
|
|
||||||
- Enter targets the highest supported Odoo version, and a VM name drops the `latest` segment
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- todo.py éclaté en neuf fichiers, un par sujet, avec un socle commun par formulaire
|
|
||||||
- Chaque entrée de menu porte une icône, les menus sont regroupés en sections, et une invite à compte à rebours laisse 15 secondes pour décider
|
|
||||||
- La branche, le profil, le type et le fuseau se choisissent par VM plutôt que globalement
|
|
||||||
- L'installation couvre Fedora, Debian, Ubuntu, Arch et openSUSE ; la synchronisation des dépôts et Poetry tournent en parallèle, silencieux sauf si EL_VERBOSE le demande
|
|
||||||
- Node.js 22 pour Capacitor 8, flanker pour Odoo 18, les extras CybroOdoo optionnels, et une dépendance Poetry déclinable par architecture
|
|
||||||
- Une VM démarre plus vite et prend le miroir joignable le plus rapide, les miroirs pacman canadiens passant en tête sur Arch
|
|
||||||
- L'indexation nomme les fichiers, jamais `git add -A`
|
|
||||||
- Entrée cible la version d'Odoo la plus élevée supportée, et un nom de VM perd le segment `latest`
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Fixed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Corrigé
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- The libvirt network no longer counts as its own collision, no longer leaves a host without network at the next boot, and its state is read in English whatever the locale
|
|
||||||
- QEMU deployment: sudo says why it needs a password, the `libvirt` group replaces it where it suffices, no host reboots unasked, and an orphan disk no longer blocks a creation
|
|
||||||
- Migration: the database drop, the account.root view Odoo 17 leaves behind, the pricelists a repair invented, and the assumptions the 13-to-18 run rested on
|
|
||||||
- Anonymisation respects what a value means, and no longer breaks past the 131 072-byte limit of a single argument
|
|
||||||
- Installation on Debian 13, Fedora, Ubuntu 26.04 and s390x: apt locks, missing compilers and headers, too little memory, and what a recent SWIG or PROJ needs
|
|
||||||
- Secrets: the KeePassXC vault opens on a server without tkinter, the forgejo installer stops echoing the password it set, and db_restore validates the master one
|
|
||||||
- A question is seen before it is answered, and one faulty repository no longer takes a whole batch down
|
|
||||||
- Three screens that fell over, a shrink that would have filled the disk, and a monitor that binned a VM before being sure
|
|
||||||
- The unit runner globs the whole directory, where it ran 1131 tests of 3703
|
|
||||||
- Proxmox no longer aims at the host instead of the VM
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Le réseau libvirt ne compte plus comme sa propre collision, ne laisse plus un hôte sans réseau au démarrage suivant, et son état est lu en anglais quelle que soit la locale
|
|
||||||
- Le déploiement QEMU : sudo dit pourquoi il faut un mot de passe, le groupe `libvirt` le remplace là où il suffit, aucun hôte ne redémarre sans qu'on le demande, et un disque orphelin ne bloque plus
|
|
||||||
- La migration : l'effacement de base, la vue account.root que laisse Odoo 17, les listes de prix qu'une réparation inventait, et les suppositions sur lesquelles tenait le passage de 13 à 18
|
|
||||||
- L'anonymisation respecte ce qu'une valeur signifie, et ne casse plus au-delà de la limite de 131 072 octets d'un seul argument
|
|
||||||
- L'installation sur Debian 13, Fedora, Ubuntu 26.04 et s390x : verrous apt, compilateurs et en-têtes manquants, mémoire trop courte, et ce qu'exigent un SWIG ou un PROJ récents
|
|
||||||
- Les secrets : le coffre KeePassXC s'ouvre sur un serveur sans tkinter, l'installateur forgejo cesse de réafficher le mot de passe qu'il a posé, et db_restore valide celui du maître
|
|
||||||
- Une question se voit avant qu'on y réponde, et un dépôt fautif n'emporte plus tout un lot
|
|
||||||
- Trois écrans qui tombaient, une réduction qui aurait rempli le disque, et un suivi qui mettait une VM à la poubelle avant d'en être sûr
|
|
||||||
- Le lanceur unitaire balaie tout le répertoire, là où il exécutait 1131 tests sur 3703
|
|
||||||
- Proxmox ne vise plus l'hôte au lieu de la VM
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Removed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Retiré
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- Ubuntu 20.04 and 22.04 support, on every architecture
|
|
||||||
- The residue check that called a language broken when its `active` is NULL
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures
|
|
||||||
- Le contrôle de résidus qui jugeait cassée une langue dont `active` est NULL
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Security
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Sécurité
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- Passwords and tokens are redacted before a command is displayed, logged or reprinted
|
|
||||||
- The Odoo master password and the KeePass one leave the command line, an environment variable carrying them instead
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Les mots de passe et jetons sont caviardés avant l'affichage, la journalisation ou le réaffichage d'une commande
|
|
||||||
- Le mot de passe maître d'Odoo et celui de KeePass quittent la ligne de commande, une variable d'environnement les portant à la place
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
|
|
||||||
## [1.7.0] - 2026-03-11
|
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
**Migration notes**
|
**Migration notes**
|
||||||
|
|
||||||
|
|
@ -561,47 +41,41 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
||||||
## Ajouté
|
## Ajouté
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
- Odoo 12.0 to 18.0 in a single workspace, switched without reinstalling: the manifests, the configuration and the addons paths follow the version named in `.odoo-version`
|
- Support Odoo migration database and module with TODO
|
||||||
- ERPLibre's Python separated from Odoo's — `.venv.erplibre` carries the repository's own tools, `.venv.odoo<version>` the server — so a tool of the repository no longer depends on the interpreter a given Odoo version imposes
|
- Support multi version odoo switch on same workspace
|
||||||
- Auto-installation driven from TODO: the menu lays down the environment it needs, Poetry, the Google Repo manifests and the addons included, rather than printing a command to retype
|
- Script for hardening the installation
|
||||||
- Migration of an Odoo database and its modules from TODO, `--neutralize` included, with the repair of the mail module that a move from PostgreSQL 17 to 18 leaves behind
|
- Support Odoo versions 12.0 to 18.0
|
||||||
- A hardening script for the installation
|
- Separate ERPLibre python installation from Odoo python with .venv.erplibre and .venv.odoo18
|
||||||
- The ERPLibre Home mobile application: TODO compiles it, deploys it, renames the software and changes its menu image
|
- Implement auto-installation with TODO.py
|
||||||
- The RobotLibre code generator, with the queue_job channels its configuration needs
|
- TODO show documentation, download database, help with code formatting
|
||||||
- ERPLibre DevOps, and the automation procedure it describes
|
- Performance script to mesure request per second
|
||||||
- The Selenium grid from `selenium_lib.py`: a KeePass vault opened for the run, file downloads, dark mode, video recording and a scenario library
|
- Support Mainframe architecture 390x
|
||||||
- A performance script measuring the requests per second a website answers
|
- Deployment with Cloudflare and Nginx
|
||||||
- Deployment: Cloudflare DNS, nginx with a non-interactive certbot, Apache templates matching the nginx ones, and a systemd unit whose working directory is configurable
|
- Support Apache configuration like Nginx
|
||||||
- The s390x mainframe architecture
|
- Support RobotLibre code generator
|
||||||
- Addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor, and the design-themes repository
|
- Support ERPLibre DevOps, automation procedure about DevOps
|
||||||
- Database backup and erase commands, and a clearer restore naming
|
- ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it
|
||||||
- A security check of the Python environment, from the menu
|
- Support Selenium grid from selenium_lib.py
|
||||||
- TODO shows the documentation, downloads a database and helps with code formatting
|
- Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor
|
||||||
- Killing an Odoo process by the port it holds, from the menu
|
|
||||||
- CLAUDE.md and the agent information document, so an assistant reads the repository's conventions instead of guessing them
|
|
||||||
- A FAQ entry on wkhtmltopdf for recent distributions
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
|
|
||||||
- Odoo 12.0 à 18.0 dans un même espace de travail, changés sans réinstaller : les manifestes, la configuration et les chemins d'addons suivent la version nommée dans `.odoo-version`
|
- Support de la migration de base de données et de modules Odoo avec TODO
|
||||||
- Le Python d'ERPLibre séparé de celui d'Odoo — `.venv.erplibre` porte les outils du dépôt, `.venv.odoo<version>` le serveur — si bien qu'un outil du dépôt ne dépend plus de l'interpréteur qu'impose une version d'Odoo
|
- Support du changement multi-version Odoo sur le même espace de travail
|
||||||
- L'auto-installation pilotée depuis TODO : le menu pose l'environnement dont il a besoin, Poetry, les manifestes Google Repo et les addons compris, au lieu d'afficher une commande à retaper
|
- Script pour le renforcement de la sécurité de l'installation
|
||||||
- La migration d'une base Odoo et de ses modules depuis TODO, `--neutralize` compris, avec la réparation du module mail que laisse un passage de PostgreSQL 17 à 18
|
- Support des versions Odoo 12.0 à 18.0
|
||||||
- Un script de renforcement de la sécurité de l'installation
|
- Séparation de l'installation Python ERPLibre de celle d'Odoo avec .venv.erplibre et .venv.odoo18
|
||||||
- L'application mobile ERPLibre Home : TODO la compile, la déploie, renomme le logiciel et change son image de menu
|
- Implémentation de l'auto-installation avec TODO.py
|
||||||
- Le générateur de code RobotLibre, avec les canaux queue_job que sa configuration réclame
|
- TODO affiche la documentation, télécharge la base de données, aide au formatage du code
|
||||||
- ERPLibre DevOps, et la procédure d'automatisation qu'il décrit
|
- Script de performance pour mesurer les requêtes par seconde
|
||||||
- La grille Selenium depuis `selenium_lib.py` : un coffre KeePass ouvert pour l'exécution, le téléchargement de fichiers, le mode sombre, l'enregistrement vidéo et une bibliothèque de scénarios
|
- Support de l'architecture Mainframe 390x
|
||||||
- Un script de performance qui mesure les requêtes par seconde qu'un site répond
|
- Déploiement avec Cloudflare et Nginx
|
||||||
- Le déploiement : DNS Cloudflare, nginx avec un certbot non interactif, gabarits Apache alignés sur ceux de nginx, et une unité systemd dont le répertoire de travail se configure
|
- Support de la configuration Apache comme Nginx
|
||||||
- L'architecture mainframe s390x
|
- Support du générateur de code RobotLibre
|
||||||
- Les addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor, et le dépôt design-themes
|
- Support d'ERPLibre DevOps, procédure d'automatisation DevOps
|
||||||
- Des commandes de sauvegarde et d'effacement de base, et un nommage plus clair à la restauration
|
- Application mobile ERPLibre Home, utiliser TODO pour compiler, déployer et personnaliser
|
||||||
- Une vérification de sécurité de l'environnement Python, depuis le menu
|
- Support de la grille Selenium depuis selenium_lib.py
|
||||||
- TODO affiche la documentation, télécharge une base et aide au formatage du code
|
- Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor
|
||||||
- Tuer un processus Odoo par le port qu'il occupe, depuis le menu
|
|
||||||
- CLAUDE.md et le document d'information des agents, pour qu'un assistant lise les conventions du dépôt au lieu de les deviner
|
|
||||||
- Une entrée de FAQ sur wkhtmltopdf pour les distributions récentes
|
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
## Changed
|
## Changed
|
||||||
|
|
@ -609,65 +83,15 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
||||||
## Modifié
|
## Modifié
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
- Odoo 18.0 becomes the default version of a checkout
|
- Docker support postgresql 18
|
||||||
- Docker moves to PostgreSQL 18, with the matching client
|
- Format script search diff file into each repository
|
||||||
- The documentation is bilingual, generated by mmg from the `.base.md` sources: a `.md` or `.fr.md` edited directly is lost at the next generation
|
- Support neutralize database from Odoo
|
||||||
- The TODO menus are grouped into sections, and the English text serves as the i18n key rather than a code of its own
|
|
||||||
- The formatting script looks for the changed files in every repository, hidden addons included, and skips a repository that is not installed
|
|
||||||
- Odoo runs on a custom database, and the menu configures queue_job as well as the SSH forwarding a remote instance needs
|
|
||||||
- Killing a process by port asks before acting, through an interactive menu
|
|
||||||
- Neutralising a database goes through Odoo's own `--neutralize`
|
|
||||||
- LinuxMint 22.3, Ubuntu 25.10, and macOS without Python 3.7
|
|
||||||
- Odoo 18 dependencies: tldextract, PyYAML, pdfminer.six, and cryptography at its latest version
|
|
||||||
- A make target runs the unit tests
|
|
||||||
- The Makefile is split: its commands live in `conf/`, and `Common.Makefile` extends it for a project of one's own
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
|
|
||||||
- Odoo 18.0 devient la version par défaut d'un checkout
|
- Support Docker postgresql 18
|
||||||
- Docker passe à PostgreSQL 18, avec le client correspondant
|
- Script de formatage recherche les fichiers diff dans chaque dépôt
|
||||||
- La documentation est bilingue, générée par mmg depuis les sources `.base.md` : un `.md` ou `.fr.md` modifié directement est perdu à la prochaine génération
|
- Support de la neutralisation de base de données depuis Odoo
|
||||||
- Les menus TODO sont regroupés en sections, et le texte anglais sert de clé i18n plutôt qu'un code à part
|
|
||||||
- Le script de formatage cherche les fichiers modifiés dans chaque dépôt, addons cachés compris, et saute un dépôt qui n'est pas installé
|
|
||||||
- Odoo tourne sur une base personnalisée, et le menu configure queue_job comme la redirection SSH qu'une instance distante réclame
|
|
||||||
- Tuer un processus par son port demande confirmation, par un menu interactif
|
|
||||||
- La neutralisation d'une base passe par le `--neutralize` d'Odoo
|
|
||||||
- LinuxMint 22.3, Ubuntu 25.10, et macOS sans Python 3.7
|
|
||||||
- Dépendances Odoo 18 : tldextract, PyYAML, pdfminer.six, et cryptography à sa dernière version
|
|
||||||
- Une cible make lance les tests unitaires
|
|
||||||
- Le Makefile est éclaté : ses commandes vivent dans `conf/`, et `Common.Makefile` l'étend pour un projet à soi
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Fixed
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Corrigé
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
- The documentation accents, and the markdown generation running in parallel
|
|
||||||
- `git_tool` returns nothing instead of raising where `.git` is absent
|
|
||||||
- `poetry iscompatible` no longer crashes on a version carrying a letter, an alpha or a release candidate
|
|
||||||
- pymssql compiles again, and the Odoo 18 requirements leave pyssql out of a production install
|
|
||||||
- wkhtmltopdf is no longer offered where it does not exist: no package is published for s390x on Ubuntu 25.10
|
|
||||||
- Selenium: the snap Firefox path, a 60-second timeout when reaching for an element, execution in a private window, and a login that waits for Odoo 18
|
|
||||||
- The formatting script ignores the files and directories it must not touch
|
|
||||||
- `db_drop_all` runs its shell command, and the backup processing keeps the permissions of what it writes
|
|
||||||
- TODO: the first import, the regeneration of `.repo/local_manifests`, the database open dialog, and a missing Odoo version reported instead of a crash
|
|
||||||
- The code generator: creating a project, extracting a class carrying a selection, and reading a model through the Python 3.11 `ast` module rather than astor
|
|
||||||
- Docker: the duplicated Odoo 18 build target, and the compose file pinned to an image that works
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
- Les accents de la documentation, et la génération markdown qui tourne en parallèle
|
|
||||||
- `git_tool` rend vide au lieu de lever là où `.git` est absent
|
|
||||||
- `poetry iscompatible` ne casse plus sur une version portant une lettre, une alpha ou une candidate
|
|
||||||
- pymssql se compile de nouveau, et les dépendances Odoo 18 laissent pyssql hors d'une installation de production
|
|
||||||
- wkhtmltopdf n'est plus proposé là où il n'existe pas : aucun paquet n'est publié pour s390x sur Ubuntu 25.10
|
|
||||||
- Selenium : le chemin du Firefox snap, un délai de 60 secondes pour atteindre un élément, l'exécution en fenêtre privée, et une connexion qui attend Odoo 18
|
|
||||||
- Le script de formatage ignore les fichiers et répertoires qu'il ne doit pas toucher
|
|
||||||
- `db_drop_all` exécute sa commande shell, et le traitement des sauvegardes garde les permissions de ce qu'il écrit
|
|
||||||
- TODO : le premier import, la régénération de `.repo/local_manifests`, le dialogue d'ouverture de base, et une version d'Odoo manquante signalée au lieu d'un plantage
|
|
||||||
- Le générateur de code : la création d'un projet, l'extraction d'une classe portant une sélection, et la lecture d'un modèle par le module `ast` de Python 3.11 plutôt que par astor
|
|
||||||
- Docker : la cible de compilation Odoo 18 en double, et le fichier compose épinglé sur une image qui fonctionne
|
|
||||||
|
|
||||||
<!-- [common] -->
|
<!-- [common] -->
|
||||||
|
|
||||||
|
|
@ -1692,11 +1116,7 @@ Parce que le dépôt d'addons a changé, le fichier de configuration doit être
|
||||||
|
|
||||||
<!-- [common] -->
|
<!-- [common] -->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/ERPLibre/ERPLibre/compare/v1.8.0...HEAD
|
[Unreleased]: https://github.com/ERPLibre/ERPLibre/compare/v1.6.0...HEAD
|
||||||
|
|
||||||
[1.8.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.7.0...v1.8.0
|
|
||||||
|
|
||||||
[1.7.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.6.0...v1.7.0
|
|
||||||
|
|
||||||
[1.6.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.5.0...v1.6.0
|
[1.6.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.5.0...v1.6.0
|
||||||
|
|
||||||
|
|
|
||||||
311
CHANGELOG.fr.md
311
CHANGELOG.fr.md
|
|
@ -9,301 +9,34 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## Ajouté
|
|
||||||
|
|
||||||
- Des préréglages de site pour le VPN : un `.json` porte la passerelle d'un site, son protocole et son groupe de connexion, et ni identifiant ni secret, si bien qu'il peut circuler. Lus depuis `conf/vpn_presets/`, puis depuis un `private/vpn/presets/` ignoré par git, puis depuis tout répertoire listé dans `vpn_preset_paths` ; sur un même identifiant le plus tardif gagne, et un site corrige un gabarit livré sans toucher de fichier suivi
|
|
||||||
- Importer un profil Cisco AnyConnect `.xml` depuis le menu, en parcourant les répertoires du client ou en tapant le chemin, et en tirer un préréglage de ses balises `HostName`, `HostAddress` et `UserGroup`
|
|
||||||
- OpenConnect distingue les deux mécanismes qui désignent un service sur un même concentrateur : le groupe de connexion dans l'URL et la valeur choisie dans un menu déroulant. Les confondre donne le formulaire d'un autre service, et des identifiants justes sont refusés sans que rien ne nomme le groupe
|
|
||||||
- Joindre une passerelle qui exige un navigateur intégré pour le SAML, ce qui arrête OpenConnect sur « No SSO handler » : l'étape web est déléguée à un greffon `openconnect-sso` que l'installateur propose de poser, et le tunnel est ensuite monté par le pilote lui-même, si bien que le nom d'interface, les fichiers d'état et le diagnostic restent au profil
|
|
||||||
- Déclarer un concentrateur qui ne compare que les premiers caractères d'un mot de passe : il est annoncé avant le dépôt du secret, et rien n'est jamais tronqué
|
|
||||||
- L'installateur VPN cherche `vpnc-script` — un fichier, et non un binaire du `PATH` — et nomme le paquet à poser par famille de distribution, au lieu de laisser le tunnel échouer sur une interface qui n'apparaît jamais
|
|
||||||
- Une commande lancée par l'exécuteur VPN reçoit `/dev/null` sur son entrée standard, si bien qu'une commande à sortie capturée ne peut plus être arrêtée par un SIGTTOU et figer le gestionnaire de paquets de la machine
|
|
||||||
- La liste des profils VPN marque ceux qui portent un tunnel vivant, si bien que connecter un profil déjà monté demande confirmation et que déconnecter un profil déjà tombé le dit au lieu de ressembler à une erreur. Un profil est jugé sur son interface et non sur le fichier d'état qu'un tunnel tué sans `down` laisse derrière lui — un état laissé était annoncé monté sur l'écran même qui déclarait le processus mort
|
|
||||||
- `Assistant › LLM` — interroger un serveur de modèle sur plusieurs tours, local ou distant. L'historique vit en mémoire et meurt avec le menu, `/save` étant le seul moyen d'en garder une trace ; toute commande porte une barre oblique initiale, donc une question collée sur plusieurs lignes reste un seul tour. Une destination tierce doit être retapée avant le premier envoi
|
|
||||||
- Reconnaissance du serveur sur onze ports et douze familles, l'identité étant lue dans le CORPS de la réponse et jamais dans le port : un port héberge jusqu'à trois produits, et une famille réémet l'API native d'une autre en entier
|
|
||||||
- Recherche d'un serveur depuis six sources — la boucle locale, les domaines QEMU de la machine, les hôtes de `~/.ssh/config`, une adresse ou un réseau saisi, et les réseaux lus en SSH sur une autre machine. Plus large qu'un `/24` est refusé avant énumération, et deux préférences bornent le balayage : `assistant_sweep_workers`, `assistant_sweep_timeout`
|
|
||||||
- Un catalogue d'outils gpt dans `script/todo/assistant/gpt/` : un fichier Markdown par outil, dont les exigences déclarées sont confrontées à ce que le serveur annonce. L'inconnu ne grise jamais un outil — seule une exigence contredite par un champ réellement lu le fait, avec le chiffre qui la refuse
|
|
||||||
- Un contexte LECTURE SEULE déclaré par outil, fichiers et commandes autorisées, montré et confirmé avant le premier envoi, borné en taille et en durée, et balayé à la recherche de données identifiantes. La limite du balayage est dite : il voit les adresses, les courriels et les chemins de compte, pas les noms
|
|
||||||
- `Exécution › GPT code › Claude Code` — lister les sessions de la machine, en interroger une avec des outils en lecture seule, ou la reprendre dans son propre terminal. Une copie est branchée par défaut, deux écritures sur une même session perdant une branche
|
|
||||||
- `check_comment_hygiene.py` signale un nom de machine pleinement qualifié dans un commentaire, en signal à relire et jamais en trouvaille : un nom d'hôte NU ne se distingue mécaniquement pas d'un mot ordinaire, donc l'absence de signal ne prouve rien sur les noms. L'en-tête de copyright, les domaines de la RFC 2606 et toute adresse portée par une URL sont laissés tranquilles
|
|
||||||
- Transform data, une entrée de menu qui ouvre un fichier externe — Excel, Access, CSV, XML ou JSON —, rapporte ce qu'il porte, puis en tire une copie anonymisée. L'original n'est jamais touché. Les nombres sont tirés dans l'étendue mesurée de leur propre colonne plutôt que dans un intervalle fixe de 0 à 1000, qui rend un taux de TVA à 743 et une année à 12 ; ils sont tirés sans remise, cent clés distinctes dans une étendue de cent ne rendant sinon que soixante-six sorties, et une fixture qui ne se réimporte plus. Le texte prend un mot français dans un dictionnaire local de 1366, attribué par un compteur et non par un tirage, et une table de correspondance portable donne au même client le même mot sur tout un lot
|
|
||||||
- Onze étiquettes structurelles sont laissées intactes sur leur NOM — `id`, `create_uid`, `sequence`, `active` et les autres, plus tout ce qui finit en `/id` ou `/.id` —, un jeu de test devant continuer de fonctionner. Toute AUTRE colonne est jugée sur son CONTENU mesuré, jamais sur la seule foi de son nom. Dans un export import-compatible, `partner_id` porte le nom affiché de la relation et `display_name` est la donnée elle-même : décider sur l'étiquette recopiait textuellement les colonnes les plus identifiantes du fichier en les annonçant comme protégées
|
|
||||||
- L'anonymiseur relit les octets qu'il vient d'écrire et refuse la copie dès qu'une valeur annoncée comme remplacée y subsiste. Un classeur porte de la donnée dans une douzaine d'endroits qui ne sont pas des cellules : un cache de tableau croisé et un lien externe en portent chacun une copie entière, un graphique met ses catégories et ses titres d'axes en cache, un format de nombre personnalisé porte un libellé, un commentaire porte son auteur, un hyperlien de cellule porte un chemin. Le filet ne dépend d'aucune liste de ces endroits, si bien qu'un endroit que personne n'a pensé à nettoyer produit un refus et non un silence — et ce qui reste par décision, une ligne d'en-tête ou une colonne laissée intacte, est nommé à l'écran avant que rien ne soit écrit
|
|
||||||
- Transform data anonymise aussi une base Odoo, l'entrée demandant la SOURCE plutôt que le format : un fichier externe, une base vivante, ou un zip de sauvegarde pris dans `image_db/`. Un zip n'est jamais modifié — il est restauré dans une base, anonymisé là, puis vidé dans un zip NEUF par la sauvegarde d'Odoo lui-même, si bien que le dump, le filestore et le manifeste sont écrits par le logiciel qui les relira. Une base vivante est modifiée ELLE-MÊME, ce qui est dit avant le travail et non après, et rien n'est tiré lorsque l'écriture n'a pas eu lieu : un renoncement rendait sinon un zip de la base intacte, annoncé comme anonymisé. Un registre inscrit les bases que l'entrée a produites, le serveur ne le disant pas
|
|
||||||
- Un nombre anonymisé peut garder son nombre de chiffres, sur l'anonymiseur de fichiers comme sur `anonymize.py` (`--keep-digits`) : 8839 tire alors dans 1000..9999, si bien que la copie garde des colonnes de la même largeur, ce qu'un export relu à l'œil ou importé dans un champ borné demande. Chaque valeur garde SA largeur et non celle de sa colonne — 7 reste à un chiffre là où 12 en garde deux — et sous l'unité la règle ne s'applique pas, 0,15 n'ayant pas de chiffre avant la virgule. L'option échange une garantie contre une autre : l'étendue mesurée ne borne plus rien, donc un taux ou une année peut sortir de sa plage, et l'aperçu le DIT au lieu de le laisser découvrir à la réimportation. C'est l'unicité qui cède quand une bande se remplit, un doublon de la même largeur valant mieux qu'une valeur d'une autre, et la bande ne dépasse jamais ce que tient le type qui ACCUEILLE le tirage, lequel n'est pas toujours celui de la colonne : un `integer` d'Odoo posé sur une colonne que PostgreSQL n'a pas créée entière — ce qu'une base montée de version porte — se coule en `numeric`, qui n'a pas de plafond, là où `integer` s'arrête à 2 147 483 647 et où un tirage au-delà fait retomber toute l'écriture, celle-ci tenant en une transaction
|
|
||||||
- L'anonymiseur pose quatre questions au lieu de onze, sept des onze ayant eu un défaut évident. Les répondre une par une pour arriver au même endroit fait passer les invites par réflexe, et une invite qu'on passe par réflexe ne consent à rien : elles vivent désormais derrière « Options avancées ? ». Le chemin court DIT ce qu'il prend, en phrases et non en oui/non, puisque ce sont des décisions prises et non des questions dont on aurait perdu la réponse. Macros et graphiques sont demandés dans les deux chemins : ils ne se voient pas dans les cellules
|
|
||||||
- `markdown-it-py` est déclaré en dépendance fonctionnelle : il rend en HTML le markdown du modèle de langage, avant assainissement, sur la page de l'assistant. Il arrivait dans l'environnement en transitif par `bandit` → `rich`, un outil de lint, si bien que retirer une dépendance de lint retirait le moteur de rendu d'une page portail
|
|
||||||
- Un cache de téléchargement partagé par les VM QEMU d'un hôte, installé depuis **Déploiement › Cache QEMU**. Deux VM de la même distribution cessent de tirer deux fois les mêmes centaines de mégaoctets : un fichier de paquet est servi du disque, tandis qu'un index est toujours repris à l'amont, si bien qu'un paquet retiré ne peut jamais devenir un « failed retrieving file … 404 ». L'index est stocké quand même et ne ressort que si l'amont est injoignable, ce qui rend un déploiement hors ligne possible. Le cache ne diminue jamais de lui-même : `--status` dit ce qu'il occupe
|
|
||||||
- L'interception est transparente et vaut pour tout le pont de l'hôte : une VM ne peut pas s'y soustraire de l'intérieur. Toutes approuvent l'autorité du cache tant que le service tourne. Pour en soustraire UNE, cocher « soustraire cette VM au cache » au déploiement : son adresse MAC est fixée avant la création et une exception est posée sur l'hôte. Pour les soustraire toutes, arrêter le service — ses règles partent avec lui. Un hôte Proxmox qui est lui-même une VM d'ici n'y échappe pas : les machines qu'il porte sortent derrière son adresse, et le déploiement pose donc l'autorité dans chacune, par ssh, avant d'installer quoi que ce soit
|
|
||||||
- La mesure ne se limite plus à Arch : tout système du catalogue dont la famille de paquets est connue, et au choix un lot de paquets ou l'installation réelle d'ERPLibre et d'Odoo 18. Mesuré sur Ubuntu 24.04 avec cette installation réelle, la seconde VM n'a tiré **aucun octet** de paquet du réseau et a fini 19 % plus vite. Vérifié sur les sept systèmes du catalogue
|
|
||||||
- Git est mis en MIROIR plutôt que caché : son protocole est une négociation, le serveur calculant sa réponse d'après ce que le client détient déjà, si bien qu'aucune réponse ne se réutilise. Un miroir nu par dépôt amont est tenu sur l'hôte et servi localement, et un miroir déjà détenu sert sans aucun réseau — mesuré, un dépôt cloné dans une VM alors que l'amont du cache était coupé. Mesuré sur une installation complète d'ERPLibre : la seconde VM n'a tiré **aucune** requête git de l'amont, là où git pesait quatre cinquièmes du trafic. **Déploiement › Cache QEMU › Miroirs git** les remplit d'avance depuis les manifestes, pour que la première VM ne paie pas tous les clonages
|
|
||||||
- Un déploiement hors ligne couvre aussi la suite de base d'une image cloud. Son apt REVALIDE l'index qu'elle livre au lieu de le télécharger, l'amont rend 304, et le cache n'avait donc aucun corps à garder — cette suite-là, et elle seule, manquait hors ligne pendant que ses voisines `-updates` et `-security` sortaient du disque. Une requête conditionnelle sur ce que le cache ne détient pas part désormais sans sa condition, une fois ; et un client qui détient sa propre copie reçoit 304 plutôt que 504 quand l'amont est muet.
|
|
||||||
- Ce qu'un déploiement hors ligne couvre, exactement : les paquets, les index de dépôts et git. PAS ce que le cache n'a pas le droit de déchiffrer — un client qui porte son propre magasin de confiance, npm et poetry en sont, passe en tunnel opaque, et un tunnel ne porte rien une fois le réseau coupé. Une installation complète d'ERPLibre demande donc toujours le réseau, même si tout ce que le cache détient est servi du disque
|
|
||||||
- Un miroir est COMPLET là où `repo sync` clone en profondeur un : il coûte donc des dizaines de gigaoctets. Sous dix gigaoctets libres, aucun miroir neuf n'est créé et la requête repart vers l'amont. Le diagnostic dit ce qu'occupent les objets et les miroirs, séparément
|
|
||||||
- **Déploiement › Cache QEMU › Âge et nettoyage** groupe le cache par âge du dernier usage (jour, semaine ou mois ; objets et dépôts git séparément) et rend ce qui n'a plus servi depuis un délai choisi, ou tout. Un objet servi voit sa date remise à jour : « vieux » veut donc dire « n'a plus servi ». Les deux nettoyages disent ce qui partirait avant d'effacer quoi que ce soit, et l'entrée 5 liste les miroirs du plus lourd au plus léger pour en effacer un
|
|
||||||
- `long_test/qemu_cache.py` mesure si le cache sert vraiment la seconde VM, et `--hors-ligne` coupe l'amont du seul service du cache pour prouver qu'une troisième se bâtit encore sur l'index stocké
|
|
||||||
- Avant de couper, le formulaire dit si le cache détient la suite de base de chaque système demandé : F5 prévient « le cache ne détient rien pour ubuntu 26.04 » et un second F5 passe outre. Le verdict n'est rendu que là où une version se nomme sans ambiguïté dans l'URL — les familles apt — plutôt que de rassurer à tort ailleurs. Le verdict est rendu composant par composant — `main`, `universe`, `restricted`, `multiverse`, sur la suite de base et sur ses compagnes `-updates` et `-security` —, car une seule URL en réserve le rendait muet quand un composant entier manquait, et apt ne le disait que vingt minutes plus tard, par « Unable to locate package »
|
|
||||||
- **Déploiement › Cache QEMU › L'emporter sur une autre machine** porte le magasin vers un autre hôte ERPLibre en un seul flux — `tar`, compressé, par ssh, sans fichier intermédiaire pour des dizaines de gigaoctets — et rend les fichiers au compte du service à l'arrivée, le même compte portant rarement le même numéro sur deux machines. Seul le MAGASIN voyage : un objet est rangé sous son URL et jamais sous la machine qui l'a pris, et un miroir git est un dépôt. Les réglages restent — pont, sous-réseau et autorité appartiennent à l'hôte, et l'entrée 1 les pose là-bas
|
|
||||||
- Le formulaire de déploiement porte une section **Réseau** avec « Sans connexion internet » : pour tout le déploiement, installation comprise, le service du cache perd sa sortie ET les VM perdent la leur — ping, autres ports, UDP, IPv6 —, si bien qu'un pas qui prendrait un autre chemin que le cache échoue au lieu de réussir en ligne sans rien dire. Les noms ne se résolvent plus par l'internet non plus : l'hôte répond lui-même à tout nom une adresse que le cache intercepte, si bien que seul ce que le cache détient est joignable ; dnsmasq doit être installé sur l'hôte. Une VM derrière le cache tire d'un seul miroir apt, fixe : la recherche de miroir de cloud-init écarte tout miroir derrière un résolveur qui répond à tout nom, et retomberait sur un autre miroir que celui dont les VM précédentes ont rempli le cache. La coupure refuse au lieu de jeter : une adresse que le cache ne détient pas répond 504 sur-le-champ, et non après un délai de connexion ; un noyau qui ne peut pas charger le module du refus reçoit le jeu qui jette. Offerte seulement là où le cache tourne, et le déploiement refuse plutôt que de partir avec l'amont debout, une VM bâtie ainsi réussissant pour la mauvaise raison. **Proxmox VE** porte la même case, offerte là où son hôte est lui-même une VM de ce pont : ses invités sortent derrière son adresse, si bien que la coupure locale les couvre et que rien n'est posé sur l'hôte distant. Un hôte Proxmox qui ne vit pas ici ne la reçoit jamais — rien ici ne sait couper sa sortie
|
|
||||||
- Un déploiement Proxmox VE peut demander l'**accélération 3D**. La VM est créée avec un écran accéléré (`--vga virtio-gl`) plutôt que la console série comme affichage, et le compte entre dans les groupes du GPU DANS l'invité : le nœud de rendu appartient à « root:render », si bien que sans eux toute application GL retombe en rendu logiciel alors que la négociation VIRGL a réussi, et rien ne le signale. La case n'est offerte que là où l'hôte distant a le nœud de rendu ET les trois bibliothèques que Proxmox charge pour cet écran — VIRGL, GL et EGL. Il nomme celles qui manquent et refuse de démarrer la machine, une fois son disque écrit et sa configuration posée : un hôte portant GL sans EGL échouerait donc à la toute dernière étape. Là où une pièce manque, le formulaire dit laquelle et quel paquet poser sur l'hôte — une case qui disparaît sans un mot se lit comme une régression. Un bouton les pose sans quitter l'écran — le formulaire rend le terminal pour que sudo puisse demander un mot de passe et qu'on voie apt travailler —, puis relit l'hôte au lieu de croire apt sur parole, et la case prend la place du message. Le port série reste posé, donc « qm terminal » fonctionne toujours
|
|
||||||
- Hors ligne, le cache rejoue ce qu'un passage en ligne a vu, et non plus les seuls corps en 200 : redirections, refus définitifs (404/410) et réponses HEAD des adresses volatiles sont gardés sans corps, sous des clés à eux, et servis SEULEMENT quand l'amont est muet. Un client TUF qui sonde la version suivante de sa racine reçoit le 404 qu'il attend au lieu d'un 504 : mise pose Python hors ligne, vérification Sigstore intacte ; les extensions GNOME, l'installateur de Claude Code et la version de rtk suivent leurs redirections hors ligne
|
|
||||||
- La coupure hors ligne tombe avec la dernière installation, et non à la fermeture du suivi : une unité root, qui reçoit la levée au lancement, attend le marqueur de fin de chaque installation et survit au suivi fermé tôt, à todo.py tué ou au terminal perdu — 12 h au plus. Le suivi est donc obligatoire hors ligne. Un second déploiement hors ligne est refusé pendant qu'un premier tourne, et un déploiement en ligne lancé entre-temps est prévenu qu'il tournerait hors ligne
|
|
||||||
- F5 lit aussi les essais hors ligne précédents de la même VM et prévient « au moins N adresses ont manqué », moins ce que le magasin détient désormais (`erplibre_go_qemu_cache --detient`, en lecture seule, sans root). **Déploiement › Cache QEMU › Combler ce qui a manqué hors ligne** les rejoue en ligne, à travers le cache
|
|
||||||
- Le journal d'installation nomme le commit que la VM exécute ; hors ligne, le récapitulatif dit, branche par branche, quel commit le miroir du cache donnera
|
|
||||||
- `long_test/qemu_cache.py --distro tous` (ou une liste séparée par des virgules) enchaîne une campagne par système du catalogue, défait les VM de chaque système avant le suivant, et finit sur un tableau des verdicts, durées et octets d'amont. Un échec n'arrête pas la série
|
|
||||||
- Le cache de téléchargement QEMU peut se nettoyer chaque jour : par âge (`EL_PURGE_AGE`, ex. `90j`) et par plafond de taille (`EL_MAX_SIZE`, ex. `50G`, le moins récemment servi partant d'abord, objets et miroirs git confondus). Les deux sont désactivés par défaut et se règlent depuis **Déploiement › Cache QEMU › Nettoyage automatique**, qui montre ce qui partirait ; `--purge-to-size` applique le plafond à la main. Une réinstallation garde les valeurs choisies
|
|
||||||
- NixOS 25.11 parmi les systèmes déployables, à côté des huit autres. C'est le seul dont aucune distribution ne publie d'image cloud : l'image est rebâtie par un tiers, donc sa version est épinglée, sa somme sha256 vérifiée à chaque téléchargement, et son origine dite avant que rien ne soit créé. cloud-init n'y reçoit aucune configuration réseau — son moteur networkd prend la clé d'un bloc pour un NOM d'interface, là où netplan honore un `match:` — et le compte y est créé avec `/bin/sh`, sshd refusant un compte dont le shell n'existe pas
|
|
||||||
- ERPLibre installé sur NixOS, ses dépendances DÉCLARÉES dans `conf/nixos/erplibre.nix` puis appliquées par `nixos-rebuild`, là où les quatre autres familles les installent commande par commande. `services.envfs` répond à `/usr/bin/env` et `programs.nix-ld` donne aux roues manylinux le chargeur dynamique qu'elles réclament ; les en-têtes de ce qui n'a pas de roue sont liés au profil du système, et leurs bibliothèques vont aussi au chargeur, un module compilé sur la machine ne portant pas de RPATH. L'installation l'atteint depuis le menu comme toute autre distribution : l'amorçage pose git, make et python3 — absents de cette image — dans le profil de l'utilisateur pour la durée du clone, et « make install_os » les redéclare pour le système. Vérifié sur une VM : le verrou de 362 paquets s'installe et Odoo répond en HTTP
|
|
||||||
- Une option `nix + nixos-anywhere` sur les AUTRES distributions, l'inverse de choisir NixOS : elle laisse une VM ordinaire capable d'installer NixOS sur toute machine joignable en SSH. Nix est appelé par son chemin absolu, le PATH du shell distant étant figé avant que l'installateur ne pose le binaire, et les fonctions des flakes sont redonnées sur la ligne de commande, l'écriture de `nix.conf` réclamant un sudo qui peut manquer
|
|
||||||
- NixOS se déploie sur un hôte **Proxmox** comme sur QEMU/KVM, ce qui a demandé trois correctifs qu'aucune lecture de code n'aurait trouvés. Son image n'a pas de secteur d'amorçage BIOS : une VM créée en SeaBIOS se déclare « running » avec une console muette, donc le catalogue marque désormais les images qui exigent l'UEFI — un marqueur par distribution, Debian 13 démarrant en SeaBIOS sur ce même hôte. `--ciuser` et `--sshkeys` s'en remettent au compte par DÉFAUT de l'image, que NixOS nomme autrement : le cloud-config du dépôt part maintenant comme extrait pour toutes les distributions, avec son bloc `users:` explicite. Et le lecteur cloud-init passe sur le bus SCSI — une image bâtie pour virtio seul n'a pas de pilote ATA et ne voit jamais un lecteur IDE, si bien que cloud-init cherchait des sources réseau et que la VM arrivait sans aucun compte. Vérifié de bout en bout : NixOS rend `/bin/sh`, Debian 13 `/bin/bash`, sudo aux deux
|
|
||||||
- L'image tierce est vérifiée contre la somme que le dépôt épingle pour elle sur le chemin Proxmox aussi, où un `wget` nu suffisait. Un seul accesseur porte cette somme pour les deux chemins, et le contrôle vaut même pour une image déjà en cache — le cas visé est un fichier substitué ou tronqué entre deux déploiements, qu'un test de présence ne voit pas
|
|
||||||
- Une entrée du cache de téléchargement peut être retirée, par son URL. Une somme qui ne correspond pas n'invalidait rien : le magasin continuait de servir les mêmes octets, et retélécharger ne changeait rien puisque c'est lui qui répond. Aucune des deux purges ne l'atteint — `--purge` efface tout, et `--purge-older-than` saute ce qui est récent alors que chaque service remet cette date, si bien qu'un objet empoisonné qui sert ne vieillit jamais. `--oublie` est le symétrique de `--detient` : mêmes lignes en entrée, mêmes fonctions de clé, mêmes refus, et un objet présent qui résiste est dit refusé plutôt qu'oublié. Les deux échecs de somme le nomment désormais, avec l'URL exacte
|
|
||||||
- Le journal d'installation porte ce que l'hôte a décidé avant de lancer : l'autorité du cache de téléchargement posée ou refusée, l'exception, le miroir. Ces lignes se disaient sur une console qui défile, pendant que le fichier qu'on rouvre après un échec ne portait que le symptôme — sur un invité sans magasin de confiance, un certificat refusé, des centaines de dérivations à construire et six cents lignes d'erreurs, sans un mot sur la cause
|
|
||||||
- Une entrée du cache de téléchargement s'oublie depuis le menu, sous « Âge et nettoyage ». Le binaire savait déjà le faire ; le menu n'offrait que les deux purges en gros, dont aucune ne vise un objet — « effacer ce qui n'a plus servi » n'atteint jamais celui que le service rajeunit chaque fois qu'il le rend, et « tout effacer » coûte le cache entier pour un fichier. `--detient` passe d'abord et fait l'aperçu : même ligne, même clé, sans rien modifier
|
|
||||||
- `erplibre_go_qemu_cache --recle` range à nouveau les objets d'un cache sous la clé courante, sans rien retélécharger, et fond les copies qu'un miroir portait sous plusieurs chemins. Les objets écrits sous l'ancienne règle de clé restent sur le disque mais deviennent INTROUVABLES, si bien que le service les redemande à l'amont et que la place qu'ils tiennent ne sert plus personne : sur un magasin de 12 764 objets, 5 419 étaient dans ce cas — 9,11 Gio — et la fusion des doublons a rendu environ 3,37 Gio. Le service doit être arrêté, le corps étant renommé avant son méta, et `--dry-run` ne fait que compter ce qui bougerait. Un statut seul n'est pas touché, sa clé portant l'hôte et non le chemin
|
|
||||||
- Le hook `pre-commit` lance `check_python_version.py` sur les fichiers indexés : il signale le source qui ne parse pas sous le Python de `conf/python-erplibre-version`, sans bloquer le commit, et dit quand aucun interpréteur de cette version n'était là pour vérifier. Ni black ni flake8 ne voient ce défaut — la cible de black borne ce qu'il écrit, jamais ce qu'il accepte
|
|
||||||
- `Déploiement › Local › [4]` ouvre un proxy SOCKS par SSH — `ssh -D`, port 1080 par défaut — pour que le navigateur atteigne, DEPUIS la machine distante, une interface qui n'écoute que sur sa boucle locale ou un hôte de son réseau. L'adresse vient de `~/.ssh/config` ou de la saisie ; un alias part tel quel à ssh, si bien que son `ProxyJump` s'applique encore et qu'une VM imbriquée reste joignable. Le réglage de Firefox s'affiche avant l'ouverture du tunnel, la commande ne rendant la main qu'au Ctrl+C
|
|
||||||
- `make format_test` formate `test/` et `long_test/`, qu'aucune cible ne couvrait : 75 fichiers sur 210 ne suivaient aucune norme, et seul un fichier signalé par un diff était touché
|
|
||||||
- `script/reverse_proxy/main.py` — un mandataire inverse de développement pour Odoo 18, sans nginx : une seule adresse envoie les pages au port web et `/websocket` au port du bus, les en-têtes `X-Forwarded-*` de `proxy_mode` n'étant posés que par le mandataire (`--trust-forwarded` prolonge plutôt une chaîne, derrière un autre mandataire). Seule la tête de la requête est lue : une WebSocket montée reste ouverte et une réponse gzip ou en morceaux passe octet pour octet. Il sert en HTTPS avec `--tls-cert`/`--tls-key`, écrit une ligne par requête (route, statut, durée ; `--quiet` la coupe), dit au démarrage quel port d'Odoo ne répond pas, et nomme ce port dans un 502. La tête d'une requête attend 30 s (408) et la connexion à Odoo 10 s (504), jamais une WebSocket ouverte. Écoute par défaut sur `127.0.0.1:8080`, `--listen 0.0.0.0` l'ouvre au réseau ; une requête par connexion, la production garde nginx
|
|
||||||
- `script/reverse_proxy/local_cert.py` émet une autorité locale, importée une fois dans le navigateur, et un certificat serveur qu'elle signe pour localhost, le nom d'hôte et ses adresses, sous `~/.erplibre/reverse_proxy_tls/` avec des clés en 0600 ; l'autorité est gardée quand le certificat est réémis
|
|
||||||
- `TODO › Execute › Network › Mandataire inverse Odoo` le lance avec les ports web et bus lus dans `config.conf`, demande cette machine seule ou tout le réseau et HTTP ou HTTPS, émet le certificat local au premier usage en HTTPS, et prévient quand `proxy_mode` est éteint ou que `workers` vaut 0 — sans worker, aucun port de bus n'écoute et `/websocket` échoue. `Network › Certificats TLS locaux` le réémet avec d'autres noms
|
|
||||||
|
|
||||||
## Modifié
|
|
||||||
|
|
||||||
- `Assistant › [1]` n'envoie plus chaque question à une seule API distante sur un modèle figé : elle interroge le serveur configuré, et ne retombe sur le distant que lorsqu'aucun serveur local ne répond
|
|
||||||
- Le contrôle d'hygiène des commentaires lit le Go, et non les seuls `#` : `//` hors d'une chaîne, la chaîne brute entre accents graves, et les blocs `/* … */`
|
|
||||||
- Les index `by-hash` de Debian et d'Ubuntu sont servis du disque, leur nom étant l'empreinte de leur contenu ; `…/releases/latest/download/…` n'est plus figé sur la première version vue
|
|
||||||
- Un amont qui refuse ou ignore les connexions est retenu 20 s : une requête qui a une réponse gardée est servie aussitôt au lieu d'attendre son délai d'établissement, qui faisait l'essentiel du temps d'une installation hors ligne ; une requête sans rien en réserve tente toujours l'amont. Un miroir git saute son rafraîchissement tant que sa forge est injoignable
|
|
||||||
- Le pré-remplissage des miroirs tourne sous le compte du service du cache, jamais en root
|
|
||||||
- Le menu des tests longs demande avant de créer de vraies machines, et redemande, avec des mots à lui, avant que `--detruire` efface des machines avec leurs disques. La commande est montrée d'abord, c'est elle qui rend la question répondable ; un plan à blanc ou un rapport de performance ne crée rien et ne demande rien
|
|
||||||
- Chaque entrée du menu Proxmox VE porte une icône, la même image voulant dire la même action que dans les autres menus de l'outil
|
|
||||||
- Le binaire du cache QEMU parle anglais ou français : journal du service, `--status`, `--age`, aide des options et erreur servie à une VM. La langue vient de `--lang`, puis d'`EL_LANG`, puis du français ; l'installateur écrit `EL_LANG` dans les réglages du service et le menu TODO passe la sienne. Règles, codes de verdict et clés JSON ne se traduisent jamais
|
|
||||||
- Un index de dépôt que le cache détient déjà est revalidé par son ETag au lieu d'être retéléchargé : l'amont juge toujours chaque requête, et un « 304 » sert le corps gardé depuis le disque. Sur une installation complète d'ERPLibre, les index pip, les métadonnées npm et le bundle de repo pesaient environ 110 Mo par VM, repris en entier à chaque fois. Un index rangé sans son hôte — partagé par tous les miroirs d'une liste qui tourne — et une réponse sans ETag sont repris en entier comme avant ; le journal d'accès nomme la nouvelle issue `revalidated`
|
|
||||||
- Une page de registre servie sous `Vary: Accept` garde une copie par représentation. npm demande la même page `/npm` abrégée, puis complète, puis de nouveau abrégée ; rangées sous une seule clé, elles se remplaçaient et les 31 Mo repartaient à chaque installation. Chaque représentation est désormais revalidée et, hors ligne, servie à part ; `--detient` lit toujours la page sous sa seule URL
|
|
||||||
- Une VM déployée l'amont du cache coupé — formulaire QEMU, Proxmox VE, ou `deploy_qemu.py --offline` — a l'audit de sécurité de npm désactivé (`NPM_CONFIG_AUDIT=false`) : il interroge un service qu'aucun cache ne rejoue, et échouait à chaque installation hors ligne. Une VM en ligne garde son audit
|
|
||||||
- Vérifier une image téléchargée ne demande plus `--verify` : c'est le défaut pour toute distribution qui publie une somme, et `--no-verify` est ce qui la saute — à réserver aux essais hors ligne, où une image substituée passerait autrement sans un mot
|
|
||||||
- `--bios` est refusé sur une image sans secteur d'amorçage BIOS, et dit pourquoi. Forcé là, il donnait une VM « running » à console muette — la panne même que ce drapeau évite ailleurs
|
|
||||||
- L'environnement virtuel d'outillage `.venv.erplibre` tourne en Python 3.14.7, indépendamment de celui d'Odoo (3.12.10 pour Odoo 18.0). `install_erplibre.sh` le bâtit par `install_venv.sh` et `EL_PYTHON_PROVIDER` plutôt qu'avec le `python3` du système
|
|
||||||
- `.venv.erplibre` sur un Python incompatible est SUPPRIMÉ puis rebâti ; ce qu'on y avait posé à la main part avec lui. Le venv d'Odoo est conservé quand seule sa version diffère, et rebâti seulement s'il est hors service : le rebâtir refait une installation Poetry entière. Un répertoire sans `pyvenv.cfg` n'est jamais effacé
|
|
||||||
- `make` et `make todo` lancent `./todo.sh`, dont le nom annonce un menu et non une installation ; il passe chaque argument à `install.sh`, qui choisit un interpréteur capable de LIRE le code avant de le lancer : `.venv.erplibre` s'il porte la bonne version, sinon le `python3` du système s'il est assez récent, sinon l'installation, qui ne part que sur un oui tapé au terminal (`o`, `oui`, `y`, `yes`) puisqu'elle peut supprimer `.venv.erplibre`. Un système plus ancien que `conf/python-erplibre-version` s'arrêterait autrement sur une erreur de syntaxe levée avant qu'aucun garde puisse nommer la commande à taper. TODO se relance ensuite dans `.venv.erplibre`, ou propose de lancer `install_erplibre.sh` en terminal
|
|
||||||
- L'image Docker de production bâtit `.venv.erplibre` sur le Python d'Odoo et s'arrête quand ce Python ne sait pas lire `script/`
|
|
||||||
- Debian 11 quitte le catalogue de déploiement : son LTS est terminé, et sa suite de sécurité n'est ni servie ni archivée — l'index qu'elle publie encore nomme des paquets dont le pool ne porte plus le fichier, et apt s'arrête avant d'installer git. Debian 13 la remplace, son image cloud étant toujours le dernier point de version
|
|
||||||
- Toutes les images de conteneur reposent sur bookworm, quelle que soit la version d'Odoo. La base est `python:<version>-slim-<version Debian>` : l'interpréteur vient de l'image officielle, jamais de Debian, et les variantes bookworm existent jusqu'à 3.7.17. Le build de wkhtmltopdf suit la version — celui de bullseye réclame libssl1.1, absente de bookworm
|
|
||||||
- Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige `>=3.12.10,<3.13`. Pour celui de l'outillage, la majeure.mineure suffit : exiger le patch écartait le Python d'une distribution d'un cran en retard — NixOS 25.11 livre 3.14.2 quand conf demande 3.14.7 — et faisait COMPILER CPython à pyenv pour une différence que rien ne réclame
|
|
||||||
- `make format` choisit le formateur d'après le contexte de chaque fichier : un module Odoo garde isort et black en `py37`, la série la plus ancienne encore supportée, quand l'outillage de ce dépôt passe par ruff, réglé une fois dans `.ruff.toml`. ruff suit les versions de CPython, là où black 24.8.0 s'arrête à `py313`, et son tri d'imports remplace isort ; c'est aussi ce qu'emploie la norme OCA depuis qu'elle a quitté black
|
|
||||||
- Les dépôts que Google Repo rapatrie sous `script/` sont écartés de ce formatage, chemin nommé compris : les reformater écrirait dans l'historique d'autrui. `target-version` y reste à `py310`, parce que les hooks git portent `#!/usr/bin/env python3` et qu'une distribution livre encore 3.10 — à partir de 3.14, ruff écrirait `except A, B:` sans parenthèses
|
|
||||||
- La liste des commandes Claude Code de `TODO › Execute › GPT code › Claude configs` compare chaque gabarit d'ERPLibre de `conf/` à sa copie de `~/.claude/commands/` : une commande non installée est affichée, une copie périmée est marquée de son compte de lignes ajoutées et retirées, et une commande venue d'ailleurs est signalée comme telle. Sur un oui, elle affiche le diff, puis redéploie les copies périmées en gardant le nom et le courriel git que portait `/commit`
|
|
||||||
- Un message de commit s'ouvre sur un sujet et un corps en anglais ; `--- FR ---` ouvre ensuite la section française, qui commence par le sujet traduit sous le même tag. Le hook `commit-msg` refuse un marqueur `--- EN ---` et une section française sans ce titre, et juge ce titre comme un sujet. `/commit` et `/git_prepare_merge` suivent le même ordre
|
|
||||||
- Menus de TODO : la langue se règle seulement depuis Configuration, l'entrée en double dans Execute disparaît, et Fork quitte le menu principal pour Configuration. Le menu principal numérote désormais Télémétrie 4 et Configuration 5. Le choix de la langue montre un drapeau par langue
|
|
||||||
- Dépendances d'Odoo 18 rafraîchies. `openai` est épinglé en 2.x, dont la 3.x exige un `idna` qu'Odoo 18 interdit ; `fsspec` est épinglé à côté de `s3fs`, qui l'exige à sa propre version exacte, si bien que les deux se montent ensemble ; `meteostat` revient en 1.x, toute 2.x plafonnant `pytz` sous 2024. PyMuPDF reste écarté sur s390x, désormais déclaré dans les requirements pour qu'une régénération le garde. Les montées majeures de `ujson` 6, `plotly` 7, `python-slugify` 9 et `sqlalchemy` 2.1 ne sont pas encore testées
|
|
||||||
- Dependabot ignore les versions majeures de `meteostat`
|
|
||||||
- Odoo 18 passe à pandas 3.0.6, cryptography 50 avec pyopenssl 26.4, Pillow 12.3 et botocore/boto3 1.43.75 avec aiobotocore 3.9.1, le plus haut botocore que sa plage étroite accepte. Les sept modules qui importent pandas exécutent leurs appels pandas sans changement ; `freq='d'` dans un tableau de bord de présence Cybro avertit désormais et cassera avec pandas 4
|
|
||||||
- Dependabot réunit `aiobotocore`, `botocore` et `boto3` dans une seule demande de fusion, chaque aiobotocore n'acceptant qu'une plage étroite de botocore ; les correctifs de sécurité arrivent toujours seuls
|
|
||||||
- `TODO › Transform data` lit Excel avec openpyxl 3.1.5 et xlsxwriter 3.2.9 ; le test de fuite qui garde l'épingle exacte d'openpyxl passe sur eux
|
|
||||||
- factur-x exige 6.8 hors s390x, la version déjà verrouillée : une régénération ne peut plus retomber sur une 4.x ou 5.x non testée
|
|
||||||
- Le choix d'interface du déploiement QEMU et de la migration Odoo, et ses préférences dans `TODO › Configuration`, marquent le formulaire TUI d'un 📋 et les questions ligne par ligne d'un 💬
|
|
||||||
|
|
||||||
## Corrigé
|
|
||||||
|
|
||||||
- La lecture des baux dnsmasq n'ouvre plus d'invite de mot de passe root : les fichiers sont lus en direct, ce qui suffit sur une installation standard où ils sont en 0644, et `sudo -n` n'est tenté qu'ensuite, qui échoue au lieu de demander. L'affichage d'une liste de VM appelait ce chemin une fois par VM, et l'attente d'une VM toutes les trois secondes pendant dix minutes
|
|
||||||
- `--max_process` repart sur Python 3.10 et plus : `loop=` a quitté `asyncio.wait` en 3.10 et `asyncio.get_event_loop()` lève hors d'une loop en marche depuis 3.14, si bien que le pool n'était même plus instanciable alors que l'aide annonçait toujours l'option
|
|
||||||
- Une invite n'écrit plus son deux-points deux fois — le menu le plus vu du logiciel demandait « Commande :: », et sept invites de déploiement à distance affichaient un deux-points suivi d'un autre
|
|
||||||
- Onze sous-menus laissent désormais leur segment dans le fil d'Ariane, et la télémétrie de navigation cesse de les classer comme des commandes sous leur nom de méthode brut
|
|
||||||
- Les trois lecteurs de `~/.ssh/config` s'accordent sur ce qu'est un nom de machine : un alias déclaré par un `host` en minuscules est vu, une tabulation sépare aussi légalement qu'un espace, et un motif nié `!nom` n'est plus pris pour une machine à joindre
|
|
||||||
- Trois clés de traduction déclarées deux fois ont disparu, et un contrôle refuse la suivante : une clé répétée écrase la précédente en silence, ce qui avait déjà coûté une étiquette de menu
|
|
||||||
- `anonymize.py` s'abstient sur une colonne numérique unique et la NOMME dans le rapport. Un tirage ne garantit rien sur une colonne unique, et un nombre n'a pas l'issue qu'a le texte — coller l'id porte l'unicité pour du texte, mais changerait la grandeur d'un nombre. Deux lignes tirant le même nombre faisaient échouer l'UPDATE et, transaction unique oblige, emportaient TOUTE l'anonymisation. Sans que le rapport la nomme, une colonne identifiante reste en clair sans que rien ne le dise
|
|
||||||
- Restaurer une sauvegarde ne lève plus `AttributeError` avant de lancer la commande : `_monitoring_restore` lisait `self._execute` là où `TODO` pose `self.execute`, et les deux entrées qui y mènent — la sauvegarde locale et la distante — étaient cassées. Un contrôle refuse le suivant : tout `self.X` que `TODO` LIT doit être posé par TODO ou par une de ses bases. Chercher le nom dans tout `script/todo/` ne voyait pas la faute, un autre objet du paquet posant bien `_execute` ; il faut le chercher dans les classes dont TODO HÉRITE
|
|
||||||
- Les scripts Selenium tournent sur un Python bâti sans `tkinter` — tout serveur dépourvu de paquet d'interface graphique. Ce module ne sert qu'au sélecteur de fichier du coffre, il est donc désormais optionnel : sans `tkinter` et sans chemin KDBX configuré, l'ouverture du coffre journalise une erreur et rend la main, au lieu de casser l'import de tous les scripts de pilotage de navigateur
|
|
||||||
- Le mode sombre repart en fenêtre privée sur un Firefox récent. La permission « Exécuter dans les fenêtres privées » est accordée à l'installation de l'extension, par le champ `allowPrivateBrowsing`, au lieu d'être cochée dans l'interface `about:addons` : Firefox refuse la navigation vers `about:addons` depuis le contexte contenu, et le contexte chrome exige `-remote-allow-system-access`, que geckodriver refuse via les capabilities, si bien que les deux voies vers cette case sont fermées. Un geckodriver qui ignore le champ installe sans la permission au lieu d'interrompre la session
|
|
||||||
- L'étiquette « - Default » reparaît aux menus des versions et des environnements : les deux lectures demandaient une clé à majuscule que le fichier des versions n'écrit pas, et une clé absente ne rend rien sans rien dire
|
|
||||||
- Le message « rien en réserve » du cache est inerte pour un interpréteur de commandes, chaque ligne étant un commentaire. Livré à un installateur bâti sur `curl … | bash`, il devenait une cascade de « command not found » qui masquait la cause. Un tel téléchargement demande en outre à curl d'échouer sur une erreur HTTP plutôt que d'exécuter la page d'erreur
|
|
||||||
- L'attente qu'une VM soit prête couvre aussi la pose de l'agent invité. Celle-ci part en unité DÉTACHÉE pour que cloud-init rende la main en quelques secondes, et elle fait un `apt-get update` : « cloud-init status --wait » disait « done » alors que le verrou des paquets était encore tenu, l'étape suivante épuisait ses reprises, puis installait sur un index jamais rafraîchi — « Impossible de trouver le paquet », un message qui accuse le dépôt et non le verrou. Un update qui n'aboutit pas le dit désormais sur le champ
|
|
||||||
- Une installation de bureau n'attend plus des minutes sur le verrou apt : le SERVICE apt-daily est arrêté et non son seul minuteur, un minuteur désactivé n'interrompant pas l'apt-get qu'il a déjà lancé ; et la reprise repasse toutes les deux secondes au lieu de dix, « DPkg::Lock::Timeout » ne couvrant pas ce verrou-là
|
|
||||||
- Les VM Fedora démarrent de nouveau : le micrologiciel charge et démarre leur chargeur, puis se fige sans écrire un octet — pas de console, pas de bail DHCP, une machine « en cours d'exécution » qui ne fait rien. Fedora est amorcée en BIOS hérité, où la même image démarre son noyau ; `--bios` garde le dernier mot
|
|
||||||
- Une VM reçoit un nom d'hôte qu'elle accepte — un souligné, que le nom de domaine libvirt tolère, lui faisait garder le nom générique de son image — et un fuseau que sa distribution connaît, un alias hérité la laissant en UTC
|
|
||||||
- starship s'installe dans une VM : son installateur tourne en root, borné par un délai root, et n'atteint jamais le `sudo -v` que sudo-rs refuse ; son crochet de shell n'écrit plus « command not found », ni ne fait échouer un rc sourcé, quand starship est absent
|
|
||||||
- Chaque outil optionnel dit s'il a été posé, et une extension GNOME dont le téléchargement a échoué n'est plus déclarée indisponible pour ce GNOME
|
|
||||||
- mise, pyenv et les extensions GNOME sont téléchargés, puis exécutés : sans pipefail, `curl | sh` ne pouvait ni signaler un téléchargement raté ni atteindre son repli
|
|
||||||
- L'unité de l'agent invité ne finit plus en échec après une pose réussie
|
|
||||||
- Le cache répond 508 à une requête qui le vise lui-même, au lieu de s'appeler jusqu'à épuiser ses descripteurs
|
|
||||||
- Le diagnostic du cache ne donne plus un service arrêté pour actif
|
|
||||||
- L'attente d'`apt-get update` est bornée par une échéance et non par un nombre d'essais. Un essai échoue en moins d'une seconde sur un verrou tenu, mais dure des minutes quand le cache rend 504 sur chaque index qu'il ne détient pas : soixante essais valaient alors des heures de silence là où cinq minutes étaient promises, et l'installation échouait ensuite sur des dépendances introuvables
|
|
||||||
- Un invité Proxmox est fixé sur le même miroir apt que le reste du parc, écrit par ssh avant tout téléchargement. Le magasin range ses index par HÔTE : une VM restée sur les dépôts par défaut de son image ne retrouvait rien de ce dont le cache avait été rempli — hors ligne, chacun de ces index manquait
|
|
||||||
- L'installateur du cache accepte un pont et un sous-réseau donnés à la main. Il sondait libvirt d'abord et mourait sur « réseau default introuvable » : le contournement annoncé dans son propre en-tête — `EL_BRIDGE` et `EL_SUBNET` — était donc hors d'atteinte. Une machine dont le réseau libvirt n'est pas démarré, ou qui porte son pont autrement, peut désormais poser le cache en le nommant ; et quand la sonde tourne et échoue, elle nomme les deux issues
|
|
||||||
- **Déploiement › Cache QEMU › Installer ou réinstaller** n'annonce plus « installé et démarré » quand l'installateur a échoué. Il n'attrapait que les exceptions : un code de sortie non nul — réseau libvirt absent, compilation qui cède — imprimait la ligne de réussite juste sous l'erreur elle-même, avec le chemin d'une autorité qui n'existe pas
|
|
||||||
- Un invité Proxmox reçoit de nouveau son guide, son fuseau, son miroir apt et l'autorité du cache. Ces quatre gestes passent par ssh et partaient dès qu'une adresse était connue, pendant que cloud-init posait encore les comptes et les clés : ils échouaient ensemble, et la VM naissait en UTC, sans guide, sans autorité et sur les dépôts de son image. Le déploiement attend désormais que la machine réponde en ssh — cinq minutes au plus — et le dit quand elle ne répond jamais, au lieu d'échouer quatre fois de suite
|
|
||||||
- Le cache ne sert plus un index de dépôt plus récent que la signature qui l'annonce. Hors ligne, chaque objet sortait avec sa propre date : un index rafraîchi samedi sous un `InRelease` de vendredi faisait échouer apt sur « File has unexpected size » ou « Hash Sum mismatch », et l'installation s'arrêtait sur des dépendances introuvables — un message qui accuse le dépôt, jamais le cache. La comparaison porte sur le `Last-Modified` de l'amont, jamais sur la date de stockage, renouvelée à chaque service
|
|
||||||
- Le refus d'emporter le cache sur une autre machine nomme le geste qui le lève, et ce geste n'est pas l'entrée 1 — elle pose le cache ICI, si bien que la suivre faisait réinstaller l'hôte qui en avait déjà un pendant que l'arrivée restait sans rien. Trois situations étaient rendues par un seul « pas de cache » : un lien ssh qui n'a jamais exécuté la sonde, une arrivée sans cache, une arrivée dont le cache n'a pas son compte de service. Chacune a désormais son message, et celle du cache absent énumère les gestes à faire SUR l'arrivée, avec les deux issues d'un installateur qui lit le réseau libvirt « default » — lever libvirt, ou nommer le pont, un libvirt arrêté le faisant mourir sur un réseau qui existe. Les gestes nomment aussi la branche à donner à l'arrivée, lue sur cet hôte : l'installateur est un fichier du dépôt, si bien qu'une machine restée sur une autre branche répond « fichier introuvable », ce qui ne ressemble en rien à un cache absent. Un quatrième cas est éprouvé avant qu'un seul octet ne parte : un sudo qui réclame un mot de passe à l'arrivée, auquel aucun terminal ne peut répondre puisque le magasin occupe lui-même l'entrée standard de ssh — le message donne le ticket à y obtenir d'abord. Le privilège n'est demandé qu'UNE fois à l'arrivée, « tar » et « chown » sous une seule invocation. Quand le sudo de là-bas réclame un mot de passe, l'entrée offre deux issues au lieu d'échouer : la ligne sudoers à poser une fois, ou un mode en deux temps — le magasin part dans le compte de l'arrivée, qui n'exige aucun privilège, et une commande affichée l'extrait depuis un terminal de là-bas, où un mot de passe se tape. Ce mode réclame deux fois le magasin à l'arrivée, vérifié avant qu'un octet ne parte, un cache ne contenant que des paquets et des archives git déjà comprimés. Le guide porte la même section
|
|
||||||
- Le formulaire de déploiement Proxmox VE éprouve le cache avant de couper le réseau, comme celui de libvirt le faisait déjà : ce que le magasin n'a pas, aucune VM coupée ne le lira, et l'échec tombait une heure plus tard, à la pose du bureau — un message qui accuse le dépôt, jamais le cache. F5 à nouveau vaut passage outre. Les deux formulaires partagent désormais un seul verdict au lieu de deux copies, qui auraient divergé au premier ajustement
|
|
||||||
- Le cache garde ses index de dépôt quand une liste de miroirs tourne. Un index publié sous l'empreinte de son contenu — « by-hash/SHA256/… » — était rangé sous une clé portant l'hôte : les mêmes octets servis par un second miroir étaient repris à l'amont, et hors ligne ils manquaient tout simplement — l'installation échouait sur des fichiers que le magasin détenait, avec un message qui accuse le dépôt. Un tel objet est désormais rangé sous son seul chemin, son nom ÉTANT la somme de son contenu
|
|
||||||
- Le diagnostic du cache n'annonce plus « aucune règle de détournement n'est posée » quand il n'a simplement pas pu les lire. Sur un hôte dont le sudo réclame un mot de passe, « sudo -n nft » ne rend rien, et ce silence était lu comme une absence de règle — ce qui envoyait réinstaller un cache qui détournait correctement. La lecture porte désormais un troisième état, « impossible de savoir », marqué d'un point et non d'une croix, comme le faisait déjà la lecture de la coupure d'amont
|
|
||||||
- Une coupure levée rend aussitôt leur chance aux amonts. Le service retient un court moment ceux dont la connexion vient d'échouer, pour qu'une installation hors ligne ne paie pas le délai d'établissement des centaines de fois ; rien ne lui disait que la coupure était finie, et les premières requêtes d'après la levée se rabattaient sur le magasin alors que le réseau était déjà revenu. La levée touche désormais un témoin dans le magasin, que cette mémoire consulte — il n'existe aucun canal vers le service en marche
|
|
||||||
- Avant de couper, le formulaire avertit aussi de ce qu'aucun index ne peut révéler : un paquet que le déploiement pose HORS du fil observé — l'agent invité, posé par une unité détachée dont l'échec ne remonte nulle part — et les dépôts git déclarés par les manifestes qui n'ont pas encore de miroir. Un index de suite en réserve suffisait à faire passer le cache pour complet alors qu'aucun octet de ce paquet ne l'avait jamais traversé ; et une négociation git ne se garde jamais, si bien qu'un dépôt sans miroir ne peut tout simplement pas être cloné une fois le réseau coupé
|
|
||||||
- L'avertissement sur les miroirs ne compte que les dépôts de la version d'Odoo déployée. Il additionnait tous les manifestes du dépôt — le déprécié compris — et annonçait 170 miroirs manquants là où un vrai déploiement Odoo 18 en rencontre quatre : une alarme qui sonne pour rien est une alarme qu'on cesse de lire. Le remplissage des miroirs, lui, prend toujours de l'avance pour toutes les versions, ce qui est son rôle
|
|
||||||
- Un hôte banni du déchiffrement sur une rafale d'erreurs de transport retrouve sa chance. Trois poignées de main manquées d'affilée le passaient en tunnel opaque, et un tunnel ne consulte jamais le magasin : un miroir de distribution condamné par quelques enregistrements corrompus renvoyait tout son trafic à l'amont — y compris les centaines d'objets déjà détenus pour lui — jusqu'au redémarrage du service. Une alerte TLS bannit toujours définitivement, le client ayant regardé notre certificat et l'ayant refusé ; mais une coupure répétée n'est qu'un soupçon, et elle se rouvre au bout de dix minutes
|
|
||||||
- **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
|
|
||||||
- `make` cherche bash au lieu de présumer `/bin/bash`. Ce chemin n'existe pas sur NixOS, où le shell vit dans le store, et make s'arrêtait avant d'exécuter la moindre recette — y compris celle qui installe de quoi créer ce chemin. Ailleurs, le shell résolu est celui d'avant
|
|
||||||
- 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
|
|
||||||
- Un invité sans ancre de confiance par fichier est soustrait au cache de téléchargement plutôt qu'intercepté sans elle. Le détournement est transparent et vaut pour tout le pont : une VM à qui l'on ne donne pas l'autorité échoue quand même sur « self-signed certificate in certificate chain » — et sur un système déclaratif, poser l'autorité arrive trop tard, la première reconstruction étant le premier téléchargement. Sur un hôte Proxmox, c'est l'HÔTE qui est excepté : un invité imbriqué sort masqué derrière lui et le pont ne voit jamais sa propre adresse. Mesuré depuis l'invité : code 000 et vérification SSL 19, puis 200 et 0. Sans cela le gestionnaire de paquets se rabattait sur 564 dérivations à construire, dont les sources échouaient pour la même raison
|
|
||||||
- NixOS apprend l'autorité du cache de téléchargement SANS reconstruire, et n'en est donc plus soustrait — ce qui lui fermait le déploiement hors ligne, le magasin étant alors la seule source et une VM exceptée n'ayant plus rien. Il n'a pas d'ancre de confiance par fichier et /etc est généré en lecture seule, quand une déclaration arriverait trop tard, la première reconstruction étant le premier téléchargement. L'autorité y est donc POINTÉE, consommateur par consommateur, par l'environnement — et l'environnement se perd à trois frontières que les quatre familles impératives ne rencontrent jamais. nix-daemon est activé par socket et ne voit aucune session : un fragment sous /run/systemd/system, un tmpfs inscriptible là où /etc ne l'est pas, l'atteint. sudo l'efface, et le nix de root parle droit au magasin local plutôt qu'au démon, téléchargeant lui-même : « Defaults env_keep » fait traverser les variables, visudo étant atteint par le profil du système, le PATH qu'y donne cloud-init ne portant aucun sudo. Et la session ssh s'ouvre une seconde avant que cloud-init n'écrive le faisceau, si bien que les exports vivent DANS l'attente de celui-ci plutôt qu'en tête de commande. Le faisceau concatène les autorités du système et celle du cache, donner la seconde seule ferait cesser d'approuver tout le reste. Vérifié amont COUPÉ sur une VM neuve : nix-shell réalise depuis le magasin ; et cache interceptant, une installation complète se termine sans un refus de certificat
|
|
||||||
- Odoo répond depuis l'extérieur d'une VM NixOS. Il écoutait sur 0.0.0.0:8069 et répondait en local, mais NixOS active un pare-feu par défaut là où aucune des quatre autres images cloud n'en active : l'hôte ne recevait rien — pas un refus, un silence jusqu'au délai — et le suivi déclarait Odoo absent sur une machine où il tournait. Mesuré depuis l'hôte : 000 après 12 s, puis 303 en 9 ms
|
|
||||||
- ERPLibre tourne comme service sur NixOS. L'installation finissait par écrire une unité dans `/etc/systemd/system`, généré depuis le store et monté en lecture seule : elle rendait 1 à sa dernière étape, après que le clone, le venv et un démarrage d'Odoo avaient tous réussi. L'unité est désormais déclarée par le module ; son interpréteur vient du store, `/bin` étant un montage FUSE d'envfs que systemd ne voit pas quand il résout l'exécutable ; et son PATH porte bash, dont l'absence arrêtait `run.sh` avant Odoo
|
|
||||||
- Ce qu'un système déclaratif doit déclarer, et que les quatre autres reçoivent gratuitement de leur image cloud : xmlsec1, sans lequel Odoo refuse d'installer auth_saml, module du chemin des addons ; parallel et shfmt, appelés par leur nom nu ; growpart, absent de tout le système alors que l'agrandissement du disque s'écrit « … || true » et rendait 0 sans rien agrandir ; et l'agent invité, qui venait de l'image et non du dépôt, le PATH de son unité manquant findmnt si bien que guest-exec mourait en 127 dès sa première ligne
|
|
||||||
- Le guide de connexion s'affiche sur NixOS. Le déploiement écrit `/etc/motd` partout et compte sur pam_motd pour le montrer — vrai des quatre images cloud, faux ici, où sshd rend « printmotd no » et où la pile PAM ne contient aucun pam_motd : le guide était écrit, complet, et personne ne le lisait. Il gagne aussi un bloc propre à NixOS, qui nomme le piège pour lequel il existe — `/etc/nixos/erplibre.nix` est réécrit par `make install_os`, et les déclarations qu'on y ajoute disparaissent sans un mot
|
|
||||||
- « make db_drop_all » n'annonce plus détruites des bases qui ne le sont pas. Il composait une commande parallel, jetait son code de retour et imprimait la liste ; le cas s'atteint dès que parallel manque du PATH, et l'opérateur passe à la suite en croyant ses bases parties
|
|
||||||
- Un miroir du cache de téléchargement refusé faute de place nomme son seuil et sa mesure. Il reprenait un champ que tout appelant ordinaire laisse à zéro — « moins de 0 o libres sur le disque » n'annonce aucun seuil et ne dit pas ce qui a été mesuré
|
|
||||||
- Une image téléchargée est vérifiée contre la somme que son éditeur publie, pour toute distribution qui en publie une et SANS le demander. La vérification existait sous un drapeau et pour Ubuntu seulement, si bien que les autres images arrivaient sans que rien ne les regarde. Six y entrent, relevées sur les dépôts plutôt que devinées : Debian publie du sha512 quand tout le reste est en sha256, les familles RHEL nomment le fichier « CHECKSUM », Rocky l'écrit en forme BSD, et Arch comme openSUSE posent une somme par image. Un fichier de sommes injoignable n'arrête plus un déploiement — c'est une panne de disponibilité — quand un écart arrête toujours tout et supprime l'image
|
|
||||||
- La locale qu'une VM déployée reçoit s'applique sur NixOS. cloud-init l'applique par locale-gen et update-locale, absents là-bas, et la VM gardait le défaut de la distribution — « fr_CA.UTF-8 » demandé, « en_US.UTF-8 » obtenu. Elle et le fuseau sont désormais déclarés par le module, et ni l'un ni l'autre n'est imposé à une NixOS qu'on avait déjà
|
|
||||||
- Le guide de connexion tient dans un terminal de 80 colonnes même sur une VM qui porte ses outils et un bureau, là où il en rendait 103 : le cadre débordait, le terminal repliait où il voulait, et l'alignement en deux colonnes — seule chose qui rend le guide lisible d'un coup d'œil — se perdait. La glose se replie désormais sous sa colonne, et passe sous sa commande quand celle-ci ne laisse plus de quoi écrire. C'est la mise en page qui porte la règle, non la longueur des textes, qui n'aurait tenu que jusqu'au prochain outil
|
|
||||||
- Une VM déployée hors ligne reçoit les variables du cache avant que son installation ne commence. « cloud-init status --wait » rend la main dès que cloud-init se déclare en erreur — un module accessoire y suffit — alors que son étape finale écrit encore l'autorité, `/etc/environment` et le fichier sudoers ; une session ouverte dans cette seconde-là vivait sans elles, et une installation lancée par sudo rejetait alors le certificat du cache. Le déploiement attend désormais l'unité cloud-final, et son seul état « activating », ce service étant un oneshot qui reste actif une fois fini
|
|
||||||
- Une métadonnée de dépôt qu'une distribution RPM nomme d'après l'empreinte de son contenu est servie du disque comme un paquet, et une demande de plage sur un fichier que le cache ne détient pas fait prendre le fichier entier en arrière-plan, une fois par clé. Un tel index de dizaines de mégaoctets était repris en entier à chaque installation — environ 110 Mo par VM sur un système de la famille RHEL — et dnf, qui prend ses métadonnées zchunk par plages, recevait un « 504 » l'amont coupé, aucune plage ne se gardant
|
|
||||||
- Une VM déployée hors ligne n'attend plus une synchronisation de l'heure qui ne peut pas venir. Une image qui active `systemd-time-wait-sync` retient `time-sync.target` jusqu'à la première réponse NTP, et l'étape finale de cloud-init est ordonnée après elle : sans serveur de temps joignable, cette étape ne démarrait jamais, ni les clés d'hôte ssh qu'elle génère, et la VM atteignait son invite de connexion sans jamais répondre en ssh
|
|
||||||
- Le cache de téléchargement ne reprend plus à l'amont un paquet qu'il détient déjà parce qu'un miroir le range sous un autre chemin. Un miroir préfixe le chemin à sa guise — « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » — et le chemin entier donnait deux clés pour les mêmes octets : sur un journal de 7099 noms livrés, 1124 vivaient sous plusieurs chemins et 3,18 Gio repartaient à l'amont pour rien. Seuls les six derniers segments comptent désormais, les segments vides tombant avec eux. Six est la plus petite borne sans collision : un chemin Debian en porte exactement six, si bien que cinq servirait le paquet d'Ubuntu pour celui de Debian, qui porte le même nom pour d'autres octets. Les paquets pacman s'arrêtent à quatre, leurs chemins étant plus courts que la borne commune, ce qui retire le préfixe du miroir et garde le nom du dépôt. Un magasin rempli avant ce changement se rattrape par `--recle`
|
|
||||||
- Une image gardée devenue périmée est reprise une fois au lieu d'arrêter le déploiement. Le répertoire « latest » d'une distribution avance à chaque version mineure et la somme publiée cesse de décrire l'image du disque, sans qu'un octet soit corrompu : la vérification la supprimait puis sortait, perdant une campagne entière — trois VM — pour une péremption qu'un seul téléchargement répare. Un second écart porte sur des octets fraîchement téléchargés, arrête tout et supprime l'image ; appelée sans liste de miroirs, et pour des sommes injoignables, rien ne change
|
|
||||||
- `long_test/qemu_cache.py` ne fait plus échouer une campagne où le cache a tout servi. Une URL n'est « déjà vue » que si la première VM en a obtenu les octets : un même paquet vit sous deux chemins selon le miroir, et la première VM peut recevoir « 504 » sur l'un — amont jugé muet — puis être servie du disque par l'autre, si bien que rien n'était rangé sous le premier chemin et que le téléchargement honnête de la seconde y était compté en faute. Un statut absent vaut livré, les journaux d'avant ne l'écrivant pas toujours, et la ligne des fichiers neufs nomme ses deux causes au lieu d'imputer à Arch quelle que soit la distribution mesurée
|
|
||||||
- Un téléchargement d'image coupé se reprend sur le même miroir, trois essais, par un `Range` qui demande la suite, au lieu de jeter ce qu'il a reçu : une image de VM pèse un demi-gigaoctet, et un `.part` tronqué renvoyait le lecteur à un `curl -C -` tapé à la main. Un serveur qui ignore le Range rend le fichier entier, et le transfert repart alors de zéro plutôt que de doubler les octets déjà là
|
|
||||||
- Les variables de certificat du cache ne désignent un faisceau que s'il existe. Une Fedora récente n'a pas `/etc/pki/tls/certs/ca-bundle.crt`, et viser un chemin absent faisait refuser à pip TOUT téléchargement, y compris ce qui n'a rien à voir avec le cache. Aucun trouvé n'écrit aucune variable, et pip garde son propre jeu de certificats
|
|
||||||
- `make version` nomme les deux Python, celui d'Odoo et celui de l'outillage. Un libellé nu laissait croire que le dépôt n'en a qu'un, et le chiffre affiché n'était pas celui du venv dans lequel on travaille
|
|
||||||
- `make` ne lance plus un `.venv.erplibre` bâti sur une autre machine, comme un checkout monté par le réseau, qui mourait sur « No module named 'encodings' ». `install.sh` lit la version de l'interpréteur en exécutant du code : `python -V` répond avant le chargement de la bibliothèque standard, et se portait donc garant d'un interpréteur incapable de démarrer. Un tel venv est désormais signalé comme bâti ailleurs, avec la commande qui le rebâtit
|
|
||||||
- Dependabot n'ouvre plus de demandes de fusion sur les requirements figés d'Odoo 12 à 17 : ses mises à jour de sécurité lisent `requirement/` comme un répertoire à part, que les exclusions ne couvraient pas
|
|
||||||
- `poetry_update.py` s'arrête sur un `pyproject.toml` absent en nommant la commande qui le crée, `make switch_odoo_XX` pour la version active, et propose de la lancer puis de se relancer quand il tourne dans un terminal
|
|
||||||
- `poetry_update.py` fait tourner Poetry dans le venv Odoo même depuis un shell sous `.venv.erplibre`, dont le Python le faisait échouer sur « InvalidCurrentPythonVersionError »
|
|
||||||
- `poetry_update.py` écarte les requirements et manifestes sous un répertoire `doc`, `docs`, `example` ou `examples` : un fichier d'exemple déclarant un `>=` lâche ne déplace plus une dépendance pour tout l'environnement
|
|
||||||
|
|
||||||
## Retiré
|
|
||||||
|
|
||||||
- La dépendance `sshconf`, déclarée et installée partout et importée nulle part
|
|
||||||
|
|
||||||
## Sécurité
|
|
||||||
|
|
||||||
- Une clé d'API et un jeton Bearer sont caviardés eux aussi avant qu'une commande soit affichée, journalisée ou réimprimée : `OPENAI_API_KEY=` partait en clair, et un jeton d'en-tête échappait par construction, ne portant ni nom d'option ni nom de variable
|
|
||||||
- En suivant une redirection, le cache ne transmet plus les identifiants du client (Authorization, Cookie, Proxy-Authorization) à un autre hôte
|
|
||||||
- Odoo 18 installe `idna` 3.20 au lieu de la 3.6 qu'épinglent ses propres requirements, touchée par CVE-2024-3651
|
|
||||||
- Odoo 18 installe `requests` 2.32.4 au lieu du 2.31.0 qu'épinglent ses propres requirements, touché par CVE-2024-35195 et CVE-2024-47081
|
|
||||||
- Le miroir git du cache QEMU ne clone plus qu'en `http` et `https` : un client pouvait nommer un dépôt `ssh://` ou `git://` dans sa requête et faire se connecter le cache, avec les clés de son compte de service, à un hôte de son choix. Les dépôts servis en HTTPS sont mis en miroir comme avant ; le binaire annonce 0.2.17
|
|
||||||
|
|
||||||
|
|
||||||
## [1.8.0] - 2026-09-04
|
|
||||||
|
|
||||||
**Notes de migration**
|
|
||||||
|
|
||||||
Recréer l'environnement virtuel, l'interpréteur Python et l'installateur de
|
|
||||||
paquets se choisissant désormais. Utiliser le guide d'installation depuis
|
|
||||||
l'outil `make`. Ubuntu 20.04 et 22.04 ne sont plus supportés.
|
|
||||||
|
|
||||||
## Ajouté
|
|
||||||
|
|
||||||
- Déployer des VM ERPLibre en QEMU/KVM depuis des images cloud : Ubuntu, Debian, Fedora, AlmaLinux, Rocky, openSUSE, Arch, Linux Mint, et Debian sur s390x. Matériel, branche et version d'Odoo se règlent par machine
|
|
||||||
- Proxmox VE comme cible de déploiement, son installation comprenant le redémarrage qu'elle exige
|
|
||||||
- Un menu QEMU : état et réparation du réseau, accélération 3D, un rapport de diagnostic, récupération de fichiers sur une VM qui ne démarre plus, virt-viewer et un tunnel de bureau distant
|
|
||||||
- Un tableau de bord d'installation et des formulaires Textual : déployer, suivre, mettre à jour, redémarrer ou effacer une VM sans quitter l'écran
|
|
||||||
- Une VM de développement mobile : PyCharm, Android Studio, un émulateur Android et un tunnel adb
|
|
||||||
- Un outil VPN, cinq technologies libres au menu, les secrets dans un coffre KeePassXC et un diagnostic qui nomme l'étage fautif
|
|
||||||
- La migration Odoo automatisée : l'outil mène toute l'exécution, revient à une étape, et répare ce qu'un changement de version laisse derrière lui
|
|
||||||
- La revue de migration : un verdict par étape, des tests de fumée sur chaque URL publique, et un contrôle du filestore
|
|
||||||
- Une trousse d'analyse en lecture seule d'une base Odoo, archive de sauvegarde comprise, avec conseil d'index PostgreSQL
|
|
||||||
- L'anonymisation d'une copie de production sans IA, et la duplication d'une base neutralisée pour de bon
|
|
||||||
- Des assistants de développement posés dans une VM, et un menu Git et Shell qui installe ce qu'un checkout réclame
|
|
||||||
- Une convention d'écriture pour ce qui reste dans git, tenue par un hook `pre-commit` et un hook `commit-msg`
|
|
||||||
- `long_test/` — des tests qui créent de vraies machines, QEMU et Proxmox imbriqués compris, hors du lanceur unitaire
|
|
||||||
- NTFY, Forgejo, un serveur git local, le courriel depuis le CLI, et la configuration SSH avec ProxyJump récursif
|
|
||||||
- L'interpréteur Python et l'installateur de paquets se choisissent, par EL_PYTHON_PROVIDER et EL_PIP_PROVIDER
|
|
||||||
- La politique d'IA générative de l'OCA, les agents et commandes Claude Code, et le contexte donné à un assistant, montré depuis le menu
|
|
||||||
- Des tests unitaires avec un plan de test bilingue, la télémétrie de navigation de TODO, et Odoo 18 qui lit les fichiers STL
|
|
||||||
|
|
||||||
## Modifié
|
|
||||||
|
|
||||||
- todo.py éclaté en neuf fichiers, un par sujet, avec un socle commun par formulaire
|
|
||||||
- Chaque entrée de menu porte une icône, les menus sont regroupés en sections, et une invite à compte à rebours laisse 15 secondes pour décider
|
|
||||||
- La branche, le profil, le type et le fuseau se choisissent par VM plutôt que globalement
|
|
||||||
- L'installation couvre Fedora, Debian, Ubuntu, Arch et openSUSE ; la synchronisation des dépôts et Poetry tournent en parallèle, silencieux sauf si EL_VERBOSE le demande
|
|
||||||
- Node.js 22 pour Capacitor 8, flanker pour Odoo 18, les extras CybroOdoo optionnels, et une dépendance Poetry déclinable par architecture
|
|
||||||
- Une VM démarre plus vite et prend le miroir joignable le plus rapide, les miroirs pacman canadiens passant en tête sur Arch
|
|
||||||
- L'indexation nomme les fichiers, jamais `git add -A`
|
|
||||||
- Entrée cible la version d'Odoo la plus élevée supportée, et un nom de VM perd le segment `latest`
|
|
||||||
|
|
||||||
## Corrigé
|
|
||||||
|
|
||||||
- Le réseau libvirt ne compte plus comme sa propre collision, ne laisse plus un hôte sans réseau au démarrage suivant, et son état est lu en anglais quelle que soit la locale
|
|
||||||
- Le déploiement QEMU : sudo dit pourquoi il faut un mot de passe, le groupe `libvirt` le remplace là où il suffit, aucun hôte ne redémarre sans qu'on le demande, et un disque orphelin ne bloque plus
|
|
||||||
- La migration : l'effacement de base, la vue account.root que laisse Odoo 17, les listes de prix qu'une réparation inventait, et les suppositions sur lesquelles tenait le passage de 13 à 18
|
|
||||||
- L'anonymisation respecte ce qu'une valeur signifie, et ne casse plus au-delà de la limite de 131 072 octets d'un seul argument
|
|
||||||
- L'installation sur Debian 13, Fedora, Ubuntu 26.04 et s390x : verrous apt, compilateurs et en-têtes manquants, mémoire trop courte, et ce qu'exigent un SWIG ou un PROJ récents
|
|
||||||
- Les secrets : le coffre KeePassXC s'ouvre sur un serveur sans tkinter, l'installateur forgejo cesse de réafficher le mot de passe qu'il a posé, et db_restore valide celui du maître
|
|
||||||
- Une question se voit avant qu'on y réponde, et un dépôt fautif n'emporte plus tout un lot
|
|
||||||
- Trois écrans qui tombaient, une réduction qui aurait rempli le disque, et un suivi qui mettait une VM à la poubelle avant d'en être sûr
|
|
||||||
- Le lanceur unitaire balaie tout le répertoire, là où il exécutait 1131 tests sur 3703
|
|
||||||
- Proxmox ne vise plus l'hôte au lieu de la VM
|
|
||||||
|
|
||||||
## Retiré
|
|
||||||
|
|
||||||
- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures
|
|
||||||
- Le contrôle de résidus qui jugeait cassée une langue dont `active` est NULL
|
|
||||||
|
|
||||||
## Sécurité
|
|
||||||
|
|
||||||
- Les mots de passe et jetons sont caviardés avant l'affichage, la journalisation ou le réaffichage d'une commande
|
|
||||||
- Le mot de passe maître d'Odoo et celui de KeePass quittent la ligne de commande, une variable d'environnement les portant à la place
|
|
||||||
|
|
||||||
|
|
||||||
## [1.7.0] - 2026-03-11
|
|
||||||
|
|
||||||
**Notes de migration**
|
**Notes de migration**
|
||||||
|
|
||||||
Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outil `make`.
|
Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outil `make`.
|
||||||
|
|
||||||
## Ajouté
|
## Ajouté
|
||||||
|
|
||||||
- Odoo 12.0 à 18.0 dans un même espace de travail, changés sans réinstaller : les manifestes, la configuration et les chemins d'addons suivent la version nommée dans `.odoo-version`
|
- Support de la migration de base de données et de modules Odoo avec TODO
|
||||||
- Le Python d'ERPLibre séparé de celui d'Odoo — `.venv.erplibre` porte les outils du dépôt, `.venv.odoo<version>` le serveur — si bien qu'un outil du dépôt ne dépend plus de l'interpréteur qu'impose une version d'Odoo
|
- Support du changement multi-version Odoo sur le même espace de travail
|
||||||
- L'auto-installation pilotée depuis TODO : le menu pose l'environnement dont il a besoin, Poetry, les manifestes Google Repo et les addons compris, au lieu d'afficher une commande à retaper
|
- Script pour le renforcement de la sécurité de l'installation
|
||||||
- La migration d'une base Odoo et de ses modules depuis TODO, `--neutralize` compris, avec la réparation du module mail que laisse un passage de PostgreSQL 17 à 18
|
- Support des versions Odoo 12.0 à 18.0
|
||||||
- Un script de renforcement de la sécurité de l'installation
|
- Séparation de l'installation Python ERPLibre de celle d'Odoo avec .venv.erplibre et .venv.odoo18
|
||||||
- L'application mobile ERPLibre Home : TODO la compile, la déploie, renomme le logiciel et change son image de menu
|
- Implémentation de l'auto-installation avec TODO.py
|
||||||
- Le générateur de code RobotLibre, avec les canaux queue_job que sa configuration réclame
|
- TODO affiche la documentation, télécharge la base de données, aide au formatage du code
|
||||||
- ERPLibre DevOps, et la procédure d'automatisation qu'il décrit
|
- Script de performance pour mesurer les requêtes par seconde
|
||||||
- La grille Selenium depuis `selenium_lib.py` : un coffre KeePass ouvert pour l'exécution, le téléchargement de fichiers, le mode sombre, l'enregistrement vidéo et une bibliothèque de scénarios
|
- Support de l'architecture Mainframe 390x
|
||||||
- Un script de performance qui mesure les requêtes par seconde qu'un site répond
|
- Déploiement avec Cloudflare et Nginx
|
||||||
- Le déploiement : DNS Cloudflare, nginx avec un certbot non interactif, gabarits Apache alignés sur ceux de nginx, et une unité systemd dont le répertoire de travail se configure
|
- Support de la configuration Apache comme Nginx
|
||||||
- L'architecture mainframe s390x
|
- Support du générateur de code RobotLibre
|
||||||
- Les addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor, et le dépôt design-themes
|
- Support d'ERPLibre DevOps, procédure d'automatisation DevOps
|
||||||
- Des commandes de sauvegarde et d'effacement de base, et un nommage plus clair à la restauration
|
- Application mobile ERPLibre Home, utiliser TODO pour compiler, déployer et personnaliser
|
||||||
- Une vérification de sécurité de l'environnement Python, depuis le menu
|
- Support de la grille Selenium depuis selenium_lib.py
|
||||||
- TODO affiche la documentation, télécharge une base et aide au formatage du code
|
- Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor
|
||||||
- Tuer un processus Odoo par le port qu'il occupe, depuis le menu
|
|
||||||
- CLAUDE.md et le document d'information des agents, pour qu'un assistant lise les conventions du dépôt au lieu de les deviner
|
|
||||||
- Une entrée de FAQ sur wkhtmltopdf pour les distributions récentes
|
|
||||||
|
|
||||||
## Modifié
|
## Modifié
|
||||||
|
|
||||||
- Odoo 18.0 devient la version par défaut d'un checkout
|
- Support Docker postgresql 18
|
||||||
- Docker passe à PostgreSQL 18, avec le client correspondant
|
- Script de formatage recherche les fichiers diff dans chaque dépôt
|
||||||
- La documentation est bilingue, générée par mmg depuis les sources `.base.md` : un `.md` ou `.fr.md` modifié directement est perdu à la prochaine génération
|
- Support de la neutralisation de base de données depuis Odoo
|
||||||
- Les menus TODO sont regroupés en sections, et le texte anglais sert de clé i18n plutôt qu'un code à part
|
|
||||||
- Le script de formatage cherche les fichiers modifiés dans chaque dépôt, addons cachés compris, et saute un dépôt qui n'est pas installé
|
|
||||||
- Odoo tourne sur une base personnalisée, et le menu configure queue_job comme la redirection SSH qu'une instance distante réclame
|
|
||||||
- Tuer un processus par son port demande confirmation, par un menu interactif
|
|
||||||
- La neutralisation d'une base passe par le `--neutralize` d'Odoo
|
|
||||||
- LinuxMint 22.3, Ubuntu 25.10, et macOS sans Python 3.7
|
|
||||||
- Dépendances Odoo 18 : tldextract, PyYAML, pdfminer.six, et cryptography à sa dernière version
|
|
||||||
- Une cible make lance les tests unitaires
|
|
||||||
- Le Makefile est éclaté : ses commandes vivent dans `conf/`, et `Common.Makefile` l'étend pour un projet à soi
|
|
||||||
|
|
||||||
## Corrigé
|
|
||||||
|
|
||||||
- Les accents de la documentation, et la génération markdown qui tourne en parallèle
|
|
||||||
- `git_tool` rend vide au lieu de lever là où `.git` est absent
|
|
||||||
- `poetry iscompatible` ne casse plus sur une version portant une lettre, une alpha ou une candidate
|
|
||||||
- pymssql se compile de nouveau, et les dépendances Odoo 18 laissent pyssql hors d'une installation de production
|
|
||||||
- wkhtmltopdf n'est plus proposé là où il n'existe pas : aucun paquet n'est publié pour s390x sur Ubuntu 25.10
|
|
||||||
- Selenium : le chemin du Firefox snap, un délai de 60 secondes pour atteindre un élément, l'exécution en fenêtre privée, et une connexion qui attend Odoo 18
|
|
||||||
- Le script de formatage ignore les fichiers et répertoires qu'il ne doit pas toucher
|
|
||||||
- `db_drop_all` exécute sa commande shell, et le traitement des sauvegardes garde les permissions de ce qu'il écrit
|
|
||||||
- TODO : le premier import, la régénération de `.repo/local_manifests`, le dialogue d'ouverture de base, et une version d'Odoo manquante signalée au lieu d'un plantage
|
|
||||||
- Le générateur de code : la création d'un projet, l'extraction d'une classe portant une sélection, et la lecture d'un modèle par le module `ast` de Python 3.11 plutôt que par astor
|
|
||||||
- Docker : la cible de compilation Odoo 18 en double, et le fichier compose épinglé sur une image qui fonctionne
|
|
||||||
|
|
||||||
|
|
||||||
## [1.6.0] - 2025-04-25
|
## [1.6.0] - 2025-04-25
|
||||||
|
|
@ -758,11 +491,7 @@ Parce que le dépôt d'addons a changé, le fichier de configuration doit être
|
||||||
- Support uniquement de python3.6 et python3.7, python3.8 cause des erreurs à l'exécution.
|
- Support uniquement de python3.6 et python3.7, python3.8 cause des erreurs à l'exécution.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/ERPLibre/ERPLibre/compare/v1.8.0...HEAD
|
[Unreleased]: https://github.com/ERPLibre/ERPLibre/compare/v1.6.0...HEAD
|
||||||
|
|
||||||
[1.8.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.7.0...v1.8.0
|
|
||||||
|
|
||||||
[1.7.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.6.0...v1.7.0
|
|
||||||
|
|
||||||
[1.6.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.5.0...v1.6.0
|
[1.6.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.5.0...v1.6.0
|
||||||
|
|
||||||
|
|
|
||||||
311
CHANGELOG.md
311
CHANGELOG.md
|
|
@ -9,301 +9,34 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Site presets for the VPN: one `.json` carries a site's gateway, protocol and connection group, and neither a username nor a secret, so it can be handed around. Read from `conf/vpn_presets/`, then from a git-ignored `private/vpn/presets/`, then from any directory listed in `vpn_preset_paths`; on the same identifier the latest wins, so a site fixes a shipped template without touching a tracked file
|
|
||||||
- Import a Cisco AnyConnect `.xml` profile from the menu, browsing the client's own directories or typing the path, and get a preset from its `HostName`, `HostAddress` and `UserGroup`
|
|
||||||
- OpenConnect tells apart the two mechanisms that designate a service on one concentrator: the connection group in the URL and the value picked from a dropdown. Confusing them hands over another service's login form, so correct credentials are refused with nothing naming the group
|
|
||||||
- Reach a gateway that demands an embedded browser for SAML, which stops OpenConnect on « No SSO handler »: the web step is delegated to an `openconnect-sso` helper that the installer offers to set up, and the tunnel is then brought up by the driver itself, so the interface name, the state files and the diagnosis stay with the profile
|
|
||||||
- Declare a concentrator that compares only the first characters of a password: it is announced before the secret is stored, and nothing is ever truncated
|
|
||||||
- The VPN installer looks for `vpnc-script` — a file, not a binary on the `PATH` — and names the package to install per distribution family, instead of letting the tunnel fail on an interface that never appears
|
|
||||||
- A command launched by the VPN runner gets `/dev/null` on standard input, so a captured-output command can no longer be stopped by a SIGTTOU and freeze the machine's package manager
|
|
||||||
- The VPN profile list marks which profiles carry a live tunnel, so connecting one already up asks first and disconnecting one already down says so instead of looking like a mistake. A profile is judged on its interface, not on the state file a tunnel killed without `down` leaves behind — a state left over used to be reported as mounted on the same screen that declared the process gone
|
|
||||||
- `Assistant › LLM` — ask a model server over several turns, local or remote. The history lives in memory and dies with the menu, `/save` being the only way to keep a trace; every command carries a leading slash, so a question pasted over several lines stays one turn. A third-party destination has to be retyped before the first send
|
|
||||||
- Server recognition across eleven ports and twelve families, identity read from the response BODY and never from the port: one port hosts up to three products, and one family re-serves another's whole native API
|
|
||||||
- Finding a server from six sources — the loopback, this machine's QEMU domains, the hosts of `~/.ssh/config`, an address or a network typed by hand, and the networks read over SSH on another machine. Anything wider than a `/24` is refused before enumeration, and two preferences bound the sweep: `assistant_sweep_workers`, `assistant_sweep_timeout`
|
|
||||||
- A gpt catalogue in `script/todo/assistant/gpt/`: one Markdown file per tool, whose declared requirements are matched against what the server announces. An unknown never greys a tool out — only a requirement contradicted by a field actually read does, with the figure that refuses it
|
|
||||||
- A declared READ-ONLY context per tool, files and allowlisted commands, shown and confirmed before the first send, capped in size and duration, and scanned for identifying data. The scan's honest limit is stated: it sees addresses, e-mails and account paths, not names
|
|
||||||
- `Execute › GPT code › Claude Code` — list the machine's sessions, ask one a question with read-only tools, or resume one in its own terminal. A copy is branched by default, since two writers on one session lose a branch
|
|
||||||
- `check_comment_hygiene.py` signals a fully qualified machine name in a comment, as a re-read signal and never a finding: a BARE host name is mechanically indistinguishable from an ordinary word, so the absence of a signal proves nothing about names. The copyright header, the RFC 2606 domains and any address carried by a URL are left alone
|
|
||||||
- Transform data, a menu entry that opens an external file — Excel, Access, CSV, XML or JSON — reports what it holds, then draws an anonymised copy of it. The original is never touched. Numbers are drawn from the measured extent of their own column rather than a fixed 0-1000 range, which turns a tax rate into 743 and a year into 12; they are drawn without replacement, since a hundred distinct keys in a range of a hundred otherwise yield sixty-six outputs and a fixture that no longer reimports. Text takes a French word from a local dictionary of 1366, assigned by a counter rather than a draw, and a portable mapping table gives the same client the same word across a whole batch
|
|
||||||
- Eleven structural labels are left alone on their NAME — `id`, `create_uid`, `sequence`, `active` and the rest, plus anything ending in `/id` or `/.id` — since a test set has to keep working. Every OTHER column is judged on its measured CONTENT, never on the strength of its name. In an import-ready export, `partner_id` holds the relation's display name and `display_name` is the data itself, so deciding on the label copied the most identifying columns of the file verbatim while announcing them as protected
|
|
||||||
- The anonymiser rereads the bytes it has just written and refuses the copy when a value it announced as replaced survives in it. A workbook holds data in a dozen places that are not cells: a pivot cache and an external link each carry an entire copy of the source, a chart caches its categories and its axis titles, a custom number format carries a label, a comment carries its author, a cell hyperlink carries a path. The guard depends on no list of those, so a place nobody thought to clean produces a refusal instead of a silence — and what stays by decision, a header row or a column left alone, is named on screen before anything is written
|
|
||||||
- Transform data anonymises an Odoo database too, the entry asking for the SOURCE rather than the format: an external file, a live database, or a backup zip taken from `image_db/`. A zip is never modified — it is restored into a database, anonymised there, then dumped into a NEW zip by Odoo's own backup, so the dump, the filestore and the manifest are written by the software that will read them back. A live database is modified ITSELF, which is said before the work and not after, and nothing is drawn when the write did not happen: declining otherwise handed back a zip of the untouched database, announced as anonymised. A register records which databases the entry produced, since the server does not
|
|
||||||
- An anonymised number can keep its digit count, on the file anonymiser and on `anonymize.py` alike (`--keep-digits`): 8839 then draws in 1000..9999, so the copy keeps columns of the same width, which an export read by eye or imported into a bounded field asks for. Each value keeps ITS own width and not its column's — 7 stays at one digit where 12 keeps two — and below the unit the rule does not apply, 0.15 having no digit before the point. The option trades one guarantee for another: the measured extent no longer bounds anything, so a rate or a year can leave its range, and the preview SAYS so rather than letting it be found at reimport. Uniqueness is what yields when a band fills up, a duplicate of the same width beating a value of a different one, and the band never exceeds what the type RECEIVING the draw holds, which is not always the column's: an Odoo `integer` over a column PostgreSQL did not create as an integer — what an upgraded base carries — is poured into `numeric`, which has no ceiling, where `integer` stops at 2 147 483 647 and a draw past it aborts the whole write, that write being one transaction
|
|
||||||
- The anonymiser asks four questions instead of eleven, seven of the eleven having had an obvious default. Answering them one by one to arrive at the same place makes prompts be passed by reflex, and a prompt passed by reflex consents to nothing; they now live behind « Advanced options? ». The short path DECLARES what it takes, in sentences rather than in yes/no, since those are decisions taken and not questions whose answer was lost. Macros and charts are asked on both paths: they are not visible in the cells
|
|
||||||
- `markdown-it-py` is declared as a functional dependency: it renders the language model's markdown to HTML, before sanitising, on the assistant page. It reached the environment transitively through `bandit` → `rich`, a lint tool, so removing a lint dependency removed a portal page's rendering engine
|
|
||||||
- A download cache shared by the QEMU VMs of a host, installed from **Deployment › QEMU cache**. Two VMs of the same distribution stop pulling the same hundreds of megabytes twice: a package file is served from disk, while an index is always taken from upstream, so a withdrawn package can never turn into a « failed retrieving file … 404 ». An index is stored all the same and only comes back out when upstream is unreachable, which is what makes an offline deployment possible. The cache never shrinks by itself: `--status` says what it occupies
|
|
||||||
- Interception is transparent and covers the whole host bridge, so a VM cannot opt out from the inside. Every VM trusts the cache's authority as long as the service runs. To take ONE machine out, tick « keep this VM out of the download cache » when deploying: its MAC address is fixed before creation and an exception is posted on the host. To take them all out, stop the service — its rules leave with it. A Proxmox host that is itself a VM here is no exception: the machines it carries come out behind its address, so the deployment poses the authority in each of them, by ssh, before installing anything
|
|
||||||
- The measurement is not limited to Arch: any catalogue system whose package family is known, and either a batch of packages or the real install of ERPLibre and Odoo 18. Measured on Ubuntu 24.04 with that real install, the second VM pulled **zero byte** of package from the network and finished 19 % faster. Verified on the seven systems of the catalogue
|
|
||||||
- Git is MIRRORED rather than cached: its protocol is a negotiation, the server computing its answer from what the client already holds, so no answer is reusable. A bare mirror per upstream repository is kept on the host and served locally, and a mirror already held serves with no network at all — measured, a repository cloned inside a VM while the cache's upstream was cut. Measured on a full ERPLibre install: the second VM pulled **zero** git request from upstream, where git had been four fifths of the traffic. **Deployment › QEMU cache › Git mirrors** fills them ahead from the manifests, so the first VM does not pay every clone
|
|
||||||
- An offline deployment also covers the base suite of a cloud image. Its apt REVALIDATES the index it ships instead of downloading it, upstream answers 304, and the cache had no body to keep — so that one suite, and only it, was missing offline while its `-updates` and `-security` neighbours came off the disk. A conditional request for something the cache does not hold now goes upstream without its condition, once, and a client that holds its own copy gets 304 rather than 504 when upstream is mute.
|
|
||||||
- What an offline deployment covers, exactly: packages, repository indexes and git. NOT what the cache is forbidden to decrypt — a client carrying its own trust store, npm and poetry among them, is tunnelled opaquely, and a tunnel carries nothing once the network is gone. A full ERPLibre install therefore still needs the network, though everything the cache holds is served from disk
|
|
||||||
- A mirror is COMPLETE where `repo sync` clones at depth one, so it costs tens of gigabytes. Below ten gigabytes free, no new mirror is created and the request goes back upstream. The diagnosis says what the objects and the mirrors each occupy
|
|
||||||
- **Deployment › QEMU cache › Age and cleanup** groups the cache by age of last use (day, week or month; objects and git repositories apart) and gives back what has not served for a chosen delay, or everything. A served object has its date renewed, so « old » means « no longer used ». Both cleanups say what would go before erasing anything, and entry 5 lists the mirrors heaviest first to remove one
|
|
||||||
- `long_test/qemu_cache.py` measures whether the cache really serves the second VM, and `--hors-ligne` cuts the upstream of the cache service alone to prove a third VM still builds from the stored index
|
|
||||||
- Before cutting, the form says whether the cache holds the base suite of each system asked for: F5 warns « cache holds nothing for ubuntu 26.04 » and a second F5 goes ahead anyway. The verdict is given only where a release is named unambiguously in the URL — the apt families — rather than reassuring wrongly elsewhere. The verdict is given component by component — `main`, `universe`, `restricted`, `multiverse`, on the base suite and on its `-updates` and `-security` companions — because a single stored URL used to silence it while a whole component was missing, and apt only said so twenty minutes later, by « Unable to locate package »
|
|
||||||
- **Deployment › QEMU cache › Copy it to another machine** carries the store to another ERPLibre host in a single stream — `tar`, compressed, over ssh, no intermediate file for tens of gigabytes — and hands the files to the service account on arrival, the same account rarely bearing the same number on two machines. Only the STORE travels: an object is keyed by its URL and never by the machine that fetched it, and a git mirror is a repository. The settings stay behind — bridge, subnet and authority belong to the host, and entry 1 poses them there
|
|
||||||
- The deployment form carries a **Network** section with « No internet connection »: for the whole deployment, install included, the cache service loses its way out AND the VMs lose theirs — ping, other ports, UDP, IPv6 — so a step that takes any path but the cache fails instead of quietly succeeding online. Names no longer resolve through the internet either: the host answers every name itself with an address the cache intercepts, so only what the cache holds can be reached; dnsmasq must be installed on the host. A VM behind the cache pulls from one fixed apt mirror: cloud-init's mirror search discards every mirror behind a resolver that answers every name, and would fall back on another mirror than the one the previous VMs filled the cache from. The cut refuses rather than drops, so an address the cache does not hold answers 504 at once instead of after a connection timeout; a kernel that cannot load the reject module gets the dropping set instead. Offered only where the cache runs, and the deployment refuses rather than run with the upstream still up, a VM built that way succeeding for the wrong reason. **Proxmox VE** carries the same box, offered where its host is itself a VM of this bridge: its guests come out behind its address, so the local cut covers them and nothing is posted on the remote host. A Proxmox host that does not live here is never offered it — nothing here can cut its way out
|
|
||||||
- A Proxmox VE deployment can ask for **3D acceleration**. The VM is created with an accelerated screen (`--vga virtio-gl`) rather than the serial console as its display, and the account joins the GPU groups INSIDE the guest: the render node belongs to `root:render`, so without them every GL application falls back to software rendering while the VIRGL negotiation reports success, and nothing says so. The box is offered only where the remote host has a render node AND the three libraries Proxmox loads for that screen — VIRGL, GL and EGL. It names the missing ones and refuses to start the machine, once its disk is written and its configuration posted, so a host carrying GL without EGL would fail at the very last step. Where a piece is missing the form says which, and the package to install on the host: a box that vanishes without a word reads as a regression. A button installs them without leaving the screen — the form hands the terminal back so sudo can ask for a password and apt can be watched — then reads the host again rather than taking apt's word for it, and the box appears in place of the message. The serial port stays posted, so `qm terminal` keeps working
|
|
||||||
- Offline, the cache replays what an online pass saw, not only 200 bodies: redirects, definitive refusals (404/410) and HEAD answers of volatile URLs are kept without a body, under keys of their own, and served ONLY when upstream is mute. A TUF client probing the next version of its root gets the 404 it expects instead of a 504, so mise installs Python offline with its Sigstore verification intact; GNOME extensions, Claude Code's installer and rtk's version lookup follow their redirects offline
|
|
||||||
- The offline cut ends when the last installation ends, not when the monitor closes: a root unit, handed the lift at launch, waits for every installation's exit marker and survives the monitor closed early, todo.py killed or the terminal gone — 12 h at most. The monitor is therefore required while offline. A second offline deployment is refused while one runs, and an online deployment started meanwhile is told it would run offline
|
|
||||||
- F5 also reads the previous offline runs of the same VM and warns « at least N addresses were missing », minus what the store holds now (`erplibre_go_qemu_cache --detient`, read-only, no root). **Deployment › QEMU cache › Fill what offline runs lacked** replays them online, through the cache
|
|
||||||
- The install log names the commit the VM runs; offline, the recap says, branch by branch, which commit the cache's mirror will give
|
|
||||||
- `long_test/qemu_cache.py --distro tous` (or a comma list) chains one campaign per catalogue system, destroys each system's VMs before the next, and ends on a table of verdict, durations and upstream bytes. A failure does not stop the series
|
|
||||||
- The QEMU download cache can clean itself up every day: by age (`EL_PURGE_AGE`, e.g. `90j`) and by size ceiling (`EL_MAX_SIZE`, e.g. `50G`, the least recently served going first, objects and git mirrors alike). Both are off by default and set from **Deployment › QEMU cache › Automatic cleanup**, which previews what would go; `--purge-to-size` runs the ceiling by hand. A reinstall keeps the values chosen
|
|
||||||
- NixOS 25.11 as a deployable system, alongside the eight others. It is the only one no distribution publishes a cloud image for: the image is rebuilt by a third party, so its release is pinned, its sha256 verified at every download, and its origin printed before anything is created. cloud-init receives no network configuration there — its networkd renderer takes the key of a block as an interface NAME, where netplan honours a `match:` — and the account is created with `/bin/sh`, sshd refusing an account whose shell does not exist
|
|
||||||
- ERPLibre installed on NixOS, its dependencies DECLARED in `conf/nixos/erplibre.nix` and applied by `nixos-rebuild`, where the four other families install them one command at a time. `services.envfs` answers `/usr/bin/env` and `programs.nix-ld` gives manylinux wheels the dynamic loader they ask for; the headers of what has no wheel are linked into the system profile, and its libraries reach the loader too, a module compiled on the machine carrying no RPATH. The install reaches it from the menu like any other distribution: the bootstrap poses git, make and python3 — absent from that image — in the user profile for the clone, and « make install_os » redeclares them for the system. Checked on a VM: the 362-package lock installs and Odoo answers over HTTP
|
|
||||||
- A `nix + nixos-anywhere` option on the OTHER distributions, the reverse of picking NixOS: it leaves an ordinary VM able to install NixOS onto any machine reachable over SSH. Nix is called by absolute path, the remote shell's PATH being frozen before the installer drops the binary, and the flake features are repeated on the command line, writing `nix.conf` needing a sudo that may fail
|
|
||||||
- NixOS deploys on a **Proxmox** host as it does on QEMU/KVM, which took three fixes no code reading could have found. Its image has no BIOS boot sector: a VM created in SeaBIOS reports « running » with a silent console, so the catalogue now marks which images need UEFI — one marker per distribution, since Debian 13 boots in SeaBIOS on that same host. `--ciuser` and `--sshkeys` defer to the image's DEFAULT account, which NixOS declares as another name: the repository's own cloud-config now travels as a snippet for every distribution, carrying an explicit `users:` block. And the cloud-init drive moves to the SCSI bus — an image built for virtio alone has no ATA driver and never sees an IDE drive, so cloud-init hunted network datasources and the VM came up with no account at all. Checked end to end: NixOS answers with `/bin/sh`, Debian 13 with `/bin/bash`, sudo on both
|
|
||||||
- The third-party image is verified against the sum the repository pins for it on the Proxmox path too, where a bare `wget` used to be enough. One accessor carries that sum for both paths, and the check runs even on a cached image — the case aimed at is a file substituted or truncated between deployments, which a presence test cannot see
|
|
||||||
- One entry of the download cache can be removed, by URL. A checksum that does not match invalidated nothing: the store kept serving the same bytes, and re-downloading changed nothing since the store is what answers. Neither existing purge reaches it — `--purge` erases everything, and `--purge-older-than` skips what is recent while every service resets that date, so a poisoned object that keeps being served never ages. `--oublie` is symmetric to `--detient`: same input lines, same key functions, same refusals, and a present object that resists is reported as a refusal rather than a forget. Both checksum failures now name it, with the exact URL
|
|
||||||
- The install log carries what the host decided before launching: the download cache authority placed or refused, the bypass, the mirror. Those lines were said on a console that scrolls away, while the file reopened after a failure held only the symptom — on a guest with no trust store, a refused certificate, hundreds of derivations to build and six hundred lines of errors, without a word on the cause
|
|
||||||
- One entry of the download cache can be forgotten from the menu, under « Age and cleanup ». The binary could already do it; the menu offered only the two bulk purges, neither of which reaches a single object — « erase what has not served » never reaches one the service rejuvenates each time it serves it, and « erase everything » costs the whole cache for one file. `--detient` runs first and is the preview: same line, same key, nothing modified
|
|
||||||
- `erplibre_go_qemu_cache --recle` stores a cache's objects again under the current key, without downloading anything, and merges the copies a mirror carried under several paths. Objects written under the former key rule stay on disk but become UNREACHABLE, so the service asks upstream for them again and the space they hold serves no one: on a store of 12 764 objects, 5 419 were in that case — 9.11 GiB — and merging the duplicates returned about 3.37 GiB. The service must be stopped, the body being renamed before its meta, and `--dry-run` only counts what would move. A status-only entry is left alone, its key carrying the host rather than the path
|
|
||||||
- The `pre-commit` hook runs `check_python_version.py` on staged files: it reports source that does not parse under the Python of `conf/python-erplibre-version`, without blocking the commit, and says when no such interpreter was there to check. Neither black nor flake8 sees that fault — black's target bounds what it writes, never what it accepts
|
|
||||||
- `Deploy › Local › [4]` opens a SOCKS proxy over SSH — `ssh -D`, port 1080 by default — so the browser reaches, FROM the remote machine, an interface listening only on its loopback or a host of its network. The address comes from `~/.ssh/config` or by hand; an alias is passed to ssh as is, so its `ProxyJump` still applies and a nested VM stays reachable. The Firefox settings print before the tunnel opens, the command only returning on Ctrl+C
|
|
||||||
- `make format_test` formats `test/` and `long_test/`, which no target covered: 75 files out of 210 followed no standard, and only a file a diff reported was ever touched
|
|
||||||
- `script/reverse_proxy/main.py` — a development reverse proxy for Odoo 18 without nginx: one address sends pages to the web port and `/websocket` to the bus port, with the `X-Forwarded-*` headers of `proxy_mode` set by the proxy alone (`--trust-forwarded` extends a chain instead, behind another proxy). Only the request head is read, so an upgraded WebSocket stays open and a gzip or chunked response passes byte for byte. It serves HTTPS with `--tls-cert`/`--tls-key`, logs one line per request (route, status, duration; `--quiet` silences it), says at start which Odoo port does not answer, and names that port in a 502. A request head waits 30 s (408) and the connection to Odoo 10 s (504), never an open WebSocket. Listens on `127.0.0.1:8080` by default, `--listen 0.0.0.0` opens it to the network; one request per connection, production keeps nginx
|
|
||||||
- `script/reverse_proxy/local_cert.py` issues a local authority, imported once in the browser, and a server certificate it signs for localhost, the host name and its addresses, under `~/.erplibre/reverse_proxy_tls/` with keys in 0600; the authority is kept when the certificate is issued again
|
|
||||||
- `TODO › Execute › Network › Odoo reverse proxy` starts it with the web and bus ports read from `config.conf`, asks for this machine only or the whole network and for HTTP or HTTPS, issues the local certificate on first HTTPS use, and warns when `proxy_mode` is off or `workers` is 0 — without a worker no bus port listens and `/websocket` fails. `Network › Local TLS certificates` issues it again with extra names
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- `Assistant › [1]` no longer sends every question to a single remote API on a fixed model: it asks whichever server is configured, and falls back to the remote one only when no local server answers
|
|
||||||
- The comment hygiene check reads Go comments, not only `#` ones: `//` outside a string, the raw string between backticks, and `/* … */` blocks
|
|
||||||
- Debian and Ubuntu `by-hash` index files are served from disk, their name being the digest of their content; `…/releases/latest/download/…` is no longer pinned to the first version seen
|
|
||||||
- An upstream that refuses or drops connections is remembered for 20 s: a request with a stored answer is served at once instead of waiting its connect timeout, which made up most of an offline install's time; a request with nothing stored still tries upstream. A git mirror skips its refresh while its forge is unreachable
|
|
||||||
- Mirror prefetch runs under the cache's service account, never as root
|
|
||||||
- The long-test menu asks before creating real machines, and asks again, in words of its own, before `--detruire` removes machines with their disks. The command is shown first, which is what makes the question answerable; a dry run or a performance report creates nothing and asks nothing
|
|
||||||
- Every entry of the Proxmox VE menu carries an icon, the same picture meaning the same action as in the other menus of the tool
|
|
||||||
- The QEMU cache binary speaks English or French: service journal, `--status`, `--age`, option help and the error served to a VM. The language comes from `--lang`, then `EL_LANG`, then French; the installer writes `EL_LANG` to the service settings and the TODO menu passes its own. Rules, verdict codes and JSON keys are never translated
|
|
||||||
- A repository index the cache already holds is revalidated with its ETag rather than downloaded again: upstream still judges every request, and a « 304 » serves the stored body from disk. On a full ERPLibre install the pip indexes, npm metadata and repo bundle had been about 110 MB per VM, taken whole each time. An index stored without its host — shared by every mirror of a rotating list — and an answer carrying no ETag are taken whole as before; the access log names the new outcome `revalidated`
|
|
||||||
- A registry page served under `Vary: Accept` keeps one copy per representation. npm asks for the same `/npm` page abridged, then complete, then abridged again; kept under one key they replaced each other and the 31 MB were fetched on every install. Each representation is now revalidated and, offline, served on its own; `--detient` still reads the page under its URL alone
|
|
||||||
- A VM deployed with the cache upstream cut — QEMU form, Proxmox VE, or `deploy_qemu.py --offline` — has npm's security audit turned off (`NPM_CONFIG_AUDIT=false`): it queries a remote service no cache can replay, and failed on every offline install. An online VM keeps its audit
|
|
||||||
- Verifying a downloaded image no longer needs `--verify`: it runs by default for every distribution that publishes a sum, and `--no-verify` is what skips it — to be kept for offline runs, where a substituted image would otherwise pass unremarked
|
|
||||||
- `--bios` is refused on an image with no BIOS boot sector, and says why. Forced there, it gave a VM reported « running » with a silent console — the very failure that flag exists to avoid elsewhere
|
|
||||||
- The tooling virtual environment `.venv.erplibre` runs Python 3.14.7, independently of the Odoo one (3.12.10 for Odoo 18.0). `install_erplibre.sh` builds it through `install_venv.sh` and `EL_PYTHON_PROVIDER` instead of the system `python3`
|
|
||||||
- `.venv.erplibre` on an incompatible Python is DELETED and rebuilt; whatever was installed in it by hand goes with it. Odoo's venv is kept when it merely differs in version, and rebuilt only when it is unusable: rebuilding it redoes a whole Poetry install. A directory without `pyvenv.cfg` is never deleted
|
|
||||||
- `make` and `make todo` run `./todo.sh`, a launcher whose name says a menu opens and not an installation; it hands every argument to `install.sh`, which picks an interpreter able to READ the code before running it: `.venv.erplibre` on the right version, else a recent enough system `python3`, else the install, which runs only on a yes typed in a terminal (`o`, `oui`, `y`, `yes`) since it can delete `.venv.erplibre`. A system older than `conf/python-erplibre-version` would otherwise stop on a syntax error raised before any guard could name the command to type. TODO then relaunches itself in `.venv.erplibre`, or offers to run `install_erplibre.sh` in a terminal
|
|
||||||
- The production Docker image builds `.venv.erplibre` on Odoo's Python and stops when that Python cannot parse `script/`
|
|
||||||
- Debian 11 is dropped from the deployment catalogue: its LTS ended, and its security suite is neither served nor archived — the index it still publishes names packages whose pool no longer holds the file, so apt stops before installing git. Debian 13 takes its place, its cloud image always being the latest point release
|
|
||||||
- Every container image is built on bookworm, whatever the Odoo version. The base is `python:<version>-slim-<suite>`: the interpreter comes from the official image, never from Debian, and the bookworm variants exist down to 3.7.17. The wkhtmltopdf build follows the suite — bullseye's requires libssl1.1, absent from bookworm
|
|
||||||
- The PATCH bounds only Odoo's venv, whose pyproject requires `>=3.12.10,<3.13`. For the tooling one, the major.minor is enough: requiring the patch turned away a distribution's Python one step behind — NixOS 25.11 ships 3.14.2 where conf asks 3.14.7 — and made pyenv COMPILE CPython for a difference nothing needs
|
|
||||||
- `make format` picks the formatter from each file's context: an Odoo module keeps isort and black on `py37`, the series still supported going down that far, while this repository's own tooling goes through ruff, configured once in `.ruff.toml`. ruff follows CPython's versions, where black 24.8.0 stops at `py313`, and its import sorting replaces isort; it is also what the OCA standard uses since it left black
|
|
||||||
- The repositories that Google Repo checks out under `script/` are excluded from that formatting, a named path included: reformatting them would write in someone else's history. `target-version` stays at `py310` there, because the git hooks carry `#!/usr/bin/env python3` and a distribution still ships 3.10 — from 3.14 on, ruff would write `except A, B:` without parentheses
|
|
||||||
- The list of Claude Code commands in `TODO › Execute › GPT code › Claude configs` compares each ERPLibre template in `conf/` with its copy in `~/.claude/commands/`: a command not installed is shown, an outdated copy is marked with its count of added and removed lines, and a command that comes from elsewhere is labelled as such. On a yes it prints the diff, then redeploys the outdated copies, keeping the git name and e-mail `/commit` carried
|
|
||||||
- A commit message opens on an English subject and an English body; `--- FR ---` then opens the French section, which starts with the subject translated under the same tag. The `commit-msg` hook refuses a `--- EN ---` marker and a French section without that title, and checks the title like a subject. `/commit` and `/git_prepare_merge` follow the same order
|
|
||||||
- TODO menus: the language is set only from Configuration, the duplicate entry in Execute is gone, and Fork moves from the main menu to Configuration. The main menu now numbers Telemetry 4 and Configuration 5. The language chooser shows a flag per language
|
|
||||||
- Odoo 18 dependencies refreshed. `openai` is pinned to 2.x, whose 3.x requires an `idna` that Odoo 18 forbids; `fsspec` is pinned beside `s3fs`, which demands it at its own exact version, so the two move together; `meteostat` returns to 1.x, every 2.x capping `pytz` below 2024. PyMuPDF stays excluded on s390x, now declared in the requirements so a regeneration keeps it. Major bumps of `ujson` 6, `plotly` 7, `python-slugify` 9 and `sqlalchemy` 2.1 are not yet tested
|
|
||||||
- Dependabot ignores the major versions of `meteostat`
|
|
||||||
- Odoo 18 moves to pandas 3.0.6, cryptography 50 with pyopenssl 26.4, Pillow 12.3 and botocore/boto3 1.43.75 with aiobotocore 3.9.1, the highest botocore its narrow range accepts. The seven modules that import pandas run their pandas calls unchanged; `freq='d'` in a Cybro attendance dashboard now warns and will break with pandas 4
|
|
||||||
- Dependabot groups `aiobotocore`, `botocore` and `boto3` into one pull request, since each aiobotocore accepts only a narrow botocore range; security fixes still arrive on their own
|
|
||||||
- `TODO › Transform data` reads Excel with openpyxl 3.1.5 and xlsxwriter 3.2.9; the leak test that guards openpyxl's exact pin passes on them
|
|
||||||
- factur-x requires 6.8 outside s390x, the version already locked, so a regeneration can no longer fall back to an untested 4.x or 5.x
|
|
||||||
- The interface chooser of the QEMU deployment and of the Odoo migration, and its preferences in `TODO › Configuration`, mark the TUI form with 📋 and the line by line questions with 💬
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- Reading the dnsmasq leases no longer opens a root password prompt: the files are read directly, which suffices on a standard install where they are 0644, and only then is `sudo -n` tried, which fails instead of asking. Displaying a VM list called that path once per VM, and waiting on a VM called it every three seconds for ten minutes
|
|
||||||
- `--max_process` runs again on Python 3.10 and later: `loop=` left `asyncio.wait` in 3.10 and `asyncio.get_event_loop()` raises outside a running loop since 3.14, so the pool was not even constructible while the help still advertised the flag
|
|
||||||
- A prompt no longer writes its colon twice — the most-seen menu of the software asked « Command:: », and seven remote-deployment prompts showed a colon followed by another
|
|
||||||
- Eleven submenus now leave their segment in the breadcrumb, and navigation telemetry stops filing them as commands under their raw method name
|
|
||||||
- The three readers of `~/.ssh/config` agree on what a machine name is: an alias declared with a lowercase `host` is seen, a tab separates as legally as a space, and a negated `!name` pattern is no longer taken for a machine to connect to
|
|
||||||
- Three translation keys declared twice are gone, and a check refuses the next one: a repeated key silently overwrites the previous, which had already cost a menu label
|
|
||||||
- `anonymize.py` abstains from a unique numeric column and NAMES it in the report. A draw guarantees nothing on a unique column, and a number has no way out where text has one — appending the id carries uniqueness for text, but would change a number's magnitude. Two rows drawing the same number failed the UPDATE and, the whole run being one transaction, took the entire anonymisation with them. Without the report naming it, an identifying column stays in the clear with nothing saying so
|
|
||||||
- Restoring a backup no longer raises `AttributeError` before running the command: `_monitoring_restore` read `self._execute` where `TODO` sets `self.execute`, and both entries leading there — the local backup and the remote one — were broken. A check refuses the next one: every `self.X` that `TODO` READS must be set by TODO or by one of its bases. Searching the name across `script/todo/` did not see the fault, another object of the package setting `_execute`; it has to be searched in the classes TODO INHERITS from
|
|
||||||
- The Selenium scripts run on a Python built without `tkinter` — any server with no graphical toolkit package. That module is only needed by the vault's file picker, so it is now optional: with no `tkinter` and no configured KDBX path, opening the vault logs an error and returns, instead of breaking the import of every browser-automation script
|
|
||||||
- Dark mode works again in a private window on a current Firefox. The « Run in Private Windows » permission is granted when the addon is installed, through the `allowPrivateBrowsing` field, rather than clicked through the `about:addons` interface: Firefox refuses navigation to `about:addons` from the content context, and the chrome context demands `-remote-allow-system-access`, which geckodriver rejects through capabilities, so both routes to that checkbox are closed. A geckodriver that ignores the field installs without the permission instead of killing the session
|
|
||||||
- The « - Default » label appears again at the version and environment menus: both reads asked for a capitalised key the version file never writes, and a missing key returns nothing without a word
|
|
||||||
- The cache's « nothing in store » message is inert as a shell script, every line being a comment. Fed to an installer built on `curl … | bash`, it used to become a cascade of « command not found » that hid the real cause. Such a download now also asks curl to fail on an HTTP error rather than execute the error page
|
|
||||||
- The wait for a VM to be ready now covers the guest-agent install too. That one is launched as a DETACHED unit so cloud-init returns in seconds, and it runs an `apt-get update`: `cloud-init status --wait` said « done » while the package lock was still held, the next step burnt through its retries, and the install then ran on an index never refreshed — « Unable to locate package », a message that blames the repository rather than the lock. An update that never succeeds now says so on the spot
|
|
||||||
- A desktop install no longer waits minutes on the apt lock: the apt-daily SERVICE is stopped and not only its timer, a timer being disabled without interrupting the apt-get it already started; and the retry comes back every two seconds rather than every ten, `DPkg::Lock::Timeout` not covering the list lock at all
|
|
||||||
- Fedora VMs boot again: the firmware loads and starts their loader, then freezes without writing a byte — no console, no DHCP lease, a machine "running" that does nothing. Fedora is booted in legacy BIOS, where the same image starts its kernel; `--bios` still wins when asked
|
|
||||||
- A VM receives a hostname it can accept — an underscore, which a libvirt domain name tolerates, made it keep its image's generic name — and a timezone its own distribution knows, a legacy alias having left it in UTC
|
|
||||||
- starship installs in a VM: its installer runs as root, bounded by a root timeout, so it never reaches the `sudo -v` that sudo-rs refuses; its shell hook no longer prints « command not found », nor fails a sourced rc, when starship is absent
|
|
||||||
- Each optional tool says whether it was installed, and a GNOME extension whose download failed is no longer reported as unavailable for this GNOME
|
|
||||||
- mise, pyenv and GNOME extensions are downloaded, then run: without pipefail, `curl | sh` could neither report a failed download nor reach its fallback
|
|
||||||
- The guest-agent unit no longer ends in failure after a successful install
|
|
||||||
- The cache answers 508 to a request that targets the cache itself, instead of calling itself until it runs out of descriptors
|
|
||||||
- The cache diagnosis no longer reports a stopped service as running
|
|
||||||
- The wait for `apt-get update` is bounded by a deadline rather than by a number of attempts. An attempt fails in under a second on a held lock, but takes minutes when the cache answers 504 on every index it does not hold: sixty attempts were then worth hours of silence where five minutes were promised, and the install went on to fail on unmet dependencies
|
|
||||||
- A Proxmox guest is pinned to the same apt mirror as the rest of the fleet, written over ssh before anything downloads. The store keys its indexes by HOST, so a VM left on the default repositories of its image found none of what the cache had been filled with — offline, every one of those indexes was missing
|
|
||||||
- The cache installer accepts a bridge and a subnet given by hand. It probed libvirt first and died on « network default not found », so the workaround its own header advertised — `EL_BRIDGE` and `EL_SUBNET` — could never be reached. A machine whose libvirt network is not started, or which carries its bridge otherwise, can now lay the cache down by naming it; and when the probe does run and fails, it names both ways out
|
|
||||||
- **Deployment › QEMU cache › Install or reinstall** no longer announces « installed and started » when the installer failed. It caught exceptions only: a non-zero exit — a missing libvirt network, a build that gave way — printed the success line right under the error itself, along with the path of an authority that does not exist
|
|
||||||
- A Proxmox guest receives its guide, its timezone, its apt mirror and the cache authority again. All four go by ssh and used to start as soon as an address was known, while cloud-init was still creating accounts and keys: they failed together, and the VM was born in UTC, guideless, without the authority and on its image's repositories. The deployment now waits for the machine to answer ssh — five minutes at most — and says so when it never does, rather than failing four times in a row
|
|
||||||
- The cache no longer serves a repository index newer than the signature that announces it. Offline, each object came out with its own date: an index refreshed on Saturday under a Friday `InRelease` made apt fail on « File has unexpected size » or « Hash Sum mismatch », and the install stopped on unmet dependencies — a message that blames the repository, never the cache. The comparison is made on the upstream `Last-Modified`, never on the storage date, which is renewed on every hit
|
|
||||||
- The refusal to copy the cache to another machine names the gesture that lifts it, and that gesture is not entry 1 — entry 1 installs the cache HERE, so following it reinstalled the host that already had one while the target stayed empty. Three situations were reported as a single « no cache installed »: an ssh link that never ran the probe, a target carrying no cache, and a target whose cache lacks its service account. Each has its own message now, and the missing-cache one lists the steps to run ON the target, the two ways past an installer that reads the « default » libvirt network included — start libvirt, or name the bridge, a stopped libvirt making it die on a network that exists. The steps also name the branch to put the target on, read from this host: the installer is a file of the repository, so a machine left on another branch answers « no such file », which looks nothing like a missing cache. A fourth case is checked before a single byte leaves: sudo asking for a password on the target, which no terminal can answer since the store itself occupies ssh's standard input — the message gives the ticket to obtain there first. The arrival is asked for the privilege ONCE, `tar` and `chown` under a single invocation. Where sudo there wants a password, the entry offers two ways out instead of failing: the one-off sudoers line that allows it, or a two-step mode — the store is sent into the target's own account, which needs no privilege at all, and a printed command extracts it from a terminal there, where a password can be typed. That mode needs twice the store on the target, checked before a byte leaves, a cache holding only already-compressed packages and git archives. The guide carries the same section
|
|
||||||
- The Proxmox VE deployment form checks the cache before cutting the network, as the libvirt one already did: what the store lacks, no cut VM will read, and the failure used to land an hour later, at the desktop step — a message that blames the repository, never the cache. F5 again means going ahead anyway. Both forms now share a single verdict instead of two copies of it, which would have drifted apart at the first adjustment
|
|
||||||
- The cache keeps its repository indexes when a mirror list rotates. An index published under the hash of its content — `by-hash/SHA256/…` — was stored under a key carrying the host, so the same bytes served by a second mirror were fetched again; offline they were simply missing, and the install failed on files the store already held, with a message that blames the repository. Such an object is now keyed by its path alone, its name BEING the checksum of its content
|
|
||||||
- The cache diagnosis no longer reports « no redirection rule is posted » when it simply could not read them. On a host whose sudo asks for a password, `sudo -n nft` returns nothing, and that silence was read as an absence of rules — sending the operator to reinstall a cache that was redirecting correctly. The reading now carries a third state, « cannot tell », marked with a dot rather than a cross, exactly as the upstream-cut reading already did
|
|
||||||
- A lifted cut gives the upstreams their chance back at once. The service remembers, for a short while, which upstreams just failed to connect, so that an offline install does not pay the connection delay hundreds of times; nothing told it the cut was over, and the first requests after the lift fell back on the store while the network was already back. The lift now touches a witness file inside the store, which that memory consults — there is no channel at all to the running service
|
|
||||||
- Before cutting, the form also warns about what no index can reveal: a package the deployment lays down OUTSIDE the watched thread — the guest agent, installed by a detached unit whose failure surfaces nowhere — and the git repositories declared by the manifests that have no mirror yet. A held suite index was enough to call the cache complete while not one byte of that package had ever crossed it; and a git negotiation is never stored, so a repository without a mirror simply cannot be cloned once the network is gone
|
|
||||||
- The mirror warning counts only the repositories of the Odoo version being deployed. It used to add up every manifest in the repository — the deprecated one included — and announced 170 missing mirrors where a real Odoo 18 deployment meets four: an alarm that fires for nothing is one that stops being read. Filling the mirrors still takes every version ahead, which is its purpose
|
|
||||||
- A host banned from decryption on a burst of transport errors gets another chance. Three failed handshakes in a row put it in an opaque tunnel, and a tunnel never consults the store: a distribution mirror condemned by a few corrupted records sent all its traffic back upstream, including the hundreds of objects already held for it, until the service was restarted. A TLS alert still bans for good — the client looked at our certificate and refused it — but a repeated cut is only a suspicion, and it reopens after ten minutes
|
|
||||||
- **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
|
|
||||||
- `make` looks for bash instead of assuming `/bin/bash`. That path does not exist on NixOS, where the shell lives in the store, and make stopped before running any recipe — including the one that installs what creates that path. Elsewhere the resolved shell is the same one as before
|
|
||||||
- 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
|
|
||||||
- A guest with no per-file trust anchor is taken out of the download cache instead of being intercepted without one. Interception is transparent and covers the whole bridge, so a VM given no authority still fails every HTTPS download on « self-signed certificate in certificate chain » — and on a declarative system placing the authority comes too late, the first rebuild being the first download. On a Proxmox host it is the HOST that is exempted: a nested guest leaves masqueraded behind it and the bridge never sees its own address. Measured from inside the guest: code 000 and SSL verification 19, then 200 and 0. Without it the package manager fell back to building 564 derivations, whose sources failed for the same reason
|
|
||||||
- NixOS learns the download cache's authority WITHOUT a rebuild, so it is no longer taken out of the cache — which used to close offline deployment to it, the store being the only source there and an exempted VM having none. It has no per-file trust anchor and /etc is generated read-only, while a declaration would come too late, the first rebuild being the first download. The authority is therefore POINTED AT, consumer by consumer, through the environment — and the environment is lost at three boundaries the four imperative families never meet. nix-daemon is socket-activated and sees no session: a drop-in under /run/systemd/system, a tmpfs writable where /etc is not, reaches it. sudo wipes it, and root's nix talks straight to the local store rather than the daemon, downloading on its own: `Defaults env_keep` carries the variables across, visudo being reached through the system profile since cloud-init's PATH there holds no sudo. And the ssh session opens one second before cloud-init writes the bundle, so the exports live inside the wait for it rather than at the head of the command. The bundle concatenates the system authorities with the cache's, giving the cache's alone would stop trusting everything else. Checked on a fresh VM with upstream CUT: nix-shell realises from the store; and with the cache intercepting, a complete install ends with no certificate refusal
|
|
||||||
- Odoo answers from outside a NixOS VM. It listened on 0.0.0.0:8069 and replied locally, but NixOS enables a firewall by default where none of the four other cloud images does: the host received nothing — not a refusal, silence until the timeout — and the monitor declared Odoo absent on a machine where it was running. Measured from the host: 000 after 12 s, then 303 in 9 ms
|
|
||||||
- ERPLibre runs as a service on NixOS. The install ended by writing a unit into `/etc/systemd/system`, generated from the store and mounted read-only: it returned 1 at its last step, after the clone, the venv and an Odoo start had all succeeded. The unit is now declared by the module; its interpreter comes from the store, `/bin` being an envfs FUSE mount that systemd does not see when it resolves the executable; and its PATH carries bash, whose absence stopped `run.sh` before Odoo
|
|
||||||
- What a declarative system must declare, and the four others receive free from their cloud image: xmlsec1, without which Odoo refuses to install auth_saml, a module of the addons path; parallel and shfmt, called by bare name; growpart, absent from the whole system while the disk grow is written « … || true » and returned 0 without growing anything; and the guest agent, which came from the image rather than the repository, its unit PATH lacking findmnt so that guest-exec died with 127 on its first line
|
|
||||||
- The connection guide is displayed on NixOS. Deployment writes `/etc/motd` everywhere and relies on pam_motd to show it — true of the four cloud images, false here, where sshd reports « printmotd no » and the PAM stack holds no pam_motd: the guide was written, complete, and nobody read it. It also gains a NixOS block naming the trap it exists for — `/etc/nixos/erplibre.nix` is rewritten by `make install_os`, and declarations added there vanish without a word
|
|
||||||
- « make db_drop_all » no longer announces databases as dropped that were not. It built a parallel command, discarded its exit status and printed the list; the case is reachable as soon as parallel is missing from the PATH, and the operator moves on believing their databases are gone
|
|
||||||
- A download cache mirror refused for lack of space names its threshold and its measurement. It echoed a field every ordinary caller leaves at zero — « less than 0 B free on disk » announces no threshold and does not say what was measured
|
|
||||||
- A downloaded image is checked against the sum its publisher ships, for every distribution that publishes one and WITHOUT asking. The check existed behind a flag and for Ubuntu only, so the other images arrived with nothing looking at them. Six now enter, read off the repositories rather than guessed: Debian publishes sha512 where everything else is sha256, the RHEL families name the file « CHECKSUM », Rocky writes the BSD form, and Arch and openSUSE ship a sum per image. An unreachable sums file no longer stops a deployment — that is an availability failure — while a mismatch still stops everything and removes the image
|
|
||||||
- The locale a deployed VM is given applies on NixOS. cloud-init applies it through locale-gen and update-locale, absent there, and the VM kept the distribution's default — « fr_CA.UTF-8 » asked for, « en_US.UTF-8 » obtained. Both it and the timezone are declared by the module now, and neither is imposed on a NixOS one already had
|
|
||||||
- The connection guide fits an 80-column terminal even on a VM carrying its tools and a desktop, where it rendered 103 columns wide: the frame overflowed, the terminal wrapped wherever it liked, and the two-column alignment — the only thing making the guide readable at a glance — was lost. A gloss now wraps under its column, and moves below its command when that command leaves no room. The layout carries the rule, not the length of the texts, which would have held only until the next tool
|
|
||||||
- A VM deployed offline gets its cache variables before its installation starts. « cloud-init status --wait » returns as soon as cloud-init declares itself in error — an accessory module suffices — while its final stage is still writing the authority, `/etc/environment` and the sudoers file; a session opened in that second lived without them, and an install run by sudo then rejected the cache certificate. The deployment now waits for the cloud-final unit, and only its « activating » state, that unit being a oneshot that stays active once finished
|
|
||||||
- Repository metadata an RPM distribution names after the hash of its content is served from disk like a package, and a range request on a file the cache does not hold fetches the whole file in the background, once per key. Such an index of tens of megabytes was taken whole on every install — about 110 MB per VM on a RHEL family system — and dnf, which fetches its zchunk metadata by ranges, got a « 504 » with upstream cut, no range being stored
|
|
||||||
- A VM deployed offline no longer waits for a time synchronisation that cannot come. An image that enables `systemd-time-wait-sync` holds `time-sync.target` until the first NTP answer, and cloud-init's final stage is ordered after it: with no reachable time server, that stage never ran, nor the ssh host keys it generates, and the VM reached its login prompt without ever answering ssh
|
|
||||||
- The download cache no longer re-fetches a package it already holds because a mirror files it under another path. A mirror prefixes the path as it pleases — « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » — and the whole path gave two keys for the same bytes: over a log of 7099 delivered names, 1124 lived under several paths and 3.18 GiB went back upstream for nothing. Only the last six segments count now, empty ones falling with them. Six is the smallest collision-free bound: a Debian path carries exactly six, so five would serve Ubuntu's package for Debian's, which bears the same name for other bytes. Pacman packages stop at four, their paths being shorter than the common bound, which drops the mirror's prefix and keeps the repository name. A store filled before this change is brought over by `--recle`
|
|
||||||
- A kept image that has gone stale is fetched once more instead of ending the deployment. A distribution's « latest » directory moves with each point release and the published sum stops describing the image on disk, with no byte corrupted: the check deleted it and exited, losing a whole campaign — three VMs — to a staleness one download repairs. A second mismatch is on freshly downloaded bytes, stops everything and deletes the image; called with no mirror list, and for unreachable sums, nothing changes
|
|
||||||
- `long_test/qemu_cache.py` no longer fails a campaign where the cache served everything. A URL counts as « already seen » only if the first VM obtained its bytes: one package lives under two paths depending on the mirror, and the first VM can get a « 504 » on one — upstream judged mute — then be served from disk by the other, so nothing was stored under the first path and the second VM's honest download was counted a fault. A missing status counts as delivered, older logs not always writing it, and the new-files line names both of its causes instead of blaming Arch whatever the system measured
|
|
||||||
- An interrupted image download resumes on the same mirror, three attempts, by a `Range` asking for the rest, instead of throwing away what it received: a VM image weighs half a gigabyte, and a truncated `.part` used to send the reader back to a hand-typed `curl -C -`. A server ignoring the Range returns the whole file, and the transfer restarts from zero rather than doubling the bytes already there
|
|
||||||
- The cache's certificate variables name a bundle only when the file exists. A recent Fedora lacks `/etc/pki/tls/certs/ca-bundle.crt`, and pointing pip at a missing path made it refuse EVERY download, including what has nothing to do with the cache. None found writes no variable, and pip keeps its own certificate set
|
|
||||||
- `make version` names both Pythons, Odoo's and the tooling's. A bare label suggested the repository had one, and the figure shown was not that of the venv the reader works in
|
|
||||||
- `make` no longer launches a `.venv.erplibre` built on another machine, such as a checkout mounted over the network, which died on « No module named 'encodings' ». `install.sh` reads the interpreter's version by running code: `python -V` answers before the standard library loads, so it vouched for an interpreter that could not start. Such a venv is now reported as built elsewhere, with the command that rebuilds it
|
|
||||||
- Dependabot no longer opens pull requests against the frozen requirements of Odoo 12 to 17: its security updates scan `requirement/` as its own directory, which the exclusions did not cover
|
|
||||||
- `poetry_update.py` stops on a missing `pyproject.toml` with the command that creates it, `make switch_odoo_XX` for the active version, and offers to run it then restart when launched from a terminal
|
|
||||||
- `poetry_update.py` runs Poetry in the Odoo venv even from a shell under `.venv.erplibre`, whose Python made it fail on « InvalidCurrentPythonVersionError »
|
|
||||||
- `poetry_update.py` skips requirements and manifests under a `doc`, `docs`, `example` or `examples` directory: an example file declaring a loose `>=` no longer moves a dependency for the whole environment
|
|
||||||
|
|
||||||
## Removed
|
|
||||||
|
|
||||||
- The `sshconf` dependency, declared and installed everywhere and imported nowhere
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- An API key and a bearer token are redacted too before a command is displayed, logged or reprinted: `OPENAI_API_KEY=` went out in the clear, and a header token escaped by construction, carrying neither an option name nor a variable name
|
|
||||||
- Following a redirect, the cache no longer forwards the client's credentials (Authorization, Cookie, Proxy-Authorization) to another host
|
|
||||||
- Odoo 18 installs `idna` 3.20 instead of the 3.6 its own requirements pin, which is affected by CVE-2024-3651
|
|
||||||
- Odoo 18 installs `requests` 2.32.4 instead of the 2.31.0 its own requirements pin, which is affected by CVE-2024-35195 and CVE-2024-47081
|
|
||||||
- The git mirror of the QEMU cache clones only over `http` and `https`: a client could name an `ssh://` or `git://` repository in its request and make the cache connect, with its service account keys, to a host of its choosing. Repositories fetched over HTTPS are mirrored as before; the binary reports 0.2.17
|
|
||||||
|
|
||||||
|
|
||||||
## [1.8.0] - 2026-09-04
|
|
||||||
|
|
||||||
**Migration notes**
|
|
||||||
|
|
||||||
Recreating the virtual environment, the Python interpreter and the package
|
|
||||||
installer being chosen now. Use the installation guide from tool `make`.
|
|
||||||
Ubuntu 20.04 and 22.04 are no longer supported.
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- Deploy ERPLibre VMs with QEMU/KVM from cloud images: Ubuntu, Debian, Fedora, AlmaLinux, Rocky, openSUSE, Arch, Linux Mint, and Debian on s390x. Hardware, branch and Odoo version are set per machine
|
|
||||||
- Proxmox VE as a deployment target, its installation including the reboot it needs
|
|
||||||
- A QEMU menu: network status and repair, 3D acceleration, a diagnostic report, file recovery from a VM that no longer boots, virt-viewer and a remote desktop tunnel
|
|
||||||
- An install dashboard and Textual forms: deploy, follow, update, restart or delete a VM without leaving the screen
|
|
||||||
- A mobile development VM: PyCharm, Android Studio, an Android emulator and an adb tunnel
|
|
||||||
- A VPN tool, five free technologies from the menu, the secrets in a KeePassXC vault and a diagnosis that names the failing stage
|
|
||||||
- Automated Odoo migration: the tool drives the whole run, goes back to a step, and repairs what a version bump leaves behind
|
|
||||||
- Migration review: a verdict per step, smoke tests on every public URL, and a filestore check
|
|
||||||
- A read-only analysis toolkit for an Odoo database, a backup zip included, with PostgreSQL index advice
|
|
||||||
- Anonymising a production copy without AI, and duplicating a database neutralised for good
|
|
||||||
- Development assistants installed inside a VM, and a Git and Shell menu that installs what a checkout needs
|
|
||||||
- A writing convention for what stays in git, held by a `pre-commit` and a `commit-msg` hook
|
|
||||||
- `long_test/` — tests that create real machines, nested QEMU and Proxmox included, kept out of the unit runner
|
|
||||||
- NTFY, Forgejo, a local git server, e-mail from the CLI, and SSH configuration with recursive ProxyJump
|
|
||||||
- The Python interpreter and the package installer are chosen, through EL_PYTHON_PROVIDER and EL_PIP_PROVIDER
|
|
||||||
- The OCA generative AI policy, Claude Code agents and commands, and the context an assistant is given, shown from the menu
|
|
||||||
- Unit tests with a bilingual test plan, navigation telemetry for TODO, and Odoo 18 reading STL files
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- todo.py split into nine files, one per subject, with a shared base per form
|
|
||||||
- Every menu entry carries an icon, the menus are grouped into sections, and a countdown prompt gives 15 seconds to decide
|
|
||||||
- Branch, profile, type and timezone are chosen per VM rather than globally
|
|
||||||
- Installation covers Fedora, Debian, Ubuntu, Arch and openSUSE; repository sync and Poetry run in parallel, quiet unless EL_VERBOSE asks
|
|
||||||
- Node.js 22 for Capacitor 8, flanker for Odoo 18, CybroOdoo extras opt-in, and a Poetry dependency declinable per architecture
|
|
||||||
- A VM boots faster and picks the fastest reachable mirror, Canadian pacman mirrors coming first on Arch
|
|
||||||
- Staging names the files, never `git add -A`
|
|
||||||
- Enter targets the highest supported Odoo version, and a VM name drops the `latest` segment
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- The libvirt network no longer counts as its own collision, no longer leaves a host without network at the next boot, and its state is read in English whatever the locale
|
|
||||||
- QEMU deployment: sudo says why it needs a password, the `libvirt` group replaces it where it suffices, no host reboots unasked, and an orphan disk no longer blocks a creation
|
|
||||||
- Migration: the database drop, the account.root view Odoo 17 leaves behind, the pricelists a repair invented, and the assumptions the 13-to-18 run rested on
|
|
||||||
- Anonymisation respects what a value means, and no longer breaks past the 131 072-byte limit of a single argument
|
|
||||||
- Installation on Debian 13, Fedora, Ubuntu 26.04 and s390x: apt locks, missing compilers and headers, too little memory, and what a recent SWIG or PROJ needs
|
|
||||||
- Secrets: the KeePassXC vault opens on a server without tkinter, the forgejo installer stops echoing the password it set, and db_restore validates the master one
|
|
||||||
- A question is seen before it is answered, and one faulty repository no longer takes a whole batch down
|
|
||||||
- Three screens that fell over, a shrink that would have filled the disk, and a monitor that binned a VM before being sure
|
|
||||||
- The unit runner globs the whole directory, where it ran 1131 tests of 3703
|
|
||||||
- Proxmox no longer aims at the host instead of the VM
|
|
||||||
|
|
||||||
## Removed
|
|
||||||
|
|
||||||
- Ubuntu 20.04 and 22.04 support, on every architecture
|
|
||||||
- The residue check that called a language broken when its `active` is NULL
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- Passwords and tokens are redacted before a command is displayed, logged or reprinted
|
|
||||||
- The Odoo master password and the KeePass one leave the command line, an environment variable carrying them instead
|
|
||||||
|
|
||||||
|
|
||||||
## [1.7.0] - 2026-03-11
|
|
||||||
|
|
||||||
**Migration notes**
|
**Migration notes**
|
||||||
|
|
||||||
Recreating the virtual environment, use installation guide from tool `make`.
|
Recreating the virtual environment, use installation guide from tool `make`.
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- Odoo 12.0 to 18.0 in a single workspace, switched without reinstalling: the manifests, the configuration and the addons paths follow the version named in `.odoo-version`
|
- Support Odoo migration database and module with TODO
|
||||||
- ERPLibre's Python separated from Odoo's — `.venv.erplibre` carries the repository's own tools, `.venv.odoo<version>` the server — so a tool of the repository no longer depends on the interpreter a given Odoo version imposes
|
- Support multi version odoo switch on same workspace
|
||||||
- Auto-installation driven from TODO: the menu lays down the environment it needs, Poetry, the Google Repo manifests and the addons included, rather than printing a command to retype
|
- Script for hardening the installation
|
||||||
- Migration of an Odoo database and its modules from TODO, `--neutralize` included, with the repair of the mail module that a move from PostgreSQL 17 to 18 leaves behind
|
- Support Odoo versions 12.0 to 18.0
|
||||||
- A hardening script for the installation
|
- Separate ERPLibre python installation from Odoo python with .venv.erplibre and .venv.odoo18
|
||||||
- The ERPLibre Home mobile application: TODO compiles it, deploys it, renames the software and changes its menu image
|
- Implement auto-installation with TODO.py
|
||||||
- The RobotLibre code generator, with the queue_job channels its configuration needs
|
- TODO show documentation, download database, help with code formatting
|
||||||
- ERPLibre DevOps, and the automation procedure it describes
|
- Performance script to mesure request per second
|
||||||
- The Selenium grid from `selenium_lib.py`: a KeePass vault opened for the run, file downloads, dark mode, video recording and a scenario library
|
- Support Mainframe architecture 390x
|
||||||
- A performance script measuring the requests per second a website answers
|
- Deployment with Cloudflare and Nginx
|
||||||
- Deployment: Cloudflare DNS, nginx with a non-interactive certbot, Apache templates matching the nginx ones, and a systemd unit whose working directory is configurable
|
- Support Apache configuration like Nginx
|
||||||
- The s390x mainframe architecture
|
- Support RobotLibre code generator
|
||||||
- Addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor, and the design-themes repository
|
- Support ERPLibre DevOps, automation procedure about DevOps
|
||||||
- Database backup and erase commands, and a clearer restore naming
|
- ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it
|
||||||
- A security check of the Python environment, from the menu
|
- Support Selenium grid from selenium_lib.py
|
||||||
- TODO shows the documentation, downloads a database and helps with code formatting
|
- Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor
|
||||||
- Killing an Odoo process by the port it holds, from the menu
|
|
||||||
- CLAUDE.md and the agent information document, so an assistant reads the repository's conventions instead of guessing them
|
|
||||||
- A FAQ entry on wkhtmltopdf for recent distributions
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- Odoo 18.0 becomes the default version of a checkout
|
- Docker support postgresql 18
|
||||||
- Docker moves to PostgreSQL 18, with the matching client
|
- Format script search diff file into each repository
|
||||||
- The documentation is bilingual, generated by mmg from the `.base.md` sources: a `.md` or `.fr.md` edited directly is lost at the next generation
|
- Support neutralize database from Odoo
|
||||||
- The TODO menus are grouped into sections, and the English text serves as the i18n key rather than a code of its own
|
|
||||||
- The formatting script looks for the changed files in every repository, hidden addons included, and skips a repository that is not installed
|
|
||||||
- Odoo runs on a custom database, and the menu configures queue_job as well as the SSH forwarding a remote instance needs
|
|
||||||
- Killing a process by port asks before acting, through an interactive menu
|
|
||||||
- Neutralising a database goes through Odoo's own `--neutralize`
|
|
||||||
- LinuxMint 22.3, Ubuntu 25.10, and macOS without Python 3.7
|
|
||||||
- Odoo 18 dependencies: tldextract, PyYAML, pdfminer.six, and cryptography at its latest version
|
|
||||||
- A make target runs the unit tests
|
|
||||||
- The Makefile is split: its commands live in `conf/`, and `Common.Makefile` extends it for a project of one's own
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- The documentation accents, and the markdown generation running in parallel
|
|
||||||
- `git_tool` returns nothing instead of raising where `.git` is absent
|
|
||||||
- `poetry iscompatible` no longer crashes on a version carrying a letter, an alpha or a release candidate
|
|
||||||
- pymssql compiles again, and the Odoo 18 requirements leave pyssql out of a production install
|
|
||||||
- wkhtmltopdf is no longer offered where it does not exist: no package is published for s390x on Ubuntu 25.10
|
|
||||||
- Selenium: the snap Firefox path, a 60-second timeout when reaching for an element, execution in a private window, and a login that waits for Odoo 18
|
|
||||||
- The formatting script ignores the files and directories it must not touch
|
|
||||||
- `db_drop_all` runs its shell command, and the backup processing keeps the permissions of what it writes
|
|
||||||
- TODO: the first import, the regeneration of `.repo/local_manifests`, the database open dialog, and a missing Odoo version reported instead of a crash
|
|
||||||
- The code generator: creating a project, extracting a class carrying a selection, and reading a model through the Python 3.11 `ast` module rather than astor
|
|
||||||
- Docker: the duplicated Odoo 18 build target, and the compose file pinned to an image that works
|
|
||||||
|
|
||||||
|
|
||||||
## [1.6.0] - 2025-04-25
|
## [1.6.0] - 2025-04-25
|
||||||
|
|
@ -758,11 +491,7 @@ Because addons repository has change, config file need to be updated.
|
||||||
- Support only python3.6 and python3.7, python3.8 causes error in runtime.
|
- Support only python3.6 and python3.7, python3.8 causes error in runtime.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/ERPLibre/ERPLibre/compare/v1.8.0...HEAD
|
[Unreleased]: https://github.com/ERPLibre/ERPLibre/compare/v1.6.0...HEAD
|
||||||
|
|
||||||
[1.8.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.7.0...v1.8.0
|
|
||||||
|
|
||||||
[1.7.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.6.0...v1.7.0
|
|
||||||
|
|
||||||
[1.6.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.5.0...v1.6.0
|
[1.6.0]: https://github.com/ERPLibre/ERPLibre/compare/v1.5.0...v1.6.0
|
||||||
|
|
||||||
|
|
|
||||||
47
CLAUDE.md
47
CLAUDE.md
|
|
@ -7,8 +7,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
## Projet
|
## Projet
|
||||||
|
|
||||||
ERPLibre est un fork communautaire d'Odoo Community Edition (OCE) supportant les versions 12 à 18.
|
ERPLibre est un fork communautaire d'Odoo Community Edition (OCE) supportant les versions 12 à 18.
|
||||||
Version actuelle : **1.8.0** | Licence : **AGPL-3.0+**
|
Version actuelle : **1.6.0** | Licence : **AGPL-3.0+**
|
||||||
Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.8.0)
|
Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.6.0)
|
||||||
|
|
||||||
## Points d'attention pour Claude
|
## Points d'attention pour Claude
|
||||||
|
|
||||||
|
|
@ -19,25 +19,38 @@ Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.8.0)
|
||||||
`ls -d .venv.odoo*` plutôt que de le composer de tête
|
`ls -d .venv.odoo*` plutôt que de le composer de tête
|
||||||
- Les scripts ERPLibre utilisent `.venv.erplibre/bin/python`
|
- Les scripts ERPLibre utilisent `.venv.erplibre/bin/python`
|
||||||
- Le Makefile principal inclut des fragments depuis `conf/make.*.Makefile`
|
- Le Makefile principal inclut des fragments depuis `conf/make.*.Makefile`
|
||||||
- Un module cloné depuis un module existant hérite de ses commentaires
|
- Les fichiers privés vont dans `private/` (non versionné)
|
||||||
et de ses docstrings : les relire avant de committer, un nom de client
|
|
||||||
ou de base y arrive tout seul
|
|
||||||
- Les fichiers privés vont dans `private/`. C'est le SEUL endroit qui a le
|
|
||||||
droit de porter une donnée de client — nom, base, machine, adresse,
|
|
||||||
chiffres. Il peut être commité, mais seulement sur un dépôt privé : sur
|
|
||||||
un fork public, ce qui s'y trouve devient public comme le reste
|
|
||||||
- Partout ailleurs — code, commentaires, messages de commit, documentation
|
|
||||||
— aucune donnée identifiante, jamais. Ce sont les fichiers qui suivent le
|
|
||||||
dépôt en amont. La règle complète, avec l'épreuve qui tranche, est dans
|
|
||||||
`.claude/rules/04-code-conventions.md`
|
|
||||||
- La DB PostgreSQL par défaut est sur le port 5432, mot de passe admin : `admin`
|
- La DB PostgreSQL par défaut est sur le port 5432, mot de passe admin : `admin`
|
||||||
- Port Odoo par défaut : 8069, longpolling : 8072
|
- Port Odoo par défaut : 8069, longpolling : 8072
|
||||||
|
- Pour les commits : suivre le format `[TYPE] description` (ex: `[FIX]`, `[UPD]`, `[ADD]`, `[REM]`)
|
||||||
- Pour la documentation : modifier les `.base.md`, jamais les `.md` ou `.fr.md` directement
|
- Pour la documentation : modifier les `.base.md`, jamais les `.md` ou `.fr.md` directement
|
||||||
- Outil mmg disponible via `source .venv.erplibre/bin/activate && mmg`
|
- Outil mmg disponible via `source .venv.erplibre/bin/activate && mmg`
|
||||||
- Les tests qui créent de VRAIES machines vivent dans `long_test/` et non dans
|
|
||||||
`test/` : le lanceur unitaire balaie `test/test_*.py` et doit rester lançable
|
## Core Principles
|
||||||
en quelques secondes, même sans virtualisation. Ils durent des heures et se
|
|
||||||
défont par `--detruire` — voir `long_test/README.md`
|
- **Simplicity First**: Make every change as simple as possible. Impact minimal code.
|
||||||
|
- **No Laziness**: Find root causes. No temporary fixes. Senior developer standards.
|
||||||
|
- **Minimal Impact**: Changes should only touch what's necessary. Avoid introducing bugs.
|
||||||
|
|
||||||
|
## Règles détaillées
|
||||||
|
|
||||||
|
Les instructions détaillées sont dans `.claude/rules/` :
|
||||||
|
|
||||||
|
| Fichier | Contenu |
|
||||||
|
|---------|---------|
|
||||||
|
| `01-versions.md` | Versions Odoo supportées, où lit-on la correspondance |
|
||||||
|
| `04-code-conventions.md` | Où sont les configs de format, conventions Git |
|
||||||
|
| `06-code-generator.md` | Génération de modules Odoo |
|
||||||
|
| `07-documentation.md` | Interdit : ne pas éditer les `.md` générés |
|
||||||
|
| `08-deployment.md` | Docker, systemd, nginx, SSL, DNS |
|
||||||
|
| `09-workflow.md` | Workflow orchestration + task management |
|
||||||
|
|
||||||
|
Chargées à la demande (`.claude/skills/`) :
|
||||||
|
|
||||||
|
| Skill | Contenu |
|
||||||
|
|-------|---------|
|
||||||
|
| `erplibre-commands` | Commandes make et scripts : versions, run, tests, DB, Docker, repo |
|
||||||
|
| `erplibre-doc-i18n` | Mode d'emploi mmg (`.base.md`) et i18n du CLI TODO |
|
||||||
|
|
||||||
L'arborescence et la liste des venvs ne sont plus documentées : `ls` et
|
L'arborescence et la liste des venvs ne sont plus documentées : `ls` et
|
||||||
`ls -d .venv.*` en donnent l'état réel, la doc dérivait de la réalité.
|
`ls -d .venv.*` en donnent l'état réel, la doc dérivait de la réalité.
|
||||||
|
|
|
||||||
114
Makefile
114
Makefile
|
|
@ -1,9 +1,4 @@
|
||||||
# bash, où qu'il soit. « /bin/bash » n'existe pas sur NixOS, où le shell
|
SHELL := /bin/bash
|
||||||
# vit dans le store : make s'y arrêtait sur « /bin/bash: No such file or
|
|
||||||
# directory » avant d'exécuter la moindre recette, y compris celle qui
|
|
||||||
# installe de quoi le créer. Le repli garde le chemin des plateformes qui
|
|
||||||
# l'ont, et l'affectation reste immédiate — SHELL est lu à chaque recette.
|
|
||||||
SHELL := $(shell command -v bash 2>/dev/null || echo /bin/bash)
|
|
||||||
ERPLIBRE_VERSION := $(strip \
|
ERPLIBRE_VERSION := $(strip \
|
||||||
$(if $(wildcard .erplibre-version),$(shell cat .erplibre-version),) \
|
$(if $(wildcard .erplibre-version),$(shell cat .erplibre-version),) \
|
||||||
)
|
)
|
||||||
|
|
@ -102,61 +97,6 @@ version:
|
||||||
pyenv_update:
|
pyenv_update:
|
||||||
~/.pyenv/bin/pyenv update
|
~/.pyenv/bin/pyenv update
|
||||||
|
|
||||||
# mise pose un CPython PRECOMPILE la ou pyenv le compile : quelques secondes
|
|
||||||
# contre une a trois minutes, et aucun paquet -dev requis. Volontairement
|
|
||||||
# EXPLICITE : « curl | sh » engage, et ce n'est pas a un script d'installation
|
|
||||||
# de le decider a la place de l'utilisateur. Une fois mise present,
|
|
||||||
# EL_PYTHON_PROVIDER=auto s'en sert tout seul.
|
|
||||||
#
|
|
||||||
# Pas de binaire s390x publie a ce jour : sur cette architecture, la cible
|
|
||||||
# le dit et n'installe rien.
|
|
||||||
# uv installe les paquets Python nettement plus vite que pip, et met en cache
|
|
||||||
# les roues qu'il CONSTRUIT — ce qui compte la ou rien n'a de roue publiee.
|
|
||||||
# Contrairement a mise, uv publie une roue s390x : un « pip install uv » dans
|
|
||||||
# le venv d'outils suffit, sans « curl | sh ». Reste EXPLICITE quand meme,
|
|
||||||
# comme install_mise : EL_PIP_PROVIDER=auto s'en sert des qu'il est la.
|
|
||||||
#
|
|
||||||
# Attention a l'attente : « poetry install », qui domine le temps
|
|
||||||
# d'installation, n'est PAS accelere — uv ne lit pas poetry.lock.
|
|
||||||
.PHONY: install_uv
|
|
||||||
install_uv:
|
|
||||||
@if command -v uv >/dev/null 2>&1; then \
|
|
||||||
echo "uv deja present : $$(uv --version)"; \
|
|
||||||
else \
|
|
||||||
echo "Installation de uv dans le venv d outils"; \
|
|
||||||
./$$(cat conf/python-erplibre-venv | xargs)/bin/pip install uv; \
|
|
||||||
echo "uv : ./$$(cat conf/python-erplibre-venv | xargs)/bin/uv"; \
|
|
||||||
echo "Ajoutez ce repertoire au PATH, ou installez uv globalement."; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Telecharge puis execute : dans « curl | sh », le statut est celui de sh,
|
|
||||||
# qui rend 0 sur une entree vide et masque un telechargement rate.
|
|
||||||
.PHONY: install_mise
|
|
||||||
install_mise:
|
|
||||||
@if command -v mise >/dev/null 2>&1; then \
|
|
||||||
echo "mise deja present : $$(mise --version)"; \
|
|
||||||
elif command -v pacman >/dev/null 2>&1; then \
|
|
||||||
sudo pacman -S --needed --noconfirm mise; \
|
|
||||||
elif [ "$$(uname -m)" = "s390x" ]; then \
|
|
||||||
echo "mise ne publie pas de binaire s390x : on reste sur pyenv."; \
|
|
||||||
else \
|
|
||||||
echo "Installation de mise depuis https://mise.run"; \
|
|
||||||
installateur="$$(mktemp)" || exit 1; \
|
|
||||||
if ! curl -fsSL -o "$$installateur" https://mise.run; then \
|
|
||||||
rm -f "$$installateur"; \
|
|
||||||
echo "Telechargement de l installateur mise impossible" \
|
|
||||||
"(reseau ou cache) : mise n est pas pose." >&2; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
if ! sh "$$installateur"; then \
|
|
||||||
rm -f "$$installateur"; \
|
|
||||||
echo "L installateur de mise a echoue (voir ci-dessus)." >&2; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
rm -f "$$installateur"; \
|
|
||||||
echo "Ajoutez ~/.local/bin a votre PATH, puis relancez l installation."; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: db_create_db_test
|
.PHONY: db_create_db_test
|
||||||
db_create_db_test:
|
db_create_db_test:
|
||||||
./script/make.sh db_drop_db_test
|
./script/make.sh db_drop_db_test
|
||||||
|
|
@ -202,47 +142,46 @@ format:
|
||||||
|
|
||||||
.PHONY: format_all
|
.PHONY: format_all
|
||||||
format_all:
|
format_all:
|
||||||
parallel ::: "./script/make.sh format_code_generator" "./script/make.sh format_code_generator_template" "./script/make.sh format_script" "./script/make.sh format_test" "./script/make.sh format_erplibre_addons" "./script/make.sh format_supported_addons"
|
parallel ::: "./script/make.sh format_code_generator" "./script/make.sh format_code_generator_template" "./script/make.sh format_script" "./script/make.sh format_erplibre_addons" "./script/make.sh format_supported_addons"
|
||||||
|
|
||||||
# Les dépôts d'addons sont nommés, jamais leur chemin : il dépend du manifeste,
|
|
||||||
# et chaque version d'Odoo n'en rapatrie qu'une partie. Voir format_addons.sh.
|
|
||||||
.PHONY: format_code_generator
|
.PHONY: format_code_generator
|
||||||
format_code_generator:
|
format_code_generator:
|
||||||
./script/maintenance/format_addons.sh --xml TechnoLibre_odoo-code-generator
|
.venv.erplibre/bin/isort --profile black -l 79 ./addons/TechnoLibre_odoo-code-generator/
|
||||||
|
./script/maintenance/black.sh ./addons/TechnoLibre_odoo-code-generator/
|
||||||
|
./script/maintenance/prettier_xml.sh ./addons/TechnoLibre_odoo-code-generator/
|
||||||
|
|
||||||
.PHONY: format_erplibre_addons
|
.PHONY: format_erplibre_addons
|
||||||
format_erplibre_addons:
|
format_erplibre_addons:
|
||||||
./script/maintenance/format_addons.sh --xml ERPLibre_erplibre_addons
|
.venv.erplibre/bin/isort --profile black -l 79 ./addons/ERPLibre_erplibre_addons/
|
||||||
./script/maintenance/format_addons.sh ERPLibre_erplibre_theme_addons
|
./script/maintenance/black.sh ./addons/ERPLibre_erplibre_addons/
|
||||||
|
./script/maintenance/prettier_xml.sh ./addons/ERPLibre_erplibre_addons/
|
||||||
|
.venv.erplibre/bin/isort --profile black -l 79 ./addons/ERPLibre_erplibre_theme_addons/
|
||||||
|
./script/maintenance/black.sh ./addons/ERPLibre_erplibre_theme_addons/
|
||||||
|
#./script/maintenance/prettier_xml.sh ./addons/ERPLibre_erplibre_theme_addons/
|
||||||
|
|
||||||
.PHONY: format_supported_addons
|
.PHONY: format_supported_addons
|
||||||
format_supported_addons:
|
format_supported_addons:
|
||||||
./script/maintenance/format_addons.sh MathBenTech_erplibre-family-management \
|
.venv.erplibre/bin/isort --profile black -l 79 ./addons/MathBenTech_erplibre-family-management/
|
||||||
MathBenTech_odoo-business-spending-management-quebec-canada
|
./script/maintenance/black.sh ./addons/MathBenTech_erplibre-family-management/
|
||||||
|
#./script/maintenance/prettier_xml.sh ./addons/MathBenTech_erplibre-family-management/
|
||||||
|
.venv.erplibre/bin/isort --profile black -l 79 ./addons/MathBenTech_odoo-business-spending-management-quebec-canada/
|
||||||
|
./script/maintenance/black.sh ./addons/MathBenTech_odoo-business-spending-management-quebec-canada/
|
||||||
|
#./script/maintenance/prettier_xml.sh ./addons/MathBenTech_erplibre-family-management/
|
||||||
|
|
||||||
.PHONY: format_code_generator_template
|
.PHONY: format_code_generator_template
|
||||||
format_code_generator_template:
|
format_code_generator_template:
|
||||||
./script/maintenance/format_addons.sh TechnoLibre_odoo-code-generator-template
|
.venv.erplibre/bin/isort --profile black -l 79 ./addons/TechnoLibre_odoo-code-generator-template/
|
||||||
|
./script/maintenance/black.sh ./addons/TechnoLibre_odoo-code-generator-template/
|
||||||
|
#./script/maintenance/prettier_xml.sh ./addons/TechnoLibre_odoo-code-generator-template/
|
||||||
|
|
||||||
# L'outillage passe par ruff, réglé une fois dans .ruff.toml, qui écarte les
|
|
||||||
# dépôts rapatriés sous script/. Les addons gardent black : voir
|
|
||||||
# script/maintenance/format_python.sh.
|
|
||||||
.PHONY: format_script
|
.PHONY: format_script
|
||||||
format_script:
|
format_script:
|
||||||
.venv.erplibre/bin/ruff check --select I --fix ./script/
|
#.venv.erplibre/bin/isort --profile black -l 79 ./script/ --gitignore
|
||||||
.venv.erplibre/bin/ruff format ./script/
|
./script/maintenance/black.sh ./script/
|
||||||
|
|
||||||
# Les tests suivent la norme de l'outillage : ils tournent dans le même venv,
|
|
||||||
# et rien ne les formatait — seul « make format » les touchait, et seulement
|
|
||||||
# s'ils étaient modifiés.
|
|
||||||
.PHONY: format_test
|
|
||||||
format_test:
|
|
||||||
.venv.erplibre/bin/ruff check --select I --fix ./test/ ./long_test/
|
|
||||||
.venv.erplibre/bin/ruff format ./test/ ./long_test/
|
|
||||||
|
|
||||||
.PHONY: format_script_isort_only
|
.PHONY: format_script_isort_only
|
||||||
format_script_isort_only:
|
format_script_isort_only:
|
||||||
.venv.erplibre/bin/ruff check --select I --fix ./script/
|
.venv.erplibre/bin/isort --profile black -l 79 ./script/ --gitignore
|
||||||
|
|
||||||
#########
|
#########
|
||||||
# log #
|
# log #
|
||||||
|
|
@ -404,9 +343,4 @@ pycharm_open:
|
||||||
|
|
||||||
.PHONY: pycharm_configure
|
.PHONY: pycharm_configure
|
||||||
pycharm_configure:
|
pycharm_configure:
|
||||||
# Le venv du dépôt, et non le python système : le script importe xmltodict, qui
|
./script/ide/pycharm_configuration.py --init
|
||||||
# vit dans .venv.erplibre. Appelé directement, il s'arrête sur
|
|
||||||
# « ModuleNotFoundError: No module named 'xmltodict' » — et c'est cette cible
|
|
||||||
# que le message d'échec de l'installation recommande. update_env_version.
|
|
||||||
# pycharm_update() l'appelle déjà ainsi : une seule autorité.
|
|
||||||
./.venv.erplibre/bin/python ./script/ide/pycharm_configuration.py --init
|
|
||||||
|
|
|
||||||
|
|
@ -86,52 +86,27 @@ Suivez-nous sur Mastodon : https://fosstodon.org/@erplibre
|
||||||
|
|
||||||
Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
||||||
|
|
||||||
The Python in that table is the one of the Odoo virtual environment. The
|
|
||||||
tooling virtual environment `.venv.erplibre` (TODO, `repo`, formatters) runs
|
|
||||||
its own interpreter, **3.14.7**, set by `conf/python-erplibre-version`. Where a
|
|
||||||
distribution does not carry it, pyenv compiles it.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
|
|
||||||
Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
||||||
|
|
||||||
Le Python de ce tableau est celui de l'environnement virtuel Odoo.
|
|
||||||
L'environnement virtuel d'outillage `.venv.erplibre` (TODO, `repo`, formateurs)
|
|
||||||
tourne sur son propre interpréteur, **3.14.7**, fixé par
|
|
||||||
`conf/python-erplibre-version`. Là où une distribution ne le porte pas, pyenv
|
|
||||||
le compile.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
# Supported platforms
|
# Supported platforms
|
||||||
|
|
||||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 and 22.04 are dropped, pikepdf requiring a qpdf 12.2 built in C++20
|
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||||
- **Linux Mint** : 22.3
|
- **macOS** : via pyenv
|
||||||
- **Debian** : 12 (bookworm) and 13 (trixie)
|
- **Windows** : via WSL or Docker
|
||||||
- **Fedora** : 41 and later
|
- **Mainframe** : 390x architecture
|
||||||
- **AlmaLinux, Rocky Linux** : 9 and 10 — RHEL and CentOS Stream take the same path
|
|
||||||
- **openSUSE** : Leap 16.0 and Tumbleweed
|
|
||||||
- **Arch Linux** : rolling release
|
|
||||||
- **NixOS** : 25.11 — the dependencies are DECLARED in `conf/nixos/erplibre.nix` and applied by `nixos-rebuild`, where the four other families install them one command at a time
|
|
||||||
- **macOS** : through mise or pyenv
|
|
||||||
- **Windows** : through WSL or Docker
|
|
||||||
- **Architectures** : amd64, arm64 and s390x (IBM Z mainframe)
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
|
|
||||||
# Plateformes supportées
|
# Plateformes supportées
|
||||||
|
|
||||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 et 22.04 sont retirées, pikepdf réclamant un qpdf 12.2 bâti en C++20
|
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||||
- **Linux Mint** : 22.3
|
- **macOS** : via pyenv
|
||||||
- **Debian** : 12 (bookworm) et 13 (trixie)
|
- **Windows** : via WSL ou Docker
|
||||||
- **Fedora** : 41 et suivantes
|
- **Mainframe** : architecture 390x
|
||||||
- **AlmaLinux, Rocky Linux** : 9 et 10 — RHEL et CentOS Stream empruntent le même chemin
|
|
||||||
- **openSUSE** : Leap 16.0 et Tumbleweed
|
|
||||||
- **Arch Linux** : rolling release
|
|
||||||
- **NixOS** : 25.11 — les dépendances y sont DÉCLARÉES dans `conf/nixos/erplibre.nix` puis appliquées par `nixos-rebuild`, là où les quatre autres familles les installent commande par commande
|
|
||||||
- **macOS** : par mise ou pyenv
|
|
||||||
- **Windows** : par WSL ou Docker
|
|
||||||
- **Architectures** : amd64, arm64 et s390x (mainframe IBM Z)
|
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
|
|
@ -254,37 +229,6 @@ make
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
`make` and `./install.sh` start TODO through the interpreter that can read it:
|
|
||||||
`.venv.erplibre` when it carries the right version, otherwise the system
|
|
||||||
`python3` when it is recent enough, otherwise the install itself — a system
|
|
||||||
older than `conf/python-erplibre-version` cannot parse the code, so it offers
|
|
||||||
the install rather than stopping on a syntax error. The install runs only on a
|
|
||||||
yes typed in a terminal (`o`, `oui`, `y` or `yes`); Enter alone, or no
|
|
||||||
terminal, means no. When the
|
|
||||||
environment is missing but the system Python suffices, TODO offers to run
|
|
||||||
`./script/install/install_erplibre.sh` (in a terminal) or prints that command. The install builds the environment
|
|
||||||
through `EL_PYTHON_PROVIDER` (mise or pyenv); an existing `.venv.erplibre` on
|
|
||||||
another Python version is DELETED and rebuilt, and whatever was installed in it
|
|
||||||
by hand goes with it.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
|
|
||||||
`make` et `./install.sh` lancent TODO par l'interpréteur capable de le lire :
|
|
||||||
`.venv.erplibre` quand il porte la bonne version, sinon le `python3` du système
|
|
||||||
s'il est assez récent, sinon l'installation elle-même — un système plus ancien
|
|
||||||
que `conf/python-erplibre-version` ne sait pas analyser le code, donc
|
|
||||||
l'installation est proposée plutôt que de s'arrêter sur une erreur de syntaxe.
|
|
||||||
Elle ne part que sur un oui tapé au terminal (`o`, `oui`, `y` ou `yes`) ;
|
|
||||||
Entrée seule, ou l'absence de terminal, vaut non. Quand
|
|
||||||
l'environnement manque mais que le Python du système suffit, TODO propose de
|
|
||||||
lancer `./script/install/install_erplibre.sh` (dans un terminal) ou affiche
|
|
||||||
cette commande. L'installation bâtit l'environnement par
|
|
||||||
`EL_PYTHON_PROVIDER` (mise ou pyenv) ; un `.venv.erplibre` existant sur une
|
|
||||||
autre version de Python est SUPPRIMÉ puis rebâti, et ce qu'on y avait posé à la
|
|
||||||
main part avec lui.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
|
|
||||||
Into Ubuntu, minimal dependency:
|
Into Ubuntu, minimal dependency:
|
||||||
|
|
@ -332,11 +276,11 @@ cd ERPLibre
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
`make install_os` detects the distribution and picks the right dependency script: apt for Ubuntu, Linux Mint and Debian, dnf for Fedora and the RHEL family, zypper for openSUSE, pacman for Arch, and a declarative module for NixOS. See the supported platforms above.
|
Support Ubuntu 20.04, 22.04, 24.04, 25.04 and OSX.
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
|
|
||||||
`make install_os` détecte la distribution et choisit le bon script de dépendances : apt pour Ubuntu, Linux Mint et Debian, dnf pour Fedora et la famille RHEL, zypper pour openSUSE, pacman pour Arch, et un module déclaratif pour NixOS. Voir les plateformes supportées ci-dessus.
|
Support Ubuntu 20.04, 22.04, 24.04, 25.04 et OSX.
|
||||||
|
|
||||||
<!-- [common] -->
|
<!-- [common] -->
|
||||||
|
|
||||||
|
|
|
||||||
38
README.fr.md
38
README.fr.md
|
|
@ -44,26 +44,13 @@ Suivez-nous sur Mastodon : https://fosstodon.org/@erplibre
|
||||||
|
|
||||||
Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
||||||
|
|
||||||
Le Python de ce tableau est celui de l'environnement virtuel Odoo.
|
|
||||||
L'environnement virtuel d'outillage `.venv.erplibre` (TODO, `repo`, formateurs)
|
|
||||||
tourne sur son propre interpréteur, **3.14.7**, fixé par
|
|
||||||
`conf/python-erplibre-version`. Là où une distribution ne le porte pas, pyenv
|
|
||||||
le compile.
|
|
||||||
|
|
||||||
|
|
||||||
# Plateformes supportées
|
# Plateformes supportées
|
||||||
|
|
||||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 et 22.04 sont retirées, pikepdf réclamant un qpdf 12.2 bâti en C++20
|
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||||
- **Linux Mint** : 22.3
|
- **macOS** : via pyenv
|
||||||
- **Debian** : 12 (bookworm) et 13 (trixie)
|
- **Windows** : via WSL ou Docker
|
||||||
- **Fedora** : 41 et suivantes
|
- **Mainframe** : architecture 390x
|
||||||
- **AlmaLinux, Rocky Linux** : 9 et 10 — RHEL et CentOS Stream empruntent le même chemin
|
|
||||||
- **openSUSE** : Leap 16.0 et Tumbleweed
|
|
||||||
- **Arch Linux** : rolling release
|
|
||||||
- **NixOS** : 25.11 — les dépendances y sont DÉCLARÉES dans `conf/nixos/erplibre.nix` puis appliquées par `nixos-rebuild`, là où les quatre autres familles les installent commande par commande
|
|
||||||
- **macOS** : par mise ou pyenv
|
|
||||||
- **Windows** : par WSL ou Docker
|
|
||||||
- **Architectures** : amd64, arm64 et s390x (mainframe IBM Z)
|
|
||||||
|
|
||||||
|
|
||||||
# Installation en production
|
# Installation en production
|
||||||
|
|
@ -126,21 +113,6 @@ make
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
`make` et `./install.sh` lancent TODO par l'interpréteur capable de le lire :
|
|
||||||
`.venv.erplibre` quand il porte la bonne version, sinon le `python3` du système
|
|
||||||
s'il est assez récent, sinon l'installation elle-même — un système plus ancien
|
|
||||||
que `conf/python-erplibre-version` ne sait pas analyser le code, donc
|
|
||||||
l'installation est proposée plutôt que de s'arrêter sur une erreur de syntaxe.
|
|
||||||
Elle ne part que sur un oui tapé au terminal (`o`, `oui`, `y` ou `yes`) ;
|
|
||||||
Entrée seule, ou l'absence de terminal, vaut non. Quand
|
|
||||||
l'environnement manque mais que le Python du système suffit, TODO propose de
|
|
||||||
lancer `./script/install/install_erplibre.sh` (dans un terminal) ou affiche
|
|
||||||
cette commande. L'installation bâtit l'environnement par
|
|
||||||
`EL_PYTHON_PROVIDER` (mise ou pyenv) ; un `.venv.erplibre` existant sur une
|
|
||||||
autre version de Python est SUPPRIMÉ puis rebâti, et ce qu'on y avait posé à la
|
|
||||||
main part avec lui.
|
|
||||||
|
|
||||||
|
|
||||||
### Manuellement
|
### Manuellement
|
||||||
|
|
||||||
Sous Ubuntu, dépendance minimale :
|
Sous Ubuntu, dépendance minimale :
|
||||||
|
|
@ -168,7 +140,7 @@ cd ERPLibre
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
`make install_os` détecte la distribution et choisit le bon script de dépendances : apt pour Ubuntu, Linux Mint et Debian, dnf pour Fedora et la famille RHEL, zypper pour openSUSE, pacman pour Arch, et un module déclaratif pour NixOS. Voir les plateformes supportées ci-dessus.
|
Support Ubuntu 20.04, 22.04, 24.04, 25.04 et OSX.
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
36
README.md
36
README.md
|
|
@ -43,25 +43,13 @@ Follow us on Mastodon : https://fosstodon.org/@erplibre
|
||||||
|
|
||||||
Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
||||||
|
|
||||||
The Python in that table is the one of the Odoo virtual environment. The
|
|
||||||
tooling virtual environment `.venv.erplibre` (TODO, `repo`, formatters) runs
|
|
||||||
its own interpreter, **3.14.7**, set by `conf/python-erplibre-version`. Where a
|
|
||||||
distribution does not carry it, pyenv compiles it.
|
|
||||||
|
|
||||||
|
|
||||||
# Supported platforms
|
# Supported platforms
|
||||||
|
|
||||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 and 22.04 are dropped, pikepdf requiring a qpdf 12.2 built in C++20
|
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||||
- **Linux Mint** : 22.3
|
- **macOS** : via pyenv
|
||||||
- **Debian** : 12 (bookworm) and 13 (trixie)
|
- **Windows** : via WSL or Docker
|
||||||
- **Fedora** : 41 and later
|
- **Mainframe** : 390x architecture
|
||||||
- **AlmaLinux, Rocky Linux** : 9 and 10 — RHEL and CentOS Stream take the same path
|
|
||||||
- **openSUSE** : Leap 16.0 and Tumbleweed
|
|
||||||
- **Arch Linux** : rolling release
|
|
||||||
- **NixOS** : 25.11 — the dependencies are DECLARED in `conf/nixos/erplibre.nix` and applied by `nixos-rebuild`, where the four other families install them one command at a time
|
|
||||||
- **macOS** : through mise or pyenv
|
|
||||||
- **Windows** : through WSL or Docker
|
|
||||||
- **Architectures** : amd64, arm64 and s390x (IBM Z mainframe)
|
|
||||||
|
|
||||||
|
|
||||||
# Installation in production
|
# Installation in production
|
||||||
|
|
@ -124,20 +112,6 @@ make
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
`make` and `./install.sh` start TODO through the interpreter that can read it:
|
|
||||||
`.venv.erplibre` when it carries the right version, otherwise the system
|
|
||||||
`python3` when it is recent enough, otherwise the install itself — a system
|
|
||||||
older than `conf/python-erplibre-version` cannot parse the code, so it offers
|
|
||||||
the install rather than stopping on a syntax error. The install runs only on a
|
|
||||||
yes typed in a terminal (`o`, `oui`, `y` or `yes`); Enter alone, or no
|
|
||||||
terminal, means no. When the
|
|
||||||
environment is missing but the system Python suffices, TODO offers to run
|
|
||||||
`./script/install/install_erplibre.sh` (in a terminal) or prints that command. The install builds the environment
|
|
||||||
through `EL_PYTHON_PROVIDER` (mise or pyenv); an existing `.venv.erplibre` on
|
|
||||||
another Python version is DELETED and rebuilt, and whatever was installed in it
|
|
||||||
by hand goes with it.
|
|
||||||
|
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
|
|
||||||
Into Ubuntu, minimal dependency:
|
Into Ubuntu, minimal dependency:
|
||||||
|
|
@ -165,7 +139,7 @@ cd ERPLibre
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
`make install_os` detects the distribution and picks the right dependency script: apt for Ubuntu, Linux Mint and Debian, dnf for Fedora and the RHEL family, zypper for openSUSE, pacman for Arch, and a declarative module for NixOS. See the supported platforms above.
|
Support Ubuntu 20.04, 22.04, 24.04, 25.04 and OSX.
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -4,31 +4,6 @@
|
||||||
|
|
||||||
# TODO load specific test file : ./run.sh -d test_file --log-level=test --test-enable --stop-after-init --test-file ./.venv.erplibre/test.py
|
# TODO load specific test file : ./run.sh -d test_file --log-level=test --test-enable --stop-after-init --test-file ./.venv.erplibre/test.py
|
||||||
|
|
||||||
###########################
|
|
||||||
# Tests unitaires python #
|
|
||||||
###########################
|
|
||||||
# Ni base de données, ni Odoo, ni VM : ils lisent le code et exécutent les
|
|
||||||
# fragments de shell que todo.py génère, « sudo », « pgrep » et « pkill »
|
|
||||||
# bouchonnés. Une dizaine de secondes, là où « make test » demande une base et
|
|
||||||
# plusieurs minutes — d'où une cible à part, faite pour être lancée souvent.
|
|
||||||
#
|
|
||||||
# DÉPENDANCE : les tests du transfert mobile lisent
|
|
||||||
# mobile/erplibre_home_mobile. Le lanceur l'annonce présent ou absent avant de
|
|
||||||
# commencer, et les tests concernés se déclarent ignorés plutôt que de passer
|
|
||||||
# en silence — un test vert sans son dépôt ne prouve rien.
|
|
||||||
#
|
|
||||||
# « test_todo.py » n'en fait PAS partie : il demande une base de données. Son
|
|
||||||
# nom sans souligné le laisse hors du motif, et c'est voulu.
|
|
||||||
.PHONY: test_unit
|
|
||||||
test_unit:
|
|
||||||
./script/test/run_unit_test.sh
|
|
||||||
|
|
||||||
# Un seul fichier, pour la boucle d'écriture :
|
|
||||||
# make test_unit_file F=test/test_qemu_forgejo.py
|
|
||||||
.PHONY: test_unit_file
|
|
||||||
test_unit_file:
|
|
||||||
./script/test/run_unit_test.sh $(F)
|
|
||||||
|
|
||||||
.PHONY: open_test_coverage
|
.PHONY: open_test_coverage
|
||||||
open_test_coverage:
|
open_test_coverage:
|
||||||
-$(BROWSER) htmlcov/index.html
|
-$(BROWSER) htmlcov/index.html
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
# TODO #
|
# TODO #
|
||||||
########
|
########
|
||||||
|
|
||||||
# Par todo.sh, et non todo.py directement : la chaîne passe par install.sh,
|
|
||||||
# qui choisit un interpréteur capable de LIRE le code avant de le lancer et
|
|
||||||
# pose le venv s'il manque. Le nom dit ce qui se passe : make affiche la
|
|
||||||
# recette, et « ./install.sh » y donnait à lire une installation là où l'on
|
|
||||||
# ouvre un menu.
|
|
||||||
.PHONY: todo
|
.PHONY: todo
|
||||||
todo:
|
todo:
|
||||||
./todo.sh
|
./script/todo/todo.py
|
||||||
|
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
||||||
#
|
|
||||||
# Dépendances système d'ERPLibre, pour NixOS.
|
|
||||||
#
|
|
||||||
# Le pendant DÉCLARATIF des quatre install_<distro>_dependency.sh. Ceux-là
|
|
||||||
# posent des paquets par apt, dnf, pacman ou zypper ; ici rien ne s'installe
|
|
||||||
# par une commande — ce qui doit rester se déclare, et ce fichier est cette
|
|
||||||
# déclaration. Un « nixos-rebuild switch » l'applique ; ce qui aurait été posé
|
|
||||||
# à la main disparaîtrait à la reconstruction suivante.
|
|
||||||
#
|
|
||||||
# Déposé en /etc/nixos/erplibre.nix par script/install/install_nixos_dependency.sh,
|
|
||||||
# qui l'ajoute aussi aux « imports » de la configuration de la machine.
|
|
||||||
#
|
|
||||||
# DEUX options portent tout le reste, et sans elles rien de ce dépôt ne
|
|
||||||
# fonctionne sur NixOS :
|
|
||||||
#
|
|
||||||
# services.envfs.enable — NixOS ne peuple pas /bin ni /usr/bin. Or le
|
|
||||||
# Makefile force « SHELL := /bin/bash » (toute cible make échouerait avant
|
|
||||||
# sa première ligne) et lib_python_provider.sh ne cherche l'interpréteur du
|
|
||||||
# système qu'en /usr/bin/pythonX.Y, jamais dans le PATH. envfs fabrique ces
|
|
||||||
# deux répertoires à la volée depuis le PATH : les scripts du dépôt marchent
|
|
||||||
# alors sans être réécrits.
|
|
||||||
#
|
|
||||||
# programs.nix-ld.enable — les binaires téléchargés (roues manylinux de pip,
|
|
||||||
# CPython précompilé de mise, archives amont) sont liés à
|
|
||||||
# /lib64/ld-linux-x86-64.so.2, qui n'existe pas ici. nix-ld le fournit et
|
|
||||||
# leur donne les bibliothèques listées plus bas.
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
# ── Ce que /bin, /usr/bin et l'éditeur de liens doivent porter ──────────
|
|
||||||
services.envfs.enable = true;
|
|
||||||
|
|
||||||
programs.nix-ld.enable = true;
|
|
||||||
# Les bibliothèques que réclament les roues manylinux d'Odoo : psycopg2 veut
|
|
||||||
# libpq, lxml veut libxml2/libxslt, cryptography veut openssl, pillow veut
|
|
||||||
# zlib et freetype. Une roue qui n'en trouve pas une échoue à l'IMPORT, pas
|
|
||||||
# à l'installation — donc bien après, et sans rapport apparent.
|
|
||||||
programs.nix-ld.libraries = with pkgs; [
|
|
||||||
stdenv.cc.cc.lib
|
|
||||||
zlib
|
|
||||||
openssl
|
|
||||||
libxml2
|
|
||||||
libxslt
|
|
||||||
libffi
|
|
||||||
libpq
|
|
||||||
freetype
|
|
||||||
fontconfig
|
|
||||||
libjpeg
|
|
||||||
glib
|
|
||||||
expat
|
|
||||||
bzip2
|
|
||||||
xz
|
|
||||||
ncurses
|
|
||||||
readline
|
|
||||||
sqlite
|
|
||||||
util-linux
|
|
||||||
openldap
|
|
||||||
cyrus_sasl
|
|
||||||
cups
|
|
||||||
libmysqlclient
|
|
||||||
];
|
|
||||||
|
|
||||||
# ── La base de données ───────────────────────────────────────────────────
|
|
||||||
# Le compte de service est SUPERUSER comme sur les autres distributions :
|
|
||||||
# ERPLibre crée et détruit des bases (migrations, copies neutralisées), ce
|
|
||||||
# qu'un rôle ordinaire ne peut pas faire.
|
|
||||||
services.postgresql = {
|
|
||||||
enable = true;
|
|
||||||
# PostGIS comme les quatre autres scripts de distribution le posent. Ici
|
|
||||||
# c'est une extension DU SERVEUR, pas un paquet du système : déclarée
|
|
||||||
# ailleurs, elle ne serait pas chargeable par « CREATE EXTENSION ».
|
|
||||||
extensions = ps: with ps; [ postgis ];
|
|
||||||
ensureUsers = [
|
|
||||||
{
|
|
||||||
name = "@EL_USER@";
|
|
||||||
ensureClauses.superuser = true;
|
|
||||||
ensureClauses.createdb = true;
|
|
||||||
ensureClauses.login = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# ── Les outils ───────────────────────────────────────────────────────────
|
|
||||||
# Les DEUX Python du dépôt, substitués depuis ses fichiers de version :
|
|
||||||
# celui d'Odoo (.python-odoo-version) et celui de l'outillage
|
|
||||||
# (conf/python-erplibre-version), qui ne sont plus la même version. envfs
|
|
||||||
# les rend visibles en /usr/bin/python3.X, où lib_python_provider.sh les
|
|
||||||
# cherche — ni mise ni pyenv n'ont alors rien à compiler. Déclarer le seul
|
|
||||||
# Python d'Odoo laissait pyenv bâtir l'autre, et la compilation de CPython
|
|
||||||
# s'arrête ici sur « Modules/_cursesmodule.o ».
|
|
||||||
#
|
|
||||||
# Le second marqueur est VIDE quand les deux versions coïncident : nommer
|
|
||||||
# deux fois le même paquet ferait entrer en collision deux chemins
|
|
||||||
# identiques dans le profil.
|
|
||||||
#
|
|
||||||
# Les sorties « .dev » portent les en-têtes : sans elles, une roue absente
|
|
||||||
# du dépôt amont devrait se compiler et ne trouverait ni libpq-fe.h ni
|
|
||||||
# openssl/ssl.h. Elles ne servent qu'à ce cas, et ne coûtent que du disque.
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
@EL_PY_ODOO_PKG@
|
|
||||||
@EL_PY_ODOO_PKG@Packages.pip
|
|
||||||
@EL_PY_ODOO_PKG@Packages.virtualenv
|
|
||||||
@EL_PY_TOOLS_PKG@
|
|
||||||
uv
|
|
||||||
nodejs_22
|
|
||||||
postgresql
|
|
||||||
postgresql.dev
|
|
||||||
git
|
|
||||||
gnumake
|
|
||||||
gcc
|
|
||||||
pkg-config
|
|
||||||
openssl
|
|
||||||
openssl.dev
|
|
||||||
zlib
|
|
||||||
zlib.dev
|
|
||||||
libxml2
|
|
||||||
libxml2.dev
|
|
||||||
libxslt
|
|
||||||
libxslt.dev
|
|
||||||
libffi
|
|
||||||
libffi.dev
|
|
||||||
# python-ldap n'a PAS de roue amont : il compile, et réclame lber.h
|
|
||||||
# (openldap) plus sasl.h. Sans ces sorties « .dev », « poetry install »
|
|
||||||
# s'arrête sur « fatal error: lber.h: No such file or directory ».
|
|
||||||
openldap
|
|
||||||
openldap.dev
|
|
||||||
cyrus_sasl
|
|
||||||
cyrus_sasl.dev
|
|
||||||
# « pg_config » est une dérivation à PART dans nixpkgs : il n'est ni dans
|
|
||||||
# postgresql ni dans sa sortie « .dev », et psycopg2 s'arrête sur
|
|
||||||
# « Error: pg_config executable not found ».
|
|
||||||
postgresql.pg_config
|
|
||||||
# pycups veut cups/http.h, mysqlclient veut mysql.h. Les quatre autres
|
|
||||||
# scripts posent les mêmes (libcups2-dev, cups-devel,
|
|
||||||
# mariadb-connector-c-devel, mariadb-libs).
|
|
||||||
cups
|
|
||||||
# « cups.lib » porte libcups.so, que « out » n'a pas : sans elle la
|
|
||||||
# compilation de pycups PASSE et l'édition de liens échoue sur « -lcups ».
|
|
||||||
cups.lib
|
|
||||||
cups.dev
|
|
||||||
libmysqlclient
|
|
||||||
libmysqlclient.dev
|
|
||||||
# « less » est le PAGINATEUR ; « nodePackages.less » est lessc, le
|
|
||||||
# compilateur LESS des assets Odoo. Les quatre autres scripts les posent
|
|
||||||
# tous deux, l'un par le gestionnaire du système et l'autre par « npm
|
|
||||||
# install -g » — geste impossible ici, le préfixe npm étant le store, en
|
|
||||||
# lecture seule. nixpkgs les porte : ils se déclarent comme le reste.
|
|
||||||
less
|
|
||||||
nodePackages.less
|
|
||||||
nodePackages.rtlcss
|
|
||||||
sshpass
|
|
||||||
curl
|
|
||||||
wget
|
|
||||||
unzip
|
|
||||||
wkhtmltopdf
|
|
||||||
# « xmlsec » pose bin/xmlsec1, que les CINQ autres plateformes posent
|
|
||||||
# aussi. Le manifeste d'auth_saml (OCA server-auth, présent dans
|
|
||||||
# l'addons_path) déclare « bin: ["xmlsec1"] », et Odoo REFUSE d'installer
|
|
||||||
# ou de mettre à niveau le module tant que le binaire n'est pas dans le
|
|
||||||
# PATH : « Unable to find 'xmlsec1' in path », en boîte de dialogue.
|
|
||||||
xmlsec
|
|
||||||
# « parallel » et « shfmt » sont appelés par leur NOM NU, l'un par
|
|
||||||
# script/database/db_drop_all.py, l'autre par script/maintenance/
|
|
||||||
# format_bash.sh. Sans parallel, « make db_drop_all » annonce des bases
|
|
||||||
# détruites qui ne l'ont pas été — une opération destructrice qui rend un
|
|
||||||
# succès qu'elle n'a pas obtenu.
|
|
||||||
parallel
|
|
||||||
shfmt
|
|
||||||
# « cloud-utils » porte growpart, qu'aucune autre voie ne fournit ici.
|
|
||||||
# L'agrandissement du disque s'écrit « sudo growpart … || true » : sans le
|
|
||||||
# binaire, il rend 0 sans rien agrandir, et la VM garde la taille de son
|
|
||||||
# image pendant que le déploiement annonce la taille demandée.
|
|
||||||
cloud-utils
|
|
||||||
];
|
|
||||||
|
|
||||||
# Le profil du système ne porte PAS « /include » : la liste par défaut de
|
|
||||||
# ce qui y est lié ne contient ni les en-têtes ni les fichiers pkg-config.
|
|
||||||
# Sans cette ligne, déclarer une sortie « .dev » ne met rien nulle part, et
|
|
||||||
# « fatal error: lber.h: No such file or directory » reste entier.
|
|
||||||
environment.pathsToLink = [ "/include" "/lib/pkgconfig" ];
|
|
||||||
|
|
||||||
# Les manuels HTML, et EUX SEULS, sont écartés.
|
|
||||||
#
|
|
||||||
# NixOS installe la sortie « doc » de CHAQUE paquet du système
|
|
||||||
# (environment.extraOutputsToInstall vaut « man info doc »). Celle de
|
|
||||||
# CPython n'est pas dans le cache binaire : le premier « nixos-rebuild »
|
|
||||||
# la BÂTIT — un Sphinx qui lit puis écrit 3 000 pages. Sur une VM de 4 Go
|
|
||||||
# et 4 cœurs, cela domine le temps d'installation ; sur une de 2 Go, la
|
|
||||||
# machine cesse de répondre pendant la construction.
|
|
||||||
#
|
|
||||||
# « documentation.doc » et non « documentation » : les pages de manuel et
|
|
||||||
# info restent, elles se lisent depuis un terminal et ne coûtent rien.
|
|
||||||
documentation.doc.enable = false;
|
|
||||||
|
|
||||||
# « sessionVariables » et NON « variables » : la seconde n'écrit que dans
|
|
||||||
# /etc/set-environment, que seul un shell de CONNEXION lit. Or le
|
|
||||||
# déploiement installe par « ssh hôte 'commande' », qui n'en est pas un —
|
|
||||||
# la variable y serait vide. sessionVariables passe par pam_env, que toute
|
|
||||||
# session traverse, y compris celle-là.
|
|
||||||
environment.sessionVariables = {
|
|
||||||
CPATH = "/run/current-system/sw/include";
|
|
||||||
LIBRARY_PATH = "/run/current-system/sw/lib";
|
|
||||||
PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig";
|
|
||||||
} // lib.optionalAttrs ("@EL_CA_BUNDLE@" != "") {
|
|
||||||
# L'autorité du cache de téléchargement, pour TOUTE session d'après
|
|
||||||
# l'installation — un « git pull » échouerait sinon sur un certificat
|
|
||||||
# qu'il ne reconnaît pas, comme le clone avant elle.
|
|
||||||
#
|
|
||||||
# La commande d'installation porte ces mêmes variables elle-même : elle
|
|
||||||
# tourne AVANT la première reconstruction, et aucun chemin PAM n'est
|
|
||||||
# inscriptible d'ici là. Les deux moitiés se relaient.
|
|
||||||
#
|
|
||||||
# « optionalAttrs » et non une valeur de repli : sur une machine sans
|
|
||||||
# cache le faisceau n'existe pas, et y pointer SSL_CERT_FILE couperait
|
|
||||||
# TLS partout.
|
|
||||||
SSL_CERT_FILE = "@EL_CA_BUNDLE@";
|
|
||||||
CURL_CA_BUNDLE = "@EL_CA_BUNDLE@";
|
|
||||||
GIT_SSL_CAINFO = "@EL_CA_BUNDLE@";
|
|
||||||
REQUESTS_CA_BUNDLE = "@EL_CA_BUNDLE@";
|
|
||||||
NODE_EXTRA_CA_CERTS = "@EL_CA_BUNDLE@";
|
|
||||||
PIP_CERT = "@EL_CA_BUNDLE@";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Les réglages régionaux demandés au déploiement.
|
|
||||||
#
|
|
||||||
# Le FUSEAU marche déjà sans cela : cloud-init pose /etc/localtime, que
|
|
||||||
# NixOS laisse mutable tant que « time.timeZone » n'est pas déclaré.
|
|
||||||
# Mesuré sur une VM installée — le lien pointe bien la zone demandée. Le
|
|
||||||
# déclarer ne répare donc rien ; il fait passer la garantie du côté du
|
|
||||||
# module, comme pour l'agent invité.
|
|
||||||
#
|
|
||||||
# La LOCALE, elle, ne marchait pas : cloud-init l'applique par locale-gen
|
|
||||||
# et update-locale, qui n'existent pas ici, et la VM gardait le défaut de
|
|
||||||
# NixOS. Mesuré : « fr_CA.UTF-8 » demandé, « en_US.UTF-8 » obtenu.
|
|
||||||
#
|
|
||||||
# « mkIf » plutôt qu'une valeur de repli : sur une machine où rien n'a été
|
|
||||||
# demandé — une NixOS que l'on avait déjà, installée par « --hote » —
|
|
||||||
# l'option n'est PAS définie, et le réglage de son propriétaire reste.
|
|
||||||
# Écrire un défaut ici l'écraserait sans le dire.
|
|
||||||
#
|
|
||||||
# CE QUE CELA COÛTE, mesuré : une locale autre que celle du défaut change
|
|
||||||
# l'ensemble des locales prises en charge, donc la dérivation de
|
|
||||||
# glibc-locales, qui n'est alors pas dans le cache binaire et se BÂTIT. La
|
|
||||||
# première reconstruction est longue, et silencieuse — il vaut mieux le
|
|
||||||
# savoir que de la prendre pour un blocage.
|
|
||||||
i18n.defaultLocale = lib.mkIf ("@EL_LOCALE@" != "") "@EL_LOCALE@";
|
|
||||||
time.timeZone = lib.mkIf ("@EL_TZ@" != "") "@EL_TZ@";
|
|
||||||
|
|
||||||
# Le guide de connexion, AFFICHÉ.
|
|
||||||
#
|
|
||||||
# Le déploiement écrit /etc/motd dans toutes les distributions, et compte
|
|
||||||
# sur pam_motd pour le montrer — c'est vrai des quatre images cloud, où
|
|
||||||
# sshd est en « PrintMotd no » et où ajouter l'inverse afficherait le guide
|
|
||||||
# DEUX FOIS. Ici, ni l'un ni l'autre : mesuré sur une VM installée, sshd
|
|
||||||
# rend « printmotd no » et /etc/pam.d/sshd ne contient AUCUN pam_motd. Le
|
|
||||||
# fichier est donc écrit, complet, et personne ne le lit.
|
|
||||||
#
|
|
||||||
# sshd et non pam_motd : le double affichage qu'on redoute ailleurs ne peut
|
|
||||||
# pas se produire tant que le PAM d'ici n'en contient pas, et c'est le seul
|
|
||||||
# des deux qui ne demande rien de plus que cette ligne.
|
|
||||||
services.openssh.settings.PrintMotd = true;
|
|
||||||
|
|
||||||
# L'agent invité, DÉCLARÉ ici plutôt que reçu de l'image.
|
|
||||||
#
|
|
||||||
# L'image épinglée l'active déjà (son configuration.nix porte la ligne), et
|
|
||||||
# c'est précisément le problème : la garantie appartient alors au tiers qui
|
|
||||||
# rebâtit l'image, pas au dépôt. Les quatre autres distributions reçoivent
|
|
||||||
# l'agent par le runcmd du déploiement, qui appelle leur gestionnaire de
|
|
||||||
# paquets — geste impossible sur un système déclaratif. Ici, c'est cette
|
|
||||||
# ligne ou rien.
|
|
||||||
#
|
|
||||||
# Sans agent, la voie Proxmox perd sa source PRIMAIRE d'adresse et retombe
|
|
||||||
# sur « ip neigh ». Déclarer l'option deux fois est sans effet : une option
|
|
||||||
# booléenne ne rompt l'évaluation que sur des valeurs DIFFÉRENTES. Hors
|
|
||||||
# QEMU, l'unité ne démarre pas — elle n'a pas de section [Install] et
|
|
||||||
# attend une règle udev sur le port virtio.
|
|
||||||
services.qemuGuest.enable = true;
|
|
||||||
|
|
||||||
# Ce que l'agent doit trouver dans son PATH.
|
|
||||||
#
|
|
||||||
# Une unité systemd n'hérite pas du profil du système : celle-ci ne porte
|
|
||||||
# que coreutils, findutils, grep, sed et systemd. Or « guest-exec » exécute
|
|
||||||
# les commandes du produit DANS ce PATH, et l'agrandissement du disque
|
|
||||||
# commence par « findmnt -no SOURCE / » — absent, donc code 127 dès la
|
|
||||||
# première ligne.
|
|
||||||
systemd.services.qemu-guest-agent.path = with pkgs; [
|
|
||||||
util-linux
|
|
||||||
e2fsprogs
|
|
||||||
cloud-utils
|
|
||||||
];
|
|
||||||
|
|
||||||
# Le port d'Odoo, OUVERT.
|
|
||||||
#
|
|
||||||
# NixOS active un pare-feu par défaut ; aucune des images cloud des quatre
|
|
||||||
# autres distributions n'en active un. Le service écoute bien sur
|
|
||||||
# 0.0.0.0:8069 et répond en local, mais l'extérieur ne reçoit RIEN — pas un
|
|
||||||
# refus, un silence, donc une attente jusqu'au délai. Ce qui sonde depuis
|
|
||||||
# l'hôte conclut « Odoo absent » sur une machine où il tourne, et le journal
|
|
||||||
# de l'installation ne porte aucune trace de la cause : elle est dans le
|
|
||||||
# pare-feu, pas dans l'application.
|
|
||||||
#
|
|
||||||
# 8069 SEUL. Le port websocket est configuré à 8072, mais Odoo ne le lie
|
|
||||||
# qu'en mode multi-processus, que cette configuration n'emploie pas : rien
|
|
||||||
# n'y écoute, et l'ouvrir donnerait un port béant sans service derrière.
|
|
||||||
# PostgreSQL n'écoute déjà que sur la boucle locale et n'a rien à ouvrir.
|
|
||||||
networking.firewall.allowedTCPPorts = [ 8069 ];
|
|
||||||
|
|
||||||
# Le service ERPLibre, DÉCLARÉ et non écrit.
|
|
||||||
#
|
|
||||||
# Sur toute autre distribution l'installation dépose l'unité par
|
|
||||||
# « tee /etc/systemd/system/erplibre.service ». Ici /etc est généré depuis
|
|
||||||
# le store et monté en lecture seule : le tee échoue sur « Read-only file
|
|
||||||
# system », et l'installation entière rend 1 à sa dernière étape, après que
|
|
||||||
# tout le reste a réussi.
|
|
||||||
#
|
|
||||||
# « wantedBy » est l'équivalent déclaratif de « systemctl enable » :
|
|
||||||
# l'activation par lien symbolique écrirait elle aussi dans /etc.
|
|
||||||
#
|
|
||||||
# L'interpréteur vient du STORE et non de /bin. /bin et /usr/bin sont ici
|
|
||||||
# un montage FUSE d'envfs, et systemd résout l'exécutable d'ExecStart
|
|
||||||
# lui-même, hors de portée de ce montage : « /bin/bash » y rend
|
|
||||||
# « 203/EXEC, Unable to locate executable ». Avec Restart=always, l'unité
|
|
||||||
# boucle alors indéfiniment. La même raison vaut pour /usr/bin/env, donc
|
|
||||||
# le shebang de run.sh ne suffirait pas davantage.
|
|
||||||
#
|
|
||||||
# La première reconstruction déclare le service AVANT qu'ERPLibre ne soit
|
|
||||||
# installé, et son démarrage échoue alors — c'est attendu, et c'est
|
|
||||||
# exactement le cas que « nixos-rebuild rend 4 » recouvre. L'installation
|
|
||||||
# le relance une fois le dépôt en place.
|
|
||||||
systemd.services.erplibre = {
|
|
||||||
description = "ERPLibre";
|
|
||||||
requires = [ "postgresql.service" ];
|
|
||||||
after = [ "network.target" "network-online.target" "postgresql.service" ];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
# Une unité systemd ne reçoit PAS le PATH d'une session : le sien ne
|
|
||||||
# porte que coreutils, findutils, grep, sed et systemd.
|
|
||||||
#
|
|
||||||
# bash — run.sh lance odoo_bin.sh et lib_db_select.sh, dont le shebang est
|
|
||||||
# « #!/usr/bin/env bash ». env est là, bash non : « env: 'bash': No such
|
|
||||||
# file or directory », et run.sh s'arrête avant Odoo.
|
|
||||||
#
|
|
||||||
# python3 — la sonde de réveil tourne AVANT que odoo_bin.sh n'active le
|
|
||||||
# venv, donc avec le python du système. Son échec est silencieux
|
|
||||||
# (« 2>/dev/null ») : sans elle, la première page ouverte attendrait le
|
|
||||||
# chargement du registre sans que rien ne le dise.
|
|
||||||
path = with pkgs; [ bash @EL_PY_ODOO_PKG@ ];
|
|
||||||
# L'unité est déclarée par le module, donc démarrée par la
|
|
||||||
# reconstruction — qui a lieu PENDANT « make install_os », alors que la
|
|
||||||
# source d'Odoo n'arrive qu'à « make install_odoo_18 ». Sans condition,
|
|
||||||
# run.sh échoue sur un odoo-bin absent et « Restart = always » le rejoue
|
|
||||||
# toutes les cinq secondes jusqu'à ce que l'installation le pose — vingt
|
|
||||||
# et un échecs mesurés sur une pose ordinaire, et un « nixos-rebuild »
|
|
||||||
# qui rend 4 parce qu'une unité n'a pas démarré.
|
|
||||||
#
|
|
||||||
# Une CONDITION, et non une dépendance : systemd saute l'unité en le
|
|
||||||
# disant une fois, sans la marquer en échec, et la démarre d'elle-même
|
|
||||||
# au prochain déclenchement une fois le fichier là. Le motif évite de
|
|
||||||
# figer la version d'Odoo ici, où elle vieillirait en silence.
|
|
||||||
unitConfig.ConditionPathExistsGlob = "@EL_DIR@/odoo*/odoo/odoo-bin";
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "simple";
|
|
||||||
User = "@EL_USER@";
|
|
||||||
WorkingDirectory = "@EL_DIR@";
|
|
||||||
ExecStart = "${pkgs.bash}/bin/bash @EL_DIR@/run.sh";
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = 5;
|
|
||||||
StandardOutput = "journal+console";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
3.14.7
|
3.12.10
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# Norme OCA pour les MODULES ODOO, calquée sur le gabarit
|
|
||||||
# oca-addons-repo-template — celui que porte tout dépôt d'addons de la
|
|
||||||
# communauté, et qu'on lit dans odoo18.0/OCA_OpenUpgrade/.ruff.toml.
|
|
||||||
#
|
|
||||||
# OCA a quitté black : ruff-format formate, et ruff remplace d'un coup isort,
|
|
||||||
# flake8 et pyupgrade. L'outillage d'ERPLibre suit sa propre norme, plus
|
|
||||||
# étroite — voir .ruff.toml à la racine, et format_python.sh qui aiguille.
|
|
||||||
#
|
|
||||||
# Un dépôt d'addons qui porte SON PROPRE .ruff.toml gagne sur ce fichier :
|
|
||||||
# format_addons.sh le laisse décider chez lui.
|
|
||||||
|
|
||||||
# La série Odoo décide, comme chez OCA, qui fixe une cible par branche. Cette
|
|
||||||
# valeur n'est qu'un repli : format_addons.sh passe celle de
|
|
||||||
# .python-odoo-version, la seule qui dise ce qui EXÉCUTE ces modules.
|
|
||||||
target-version = "py310"
|
|
||||||
# 88, la largeur d'OCA, et non les 79 de l'outillage.
|
|
||||||
line-length = 88
|
|
||||||
|
|
||||||
[lint]
|
|
||||||
extend-select = [
|
|
||||||
"B", # bugbear
|
|
||||||
"C90", # complexité
|
|
||||||
"E501", # ligne trop longue
|
|
||||||
"I", # tri des imports
|
|
||||||
"UP", # pyupgrade
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
|
||||||
# Un __init__.py d'addon liste ses modules : ni inutilisés, ni à trier.
|
|
||||||
"__init__.py" = ["F401", "I001"]
|
|
||||||
# Un manifeste est un dictionnaire nu, que bugbear prend pour une expression
|
|
||||||
# sans effet.
|
|
||||||
"__manifest__.py" = ["B018"]
|
|
||||||
|
|
||||||
[lint.isort]
|
|
||||||
# Ce qu'isort ne sait pas faire : « odoo » et « odoo.addons » sont deux
|
|
||||||
# sections à elles seules, avant le code du module.
|
|
||||||
section-order = [
|
|
||||||
"future",
|
|
||||||
"standard-library",
|
|
||||||
"third-party",
|
|
||||||
"odoo",
|
|
||||||
"odoo-addons",
|
|
||||||
"first-party",
|
|
||||||
"local-folder",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.isort.sections]
|
|
||||||
"odoo" = ["odoo"]
|
|
||||||
"odoo-addons" = ["odoo.addons"]
|
|
||||||
|
|
||||||
[lint.mccabe]
|
|
||||||
max-complexity = 16
|
|
||||||
|
|
@ -24,14 +24,13 @@
|
||||||
"odoo_version": "15.0",
|
"odoo_version": "15.0",
|
||||||
"python_version": "3.8.20",
|
"python_version": "3.8.20",
|
||||||
"poetry_version": "1.8.3",
|
"poetry_version": "1.8.3",
|
||||||
"is_deprecated": true,
|
|
||||||
"note": "Deprecated"
|
"note": "Deprecated"
|
||||||
},
|
},
|
||||||
"odoo16.0_python3.10.18": {
|
"odoo16.0_python3.10.18": {
|
||||||
"odoo_version": "16.0",
|
"odoo_version": "16.0",
|
||||||
"python_version": "3.10.18",
|
"python_version": "3.10.18",
|
||||||
"poetry_version": "1.8.3",
|
"poetry_version": "1.8.3",
|
||||||
"note": "Previous official support"
|
"note": "Official support ERPLibre 1.6.0"
|
||||||
},
|
},
|
||||||
"odoo17.0_python3.10.18": {
|
"odoo17.0_python3.10.18": {
|
||||||
"odoo_version": "17.0",
|
"odoo_version": "17.0",
|
||||||
|
|
@ -44,6 +43,6 @@
|
||||||
"python_version": "3.12.10",
|
"python_version": "3.12.10",
|
||||||
"poetry_version": "2.1.3",
|
"poetry_version": "2.1.3",
|
||||||
"default": true,
|
"default": true,
|
||||||
"note": "Official support ERPLibre 1.8.0"
|
"note": "Last version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: commit
|
name: commit
|
||||||
description: "ERPLibre commit: OCA tag, bilingual body, Assisted-by trailer per AI_POLICY.md."
|
description: "OCA/Odoo conventional commit in English with dynamic Claude Code attribution."
|
||||||
disable-model-invocation: true
|
disable-model-invocation: true
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash(git add:*)
|
- Bash(git add:*)
|
||||||
|
|
@ -8,6 +8,8 @@ allowed-tools:
|
||||||
- Bash(git commit:*)
|
- Bash(git commit:*)
|
||||||
- Bash(git diff:*)
|
- Bash(git diff:*)
|
||||||
- Bash(git log:*)
|
- Bash(git log:*)
|
||||||
|
- Bash(claude --version)
|
||||||
|
- Bash(cat:*)
|
||||||
- Bash(python3:*)
|
- Bash(python3:*)
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -17,271 +19,62 @@ allowed-tools:
|
||||||
- Full diff: !`git diff HEAD`
|
- Full diff: !`git diff HEAD`
|
||||||
- Current branch: !`git branch --show-current`
|
- Current branch: !`git branch --show-current`
|
||||||
- Last 5 commits (for style reference): !`git log --oneline -5`
|
- Last 5 commits (for style reference): !`git log --oneline -5`
|
||||||
|
- Claude Code version: !`claude --version 2>/dev/null | head -1`
|
||||||
|
|
||||||
## Task
|
## Task
|
||||||
|
|
||||||
Write a commit that satisfies `AI_POLICY.md` — the OCA generative AI policy
|
Before committing, retrieve the active model with:
|
||||||
ERPLibre adopts — and the conventions below.
|
|
||||||
|
|
||||||
### Resolve the model — `{MODEL}`
|
|
||||||
|
|
||||||
Run this first. It reads the model from the CURRENT session transcript, which
|
|
||||||
is the only source that stays right when the model is switched mid-session
|
|
||||||
with `/model` or a CLI flag:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import glob, json, os, sys
|
import json, os
|
||||||
sid = os.environ.get('CLAUDE_CODE_SESSION_ID', '')
|
path = os.path.expanduser('~/.claude/settings.json')
|
||||||
hits = glob.glob(os.path.expanduser('~/.claude/projects/*/%s.jsonl' % sid)) if sid else []
|
try:
|
||||||
mid = ''
|
d = json.load(open(path))
|
||||||
for path in hits[:1]:
|
print(d.get('model', 'claude-sonnet-4-6'))
|
||||||
with open(path) as fh:
|
except:
|
||||||
for line in fh:
|
print('claude-sonnet-4-6')
|
||||||
try:
|
|
||||||
m = json.loads(line).get('message', {}).get('model', '')
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if m and not m.startswith('<'):
|
|
||||||
mid = m
|
|
||||||
if not mid:
|
|
||||||
sys.exit('UNKNOWN')
|
|
||||||
mid = mid.removeprefix('claude-')
|
|
||||||
parts = [p for p in mid.split('-') if not (len(p) == 8 and p.isdigit())]
|
|
||||||
print('Claude %s %s' % (parts[0].capitalize(), '.'.join(parts[1:])))
|
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
It prints the trailer value: `Claude Opus 5`, `Claude Sonnet 4.6`,
|
Then create an OCA/Odoo-compliant commit.
|
||||||
`Claude Haiku 4.5`. There is deliberately no fallback name — on `UNKNOWN`,
|
|
||||||
use the model you know you are running as, and never a value read from a
|
|
||||||
settings file: `~/.claude/settings.json` usually has no `model` key at all,
|
|
||||||
so it would quietly declare the default instead of the truth.
|
|
||||||
|
|
||||||
### Tags
|
### OCA Tags
|
||||||
|
|
||||||
| Tag | Usage |
|
| Tag | Usage |
|
||||||
|-----|-------|
|
|-----|-------|
|
||||||
| `[UPD]` | Update existing code, data or configuration |
|
| `[IMP]` | Improvement / new feature |
|
||||||
| `[FIX]` | Bug fix |
|
| `[FIX]` | Bug fix |
|
||||||
| `[ADD]` | New module, file or capability |
|
| `[REF]` | Refactoring |
|
||||||
| `[IMP]` | Improvement to something that already works |
|
| `[ADD]` | New module |
|
||||||
| `[REF]` | Refactoring, no observable behaviour change |
|
| `[REM]` | Remove code/module |
|
||||||
| `[REM]` | Remove code or module |
|
| `[MOV]` | Move/rename |
|
||||||
| `[MOV]` | Move or rename |
|
|
||||||
| `[I18N]` | Translations |
|
| `[I18N]` | Translations |
|
||||||
|
|
||||||
The first five cover every one of the last 400 commits. Reach for `[REM]`,
|
|
||||||
`[MOV]` or `[I18N]` only when one of them genuinely fits better.
|
|
||||||
|
|
||||||
### Format
|
### Format
|
||||||
|
|
||||||
```
|
```
|
||||||
[TAG] scope: short description in imperative mood, in English
|
[TAG] module_name: short description in imperative mood
|
||||||
|
|
||||||
Explain WHY the change was made — the diff already shows what. Name the
|
Explain WHY the change was made (not what — the diff already shows that).
|
||||||
failure mode, and what was measured rather than assumed, in words a
|
Keep lines under 80 characters.
|
||||||
stranger to this site can read: the software, the version, the class of
|
|
||||||
situation — never a customer, a real database, a machine or an address.
|
|
||||||
Wrap at 80 characters.
|
|
||||||
|
|
||||||
--- FR ---
|
Generated by Claude Code {VERSION} {MODEL}
|
||||||
|
|
||||||
[TAG] portée : le sujet, traduit en français
|
Co-Authored-By: Your Name <your@email.com>
|
||||||
|
|
||||||
The same body, translated.
|
|
||||||
|
|
||||||
Assisted-by: {MODEL}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The subject line is in ENGLISH, and the English body follows it: together
|
|
||||||
they are the English section. `--- FR ---` then opens the French section,
|
|
||||||
which starts with the subject translated, under the same tag, and goes on
|
|
||||||
with the translated body.
|
|
||||||
|
|
||||||
### The subject line
|
|
||||||
|
|
||||||
The subject is read a hundred times for every time the body is read: in
|
|
||||||
`git log --oneline`, in a blame, in a release note, in a bisect. It has one
|
|
||||||
job — say what the code is about.
|
|
||||||
|
|
||||||
**The test.** Read the subject alone, with no diff and no body. Can you say
|
|
||||||
which part of the system it concerns, and what is now different about it? If
|
|
||||||
not, it is not finished.
|
|
||||||
|
|
||||||
**Name the thing, then what changed about it.** The symptom, the quoted error
|
|
||||||
and the metaphor are EVIDENCE, and evidence belongs in the body. A subject
|
|
||||||
built on them reads well and tells the next reader nothing:
|
|
||||||
|
|
||||||
| Instead of | Write |
|
|
||||||
|-----------|-------|
|
|
||||||
| `[FIX] cleanup: the children leave with their bounce` | `[FIX] cleanup: ssh entries that bounce through a deleted VM` |
|
|
||||||
| `[FIX] proxmox: "storage is missing" was the symptom, not the cause` | `[FIX] proxmox: report pmxcfs down, not "no storage"` |
|
|
||||||
| `[FIX] migration: a faulty module no longer takes the whole batch` | `[FIX] migration: isolate a module's failure during uninstall` |
|
|
||||||
|
|
||||||
The scope is not the subject. `proxmox` says WHERE; the words after the colon
|
|
||||||
must say WHAT. A subject that works with its scope removed is usually the
|
|
||||||
right one.
|
|
||||||
|
|
||||||
**Summarise the whole commit, not its largest piece.** When the work has two
|
|
||||||
faces — a guard moved and the check that proves it, a screen and the service
|
|
||||||
under it — the subject covers both or the commit should have been two. If the
|
|
||||||
only honest subject needs an `and` joining two unrelated things, split it.
|
|
||||||
|
|
||||||
**It must be complete in 72 characters.** A subject cut mid-phrase by
|
|
||||||
`--oneline` has failed at the one place it is read most. Write it to fit
|
|
||||||
rather than trimming it afterwards: drop the adjectives, keep the nouns.
|
|
||||||
|
|
||||||
When the work genuinely will not fit in a sentence, do not write an amputated
|
|
||||||
one — write **keywords that summarise**. A comma-separated list of the nouns
|
|
||||||
that matter says more in the space than half a sentence does:
|
|
||||||
|
|
||||||
```
|
|
||||||
[FIX] proxmox: pmxcfs down, pvesm silent, diagnosis at the source
|
|
||||||
[ADD] migration: site copies, doubled indexes, lost settings
|
|
||||||
```
|
|
||||||
|
|
||||||
That form is a fallback, not a default. Prefer the sentence when it fits.
|
|
||||||
|
|
||||||
**The guard rail.** `script/git/hooks/commit-msg` refuses a subject with no
|
|
||||||
tag, one over 72 characters, and one opening on a quotation. It reads the body
|
|
||||||
too: over ten lines for one language, an IP address, an e-mail, and a
|
|
||||||
`/home/<account>/` path. And the order of the languages: a `--- EN ---`
|
|
||||||
marker, or a French section that does not open on the translated subject
|
|
||||||
under the same tag. Whether the subject is really in English stays yours to
|
|
||||||
check. Install the hook with `git config core.hooksPath script/git/hooks`; `git commit
|
|
||||||
--no-verify` passes a legitimate exception. It checks only what is mechanical
|
|
||||||
— whether the subject says what the code is about, and whether the body tells
|
|
||||||
the story instead of the mechanism, stay judgements, and the tests above are
|
|
||||||
how you make them.
|
|
||||||
|
|
||||||
### Keep it short
|
|
||||||
|
|
||||||
The body answers one question: why was this necessary. Stop once it is
|
|
||||||
answered — the reader owes you nothing beyond that.
|
|
||||||
|
|
||||||
**Eight lines per language. Ten is the ceiling.** Past that, the reasoning
|
|
||||||
belongs in `tasks/`, which is not versioned, and the commit points at it —
|
|
||||||
never in a code comment: a comment says how the code WORKS, not what happened
|
|
||||||
the week it was written. The budget is per language: bilingual doubles
|
|
||||||
everything, so it buys terseness, it does not excuse length.
|
|
||||||
|
|
||||||
Cut, in this order:
|
|
||||||
|
|
||||||
- Anything the diff already says. `adds function X` is visible; `X because
|
|
||||||
the DHCP lease can be stale` is not.
|
|
||||||
- Headings and bullet lists. If the change really needs sections, it needs
|
|
||||||
several commits.
|
|
||||||
- Every clause that would not change what a reader does: no `this commit`,
|
|
||||||
no `I decided to`, no summary of the summary, no restating the subject.
|
|
||||||
|
|
||||||
Keep, always: the failure mode the change removes, the figure that bounds it,
|
|
||||||
and one line naming what you verified and how. A single `Checked: 4 jobs,
|
|
||||||
1.63 s at parallelism 1 vs 0.58 s at 4` is worth three paragraphs of prose.
|
|
||||||
|
|
||||||
Two tests decide what survives, and they apply to every sentence of the body.
|
|
||||||
|
|
||||||
**Tense and subject.** Each sentence says what the code now does or refuses,
|
|
||||||
in the present. A sentence whose subject is an incident, a session, a machine,
|
|
||||||
a date or a person is cut — including your own reasoning: no `my conclusion
|
|
||||||
was wrong`, no `three faults found by running it`. That belongs in `tasks/`.
|
|
||||||
|
|
||||||
**Nothing identifying.** No customer or third-party organisation, no real
|
|
||||||
database name, no VM or host name, no IP address, no e-mail, no path carrying
|
|
||||||
a user name, no label or figure taken from a customer's data. Generalise to
|
|
||||||
the CLASS of situation instead of censoring: `on a production database`, `on a
|
|
||||||
development VM`, `on a host that demands interactive sudo` — the class is what
|
|
||||||
serves the reader; the name never was. A figure that is a durable limit or
|
|
||||||
threshold stays (65 536 inotify watches); a reading taken during one incident
|
|
||||||
goes.
|
|
||||||
|
|
||||||
### Bilingual body
|
|
||||||
|
|
||||||
Every AI-assisted commit carries its subject and its body twice, English
|
|
||||||
first: the subject and the body under it are in English, then `--- FR ---`,
|
|
||||||
then the subject translated into French under the same tag, then the body
|
|
||||||
translated. The order never varies — `--- EN ---` is refused — so a reader of
|
|
||||||
`git log --oneline` always gets English, and a French reader finds the French
|
|
||||||
title where the French section starts.
|
|
||||||
|
|
||||||
The translated subject obeys the same rules as the subject: 72 characters,
|
|
||||||
no opening quotation. It does not count against the body's line budget.
|
|
||||||
|
|
||||||
Translate, do not re-summarise: a reader of either language must get the same
|
|
||||||
reasoning, the same measured figures and the same caveats.
|
|
||||||
|
|
||||||
### The Assisted-by trailer
|
|
||||||
|
|
||||||
`AI_POLICY.md` makes this binary — there was AI involvement or there was not,
|
|
||||||
with no threshold to judge. Anything from a single suggestion to fully
|
|
||||||
autonomous coding means the trailer, and it says nothing about the quality of
|
|
||||||
the work.
|
|
||||||
|
|
||||||
- One `Assisted-by:` line per model. A session that switched models declares
|
|
||||||
each of them, one line each.
|
|
||||||
- NEVER name an AI in `Co-authored-by:`: authorship of a work by a machine is
|
|
||||||
legally undefined. That field is for other HUMANS who worked on the change.
|
|
||||||
You are already the author, so never co-author yourself.
|
|
||||||
- No blank line between trailers.
|
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
|
|
||||||
- Subject: imperative mood, **72 characters maximum**, aim for 50.
|
- **English only**, imperative mood, subject line under 50 chars
|
||||||
- `scope` is the Odoo technical module (`sale_order`, `account`, `stock`) or
|
- Use the Odoo technical module name (e.g. `sale_order`, `account`, `stock`)
|
||||||
the area of the repository (`script todo`, `qemu ssh`, `migration`).
|
- If multiple modules are impacted, suggest splitting into separate commits
|
||||||
- The commit stands on its own: state what was verified, and how. If a claim
|
|
||||||
was not checked, say so rather than implying it was. Standing on its own
|
|
||||||
means it needs no OTHER COMMIT to be understood — not that it carries the
|
|
||||||
whole investigation. The line budget above still binds.
|
|
||||||
- If you cannot explain and defend every line, do not commit it.
|
|
||||||
|
|
||||||
### Size and pace
|
|
||||||
|
|
||||||
A patch under 30 lines in a single file is the reference point. Past ~500
|
|
||||||
lines the policy asks for prior agreement with a maintainer — say so instead
|
|
||||||
of committing quietly. When several unrelated modules are touched, propose
|
|
||||||
splitting into separate commits before writing anything.
|
|
||||||
|
|
||||||
### Execute
|
### Execute
|
||||||
|
|
||||||
The timezone comes from the system, as it should: nothing is forced here, so
|
|
||||||
each contributor's commits carry their own zone. If yours land at `+0000`,
|
|
||||||
the machine itself is on UTC — common on a server or a VM — and the fix
|
|
||||||
belongs there, `sudo timedatectl set-timezone <Area/City>`, because it
|
|
||||||
affects every commit and not just this one.
|
|
||||||
|
|
||||||
The identity is passed explicitly with `-c`, which sets the author AND the
|
|
||||||
committer. `--author` alone sets only the author, and a checkout with no
|
|
||||||
configured `user.email` then fails on the committer.
|
|
||||||
|
|
||||||
Use a heredoc rather than `-m`: a body with quotes, backticks or accented
|
|
||||||
characters survives it unharmed.
|
|
||||||
|
|
||||||
**Name the files. Never `git add -A`.** It stages everything untracked, and
|
|
||||||
this repository keeps two directories untracked ON PURPOSE: `private/`, the
|
|
||||||
only place allowed to hold customer data, and `tasks/`, where the convention
|
|
||||||
sends the investigation precisely because it is not versioned. A sweep commits
|
|
||||||
both. It also swallows whatever else is in flight in the checkout — another
|
|
||||||
tool's output, a half-finished edit — under a subject that does not cover it.
|
|
||||||
|
|
||||||
`git status --porcelain` lists what changed; stage the paths that belong to
|
|
||||||
the subject you just wrote, and no others. When one file carries two subjects,
|
|
||||||
`git add -p` stages the hunks that belong to this commit.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git status --porcelain
|
git add -A && git commit --author="Your Name <your@email.com>" -m "[TAG] module: description
|
||||||
git add script/module/thing.py test/test_thing.py
|
|
||||||
git -c user.name="Your Name" -c user.email="your@email.com" commit -F - <<'MSG'
|
|
||||||
[TAG] scope: description in English
|
|
||||||
|
|
||||||
Explain WHY here.
|
Explain WHY here.
|
||||||
|
|
||||||
--- FR ---
|
Generated by Claude Code {VERSION} {MODEL}
|
||||||
|
|
||||||
[TAG] portée : la description en français
|
Co-Authored-By: Your Name <your@email.com>"
|
||||||
|
|
||||||
The same body, translated.
|
|
||||||
|
|
||||||
Assisted-by: {MODEL}
|
|
||||||
MSG
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
---
|
|
||||||
name: git_prepare_merge
|
|
||||||
description: "ERPLibre merge preparation: changelog entry, then the merge message for the current branch."
|
|
||||||
disable-model-invocation: true
|
|
||||||
allowed-tools:
|
|
||||||
- Bash(git status:*)
|
|
||||||
- Bash(git branch:*)
|
|
||||||
- Bash(git log:*)
|
|
||||||
- Bash(git diff:*)
|
|
||||||
- Bash(git merge-base:*)
|
|
||||||
- Bash(sed:*)
|
|
||||||
- Bash(make doc_markdown:*)
|
|
||||||
- Bash(python3:*)
|
|
||||||
- Read
|
|
||||||
- Edit
|
|
||||||
- Write
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
- Current branch: !`git branch --show-current`
|
|
||||||
- Branch commits: !`git log --oneline $(git merge-base HEAD master)..HEAD`
|
|
||||||
- Files touched: !`git diff --stat $(git merge-base HEAD master)..HEAD`
|
|
||||||
- Working tree: !`git status --porcelain`
|
|
||||||
- Changelog head: !`sed -n '24,45p' CHANGELOG.base.md`
|
|
||||||
|
|
||||||
## Task
|
|
||||||
|
|
||||||
Prepare the merge of the CURRENT branch into its integration branch. Two
|
|
||||||
deliverables, in this order: the changelog entry, then the merge message.
|
|
||||||
Nothing is merged here — `/git_prepare_merge` prepares, the human merges.
|
|
||||||
|
|
||||||
### 0. Read the branch
|
|
||||||
|
|
||||||
`master` is production, `develop` is where the work lands. Take the target
|
|
||||||
from where the branch forked: `git merge-base HEAD develop` and
|
|
||||||
`git merge-base HEAD master`, the closer of the two names the target.
|
|
||||||
|
|
||||||
Read the WHOLE branch before writing a word — `git log -p <base>..HEAD` for
|
|
||||||
the commits, `git diff <base>..HEAD` for the net result. A merge message
|
|
||||||
summarises what the branch delivers, which is rarely the concatenation of its
|
|
||||||
subjects: commits that undo each other cancel, and a fix to a feature added on
|
|
||||||
the same branch is part of the feature, not a separate line.
|
|
||||||
|
|
||||||
Stop and say so, rather than inventing, when the branch is empty, when it is
|
|
||||||
already merged, or when the working tree carries changes not yet committed —
|
|
||||||
uncommitted work is not part of the merge and must not be described as if it
|
|
||||||
were.
|
|
||||||
|
|
||||||
### 1. The changelog entry
|
|
||||||
|
|
||||||
`CHANGELOG.base.md` at the repository root is the SOURCE. `CHANGELOG.md` and
|
|
||||||
`CHANGELOG.fr.md` are generated by mmg and every direct edit to them is lost
|
|
||||||
at the next `make doc_markdown` — never open them to write.
|
|
||||||
|
|
||||||
The entry goes under `## [Unreleased]`, in the section that fits: Added /
|
|
||||||
Ajouté, Changed / Modifié, Fixed / Corrigé, Removed / Retiré, Security /
|
|
||||||
Sécurité. Create the pair of headings if the section does not exist yet, in
|
|
||||||
the file's own order.
|
|
||||||
|
|
||||||
The file alternates language blocks with markers. Within one section the
|
|
||||||
English bullets sit under `<!-- [en] -->` and the French translation under
|
|
||||||
`<!-- [fr] -->`, in the SAME order: the two lists are read side by side, and a
|
|
||||||
bullet added to one language only leaves the other half wrong. Nothing goes
|
|
||||||
under `<!-- [common] -->` but the version headings.
|
|
||||||
|
|
||||||
What a bullet says: what the software now DOES or REFUSES, in the present, for
|
|
||||||
someone who was not on this branch. It is longer than a commit subject and
|
|
||||||
shorter than the commit body — the reader is choosing whether to upgrade, not
|
|
||||||
reviewing the diff. Keep the failure mode removed, the figure that bounds it,
|
|
||||||
the flag or the file a user has to know. Drop the internals nobody outside
|
|
||||||
calls.
|
|
||||||
|
|
||||||
The two rules of `.claude/rules/04-code-conventions.md` hold here as
|
|
||||||
everywhere: nothing identifying — no customer, no real database, no host, no
|
|
||||||
address, no account path — and the code as the subject, never the session
|
|
||||||
that produced it.
|
|
||||||
|
|
||||||
Regenerate afterwards, and stage the three files together, the generated pair
|
|
||||||
being what most readers actually open:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make doc_markdown
|
|
||||||
git status --porcelain CHANGELOG.base.md CHANGELOG.md CHANGELOG.fr.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. The merge message
|
|
||||||
|
|
||||||
Resolve `{MODEL}` exactly as `/commit` does — the trailer is required here
|
|
||||||
too, a merge message being as AI-assisted as any other. Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -c "
|
|
||||||
import glob, json, os, sys
|
|
||||||
sid = os.environ.get('CLAUDE_CODE_SESSION_ID', '')
|
|
||||||
hits = glob.glob(os.path.expanduser('~/.claude/projects/*/%s.jsonl' % sid)) if sid else []
|
|
||||||
mid = ''
|
|
||||||
for path in hits[:1]:
|
|
||||||
with open(path) as fh:
|
|
||||||
for line in fh:
|
|
||||||
try:
|
|
||||||
m = json.loads(line).get('message', {}).get('model', '')
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if m and not m.startswith('<'):
|
|
||||||
mid = m
|
|
||||||
if not mid:
|
|
||||||
sys.exit('UNKNOWN')
|
|
||||||
mid = mid.removeprefix('claude-')
|
|
||||||
parts = [p for p in mid.split('-') if not (len(p) == 8 and p.isdigit())]
|
|
||||||
print('Claude %s %s' % (parts[0].capitalize(), '.'.join(parts[1:])))
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
The shape, as this repository writes it:
|
|
||||||
|
|
||||||
```
|
|
||||||
Merge branch '<branch>'
|
|
||||||
|
|
||||||
[TAG] scope: what the branch delivers, in English, 72 characters maximum
|
|
||||||
|
|
||||||
<N> commits. Why the branch existed: the failure mode it removes, the
|
|
||||||
figure that bounds it, what was verified and how. Wrap at 80 characters.
|
|
||||||
|
|
||||||
--- FR ---
|
|
||||||
|
|
||||||
[TAG] portée : ce que livre la branche, traduit en français
|
|
||||||
|
|
||||||
The same body, translated.
|
|
||||||
|
|
||||||
Assisted-by: {MODEL}
|
|
||||||
```
|
|
||||||
|
|
||||||
The first line stays `Merge branch '<branch>'` — git writes it, tools read it,
|
|
||||||
and a merge whose first line says something else no longer looks like a merge
|
|
||||||
in `git log --oneline`. The tagged line beneath it is what a reader gets from
|
|
||||||
`--oneline` on the second row and from a release note, so it carries the same
|
|
||||||
duty as a commit subject: name the part of the system, then what is now
|
|
||||||
different about it. The evidence — the symptom, the quoted error, the
|
|
||||||
metaphor — belongs in the body.
|
|
||||||
|
|
||||||
The body: the same budget as a commit, per language, and here a merge covers
|
|
||||||
several commits, so it is a SUMMARY and not a list. Open by stating how many
|
|
||||||
commits the branch carries, then say what they add up to. No bullet list, no
|
|
||||||
per-commit rundown: `git log <base>..HEAD` already gives that, and a body
|
|
||||||
repeating it teaches nothing.
|
|
||||||
|
|
||||||
The bilingual rule holds as for a commit — the tagged line and the body in
|
|
||||||
English, then `--- FR ---`, then the tagged line translated under the same
|
|
||||||
tag, then the translated body — as does the ban on naming an AI in
|
|
||||||
`Co-authored-by:`.
|
|
||||||
|
|
||||||
**The hook does not check this one.** `script/git/hooks/commit-msg` skips any
|
|
||||||
message beginning with `Merge `, along with `Revert `, `fixup!` and `squash!`.
|
|
||||||
Length, addresses, account paths and the order of the languages pass
|
|
||||||
unchallenged here, so the discipline is entirely yours.
|
|
||||||
|
|
||||||
### 3. Hand it over
|
|
||||||
|
|
||||||
Write the message to `tasks/merge_message.txt` — `tasks/` is not versioned,
|
|
||||||
which is why the repository sends working material there — and print the two
|
|
||||||
commands the human runs, with `--no-ff` so the branch keeps a merge commit and
|
|
||||||
its history stays readable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git switch <target>
|
|
||||||
git merge --no-ff <branch> -F tasks/merge_message.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not run them. Do not switch branch, do not merge, do not push: the merge is
|
|
||||||
the human's decision and the last chance to read the message before it is
|
|
||||||
permanent. Report, in a sentence each, the changelog section written to and
|
|
||||||
the number of commits summarised.
|
|
||||||
|
|
@ -17,14 +17,6 @@ allowed-tools:
|
||||||
- Current todo.json sections: !`python3 -c "import json; d=json.load(open('script/todo/todo.json')); print('\n'.join(d.keys()))"`
|
- Current todo.json sections: !`python3 -c "import json; d=json.load(open('script/todo/todo.json')); print('\n'.join(d.keys()))"`
|
||||||
- Current i18n keys count: !`grep -c fr.: script/todo/todo_i18n.py`
|
- Current i18n keys count: !`grep -c fr.: script/todo/todo_i18n.py`
|
||||||
|
|
||||||
## Planning first
|
|
||||||
|
|
||||||
`/todo_plan_max` plans one entry at maximum effort: it asks what forks the
|
|
||||||
design, uses the superpowers plugin when it is installed, and writes the
|
|
||||||
specification to `tasks/todo.md`. Both commands are deployed together by
|
|
||||||
`TODO › Execute › GPT code › Claude configs`. When such a specification
|
|
||||||
exists, implement it rather than re-deciding the design here.
|
|
||||||
|
|
||||||
## Architecture Reference
|
## Architecture Reference
|
||||||
|
|
||||||
### Files to modify
|
### Files to modify
|
||||||
|
|
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
---
|
|
||||||
name: todo_generate_code
|
|
||||||
description: "Write code in ERPLibre by the rules the repository actually enforces: Odoo/OCA module conventions first, then the real format and verify toolchain."
|
|
||||||
disable-model-invocation: true
|
|
||||||
effort: high
|
|
||||||
allowed-tools:
|
|
||||||
- Read
|
|
||||||
- Edit
|
|
||||||
- Write
|
|
||||||
- Grep
|
|
||||||
- Glob
|
|
||||||
- Bash(make format:*)
|
|
||||||
- Bash(make test_unit:*)
|
|
||||||
- Bash(make test_unit_file:*)
|
|
||||||
- Bash(./script/maintenance/:*)
|
|
||||||
- Bash(python3:*)
|
|
||||||
- Bash(git status:*)
|
|
||||||
- Bash(git diff:*)
|
|
||||||
- Bash(ls:*)
|
|
||||||
- Bash(cat:*)
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
- Odoo series in this checkout: !`cat .odoo-version`
|
|
||||||
- Available venvs: !`ls -d .venv.* 2>/dev/null`
|
|
||||||
- Branch and pending work: !`git status --porcelain`
|
|
||||||
- Addons trees: !`ls -d odoo*/addons/*/ 2>/dev/null | head -12`
|
|
||||||
|
|
||||||
## Task
|
|
||||||
|
|
||||||
Write code in this repository by the rules it ENFORCES, which are not always
|
|
||||||
the rules it documents. Establish them first, then write. The sections below
|
|
||||||
were read off the configuration files and the in-house modules, not off the
|
|
||||||
prose; where a document disagrees with a tool, the tool wins and the
|
|
||||||
disagreement is named.
|
|
||||||
|
|
||||||
### The effort tier
|
|
||||||
|
|
||||||
The `effort: high` above applies to this invocation — "comprehensive
|
|
||||||
implementation with extensive testing", the tier for code that has to survive
|
|
||||||
review.
|
|
||||||
|
|
||||||
A session can be pinned above it. `/effort ultracode` sets xhigh AND turns on
|
|
||||||
dynamic workflow orchestration, and a system-reminder then asks for a workflow
|
|
||||||
on every substantive task. That standing opt-in does not apply here: writing
|
|
||||||
one module correctly is one agent's job, and fanning it out multiplies both the
|
|
||||||
token cost and the ways the pieces disagree.
|
|
||||||
|
|
||||||
So when a reminder says ultracode is on, say in one line that this command
|
|
||||||
works at high, and ask the user to type `/effort high` — that one command sets
|
|
||||||
the tier and clears the ultracode flag in the same move. A command's
|
|
||||||
frontmatter cannot release a session pin; only the user's own `/effort` can,
|
|
||||||
from an interactive terminal. Work solo either way.
|
|
||||||
|
|
||||||
### 1. Before writing a line
|
|
||||||
|
|
||||||
**Find the tree.** New modules go under `odoo<VERSION>/addons/<Org>_<repo>/<module>/`
|
|
||||||
— the version-prefixed tree is the real addons root, and the generator
|
|
||||||
rewrites any `addons/` it is handed into `odoo<VERSION>/addons/`
|
|
||||||
(`script/code_generator/new_project.py:144-146`). The version is in
|
|
||||||
`.odoo-version`; the venv carries BOTH versions in its name, so find it with
|
|
||||||
`ls -d .venv.odoo*` rather than composing it from memory.
|
|
||||||
|
|
||||||
**Read the neighbours.** Two or three in-house modules under
|
|
||||||
`odoo<VERSION>/addons/ERPLibre_erplibre_addons/` show the conventions in force
|
|
||||||
better than any list. Copy their shape.
|
|
||||||
|
|
||||||
**Bootstrap rather than hand-roll.** `script/code_generator/new_project.py -d
|
|
||||||
<git root dir> -m <module name>` creates a module; `create_from_existing_module.py`
|
|
||||||
clones one. A module cloned from an existing one INHERITS its comments and
|
|
||||||
docstrings — reread them before committing, a client or database name travels
|
|
||||||
that way on its own.
|
|
||||||
|
|
||||||
**Two rules bind every line you write**, and no tool checks either:
|
|
||||||
- Nothing identifying outside `private/` — no customer or third-party
|
|
||||||
organisation, no real database, host or VM name, no IP, e-mail or path
|
|
||||||
carrying an account name. Generalise to the class of situation instead.
|
|
||||||
- A comment says how the CODE works, in the present. A sentence whose subject
|
|
||||||
is an incident, a machine, a date or a person belongs in `tasks/`, which is
|
|
||||||
not versioned.
|
|
||||||
|
|
||||||
**Never edit a generated file.** `FICHIER.md` and `FICHIER.fr.md` come from
|
|
||||||
`FICHIER.base.md` through mmg; an edit is lost at the next `make doc_markdown`.
|
|
||||||
|
|
||||||
### 2. Odoo module conventions
|
|
||||||
|
|
||||||
`__manifest__.py` carries at minimum `name`, `version`, `author`, `license`,
|
|
||||||
`category`, `summary`, `depends`, `data`, `installable`. `version` is
|
|
||||||
`<series>.1.0.0` — `18.0.1.0.0` on an 18.0 checkout — and `license` is
|
|
||||||
`AGPL-3`, which every in-house module uses.
|
|
||||||
|
|
||||||
The `data` list is ordered `security/`, then `data/`, then `wizards/`, then
|
|
||||||
`views/`, with `views/menu.xml` last.
|
|
||||||
|
|
||||||
Non-Odoo Python requirements go in `external_dependencies: {"python": [...]}`,
|
|
||||||
and the import is guarded in the model with `try/except ImportError` logging at
|
|
||||||
debug level — a missing optional dependency must not break the registry.
|
|
||||||
|
|
||||||
Layout: `models/`, `views/`, `security/`, `wizards/`, `data/`, `controllers/`,
|
|
||||||
`i18n/`, `report/`, `tests/`, `static/description/`. Ship
|
|
||||||
`static/description/icon.png`.
|
|
||||||
|
|
||||||
Naming, one for one:
|
|
||||||
- `models/<model_name_with_dots_as_underscores>.py`, one model per file —
|
|
||||||
model `devops.workspace` lives in `models/devops_workspace.py`.
|
|
||||||
- `views/<same_name>.xml` for a Model; a TransientModel's Python AND its XML
|
|
||||||
both live in `wizards/`.
|
|
||||||
- `ir.ui.view` ids: `<model_underscored>_view_<type>` (`_view_form`,
|
|
||||||
`_view_tree`, `_view_search`, `_view_kanban`).
|
|
||||||
- `ir.actions.act_window` ids: `<model_underscored>_<action>_action_window`;
|
|
||||||
server actions: `<model_underscored>_<name>_server_action`.
|
|
||||||
- `res.groups` go in `security/<module_name>.xml` — named after the module, not
|
|
||||||
`security.xml` — and that file is listed BEFORE `ir.model.access.csv`.
|
|
||||||
|
|
||||||
`security/ir.model.access.csv` carries exactly the header
|
|
||||||
`id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink`.
|
|
||||||
Split the rows by privilege level — a read-only row for the broad group, a
|
|
||||||
full-CRUD row for the administrative one — rather than one blanket row.
|
|
||||||
|
|
||||||
Every Python file opens with the AGPL licence comment; hand-written models and
|
|
||||||
hooks also carry the shebang and copyright lines. A package `__init__.py`
|
|
||||||
starts with `# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)`,
|
|
||||||
a blank line, then imports. The root `__init__.py` imports subpackages on one
|
|
||||||
line (`from . import models, wizards`) and a hook by name
|
|
||||||
(`from .hooks import post_init_hook`); hooks live in a top-level `hooks.py` and
|
|
||||||
are declared under the matching manifest key.
|
|
||||||
|
|
||||||
Models declare `_name`, `_description` and, when mixins are used, `_inherit`;
|
|
||||||
the class name is the CamelCase of the model. Odoo symbols come in one grouped
|
|
||||||
import — `from odoo import _, api, exceptions, fields, models`.
|
|
||||||
|
|
||||||
Translations go in `i18n/` as `<module>.pot` plus one `.po` per locale.
|
|
||||||
|
|
||||||
### 3. Format — the toolchain that exists
|
|
||||||
|
|
||||||
`make format` before committing. It formats only what git reports as
|
|
||||||
modified, added, renamed or untracked, dispatching each file by extension —
|
|
||||||
so it is cheap and safe to run repeatedly. `make format_all` sweeps whole
|
|
||||||
areas instead.
|
|
||||||
|
|
||||||
What it runs underneath, and what to match when writing by hand:
|
|
||||||
|
|
||||||
| Kind | Tool and settings |
|
|
||||||
|------|-------------------|
|
|
||||||
| Python | `isort --profile black -l 79`, then `black -l 79 --preview -t py37` |
|
|
||||||
| XML | prettier + `@prettier/plugin-xml`, tab-width 4, print-width 120 |
|
|
||||||
| XML under `data/` | same, print-width 999999999 — long data strings stay on one line |
|
|
||||||
| js, css, scss, html | prettier, tab-width 4, print-width 120, no bracket spacing |
|
|
||||||
| Shell | `shfmt -i 2 -ci -w` |
|
|
||||||
|
|
||||||
black and isort run from `.venv.erplibre`, prettier from the repo-pinned
|
|
||||||
`./node_modules/.bin` — a globally installed prettier is a different version
|
|
||||||
and reformats differently. Nothing under `not_supported_files/` is formatted.
|
|
||||||
|
|
||||||
**What NOT to run**, each verified absent from this checkout rather than
|
|
||||||
assumed:
|
|
||||||
- `./script/maintenance/autopep8.sh` — `oca-autopep8` is not installed; the
|
|
||||||
script exits 1. `doc/DEVELOPMENT.base.md` still recommends it; the document
|
|
||||||
is stale.
|
|
||||||
- Any `oca-*` console script (`oca-gen-addon-readme`, `oca-towncrier`, …) —
|
|
||||||
`script/OCA_maintainer-tools` is checked out but never pip-installed, and its
|
|
||||||
install block in `install_locally_dev.sh` is commented out. Run one as a
|
|
||||||
module from that directory if you truly need it.
|
|
||||||
- `pre-commit run` at the repository root — there is no root
|
|
||||||
`.pre-commit-config.yaml`. ERPLibre's own hooks are hand-written Python,
|
|
||||||
installed once per clone with
|
|
||||||
`git config core.hooksPath script/git/hooks`.
|
|
||||||
|
|
||||||
**A vendored OCA addon** under `odoo<VERSION>/addons/OCA_*` carries its own
|
|
||||||
`.pre-commit-config.yaml`: run THAT from its directory instead of the ERPLibre
|
|
||||||
format scripts, so the patch matches what upstream will accept.
|
|
||||||
|
|
||||||
### 4. Lint
|
|
||||||
|
|
||||||
Neither linter runs on its own — no target, no hook, no CI invokes them, so
|
|
||||||
running them is a deliberate act:
|
|
||||||
|
|
||||||
- flake8 lives in the Odoo venv, not `.venv.erplibre`. The root `.flake8`
|
|
||||||
applies: max-line-length 80, max-complexity 16, `select = C,E,F,W,B,B9`,
|
|
||||||
ignoring E203, E501 and W503 for black compatibility.
|
|
||||||
- pylint-odoo also lives in the Odoo venv:
|
|
||||||
`.venv.odoo<...>/bin/pylint --load-plugins=pylint_odoo --rcfile=<file> <path>`.
|
|
||||||
The `--rcfile` is not optional — the repository root ships no `.pylintrc`,
|
|
||||||
and the ones found under vendored trees belong to those projects.
|
|
||||||
|
|
||||||
### 5. Verify
|
|
||||||
|
|
||||||
Verification is entirely local: `.github/` holds no workflow, so nothing
|
|
||||||
catches a mistake after the fact.
|
|
||||||
|
|
||||||
**Repository scripts** — `make test_unit` is the fast gate: no PostgreSQL, no
|
|
||||||
Odoo, no VM, a few seconds. While iterating on one file, `make test_unit_file
|
|
||||||
F=test/test_<name>.py`.
|
|
||||||
|
|
||||||
A new file in `test/` must declare at least one `test_*` function and end with
|
|
||||||
`if __name__ == "__main__": unittest.main()` as the LAST top-level statement —
|
|
||||||
the suite has its own test that enforces both, since the runner selects files
|
|
||||||
by the glob `test/test_*.py`. A test that creates a real machine, installs a
|
|
||||||
system or runs for hours goes in `long_test/` instead, and undoes itself with
|
|
||||||
`--detruire`.
|
|
||||||
|
|
||||||
**An Odoo module** — drop the database, then run the module's tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./odoo_bin.sh db --drop --database test_<module>
|
|
||||||
./test.sh -d test_<module> --db-filter test_<module> -i <module>
|
|
||||||
```
|
|
||||||
|
|
||||||
`./test.sh` is `./run.sh` with `ODOO_MODE_TEST=true --workers 0`, which adds
|
|
||||||
`--test-enable --no-http --stop-after-init`. For coverage, bracket that with
|
|
||||||
`./.venv.erplibre/bin/coverage erase` before and a combine/report after, and
|
|
||||||
set `ODOO_MODE_COVERAGE=true` — coverage is switched on through the
|
|
||||||
environment, never a CLI flag. `make open_test_coverage` opens the report.
|
|
||||||
|
|
||||||
To run ONE test file inside a module, install the module first, then pass
|
|
||||||
`--test-file=`.
|
|
||||||
|
|
||||||
### 6. Then, and only then, commit
|
|
||||||
|
|
||||||
`make format`, the tests above, then `/commit` — which resolves the model,
|
|
||||||
writes the `[TYPE] portée : sujet` subject under 72 characters, the bilingual
|
|
||||||
body under ten lines per language, and the `Assisted-by:` trailer. The
|
|
||||||
`commit-msg` hook REFUSES a message that breaks the mechanical part of that;
|
|
||||||
the `pre-commit` hook only reports comments worth rereading and never blocks.
|
|
||||||
|
|
||||||
Stage by naming files. `git add -A` sweeps in `private/` and `tasks/`, which
|
|
||||||
are untracked on purpose.
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
---
|
|
||||||
name: todo_plan_max
|
|
||||||
description: "Plan a todo.py command at maximum effort: questions first, superpowers when installed, then the specification /todo_add_command implements."
|
|
||||||
disable-model-invocation: true
|
|
||||||
effort: max
|
|
||||||
allowed-tools:
|
|
||||||
- Bash(claude plugin list:*)
|
|
||||||
- Bash(claude plugin marketplace list:*)
|
|
||||||
- Bash(grep:*)
|
|
||||||
- Bash(sed:*)
|
|
||||||
- Bash(ls:*)
|
|
||||||
- Bash(git log:*)
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
- Write
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
- Existing menus: !`grep -n "def prompt_execute" script/todo/todo.py`
|
|
||||||
- Menu sections in todo.json: !`python3 -c "import json;print(*json.load(open('script/todo/todo.json')),sep='\n')"`
|
|
||||||
- Installed plugins: !`claude plugin list`
|
|
||||||
- Last commits on the menu: !`git log --oneline -8 -- script/todo/`
|
|
||||||
|
|
||||||
## Task
|
|
||||||
|
|
||||||
Plan ONE command for the `script/todo/todo.py` menu, at the effort tier this
|
|
||||||
file pins. Planning is the whole job: this command produces a specification
|
|
||||||
and writes no menu code. `/todo_add_command` implements what comes out.
|
|
||||||
|
|
||||||
### The effort tier
|
|
||||||
|
|
||||||
The `effort: max` above applies to this invocation and to it alone.
|
|
||||||
|
|
||||||
Two other levers exist, and they are the user's to pull, not yours. `/effort
|
|
||||||
ultracode` PINS ultracode for the rest of the session — every substantive task
|
|
||||||
then goes through the Workflow tool, and an interactive terminal releases the
|
|
||||||
pin with `/effort high`. The bare keyword `ultracode` in a typed prompt opts in
|
|
||||||
that ONE turn. Say which of the two would serve, and let the user type it;
|
|
||||||
never claim a pin that is not shown as on.
|
|
||||||
|
|
||||||
Invoking this command is itself an explicit opt-in to multi-agent
|
|
||||||
orchestration, so the Workflow tool is available here without any further ask.
|
|
||||||
Reach for it when the plan genuinely has independent dimensions to explore in
|
|
||||||
parallel — several candidate designs, or a survey of how the existing menus
|
|
||||||
already solve the problem. A single obvious entry does not need one, and a
|
|
||||||
workflow spawned for it burns tokens the user is paying for.
|
|
||||||
|
|
||||||
### 1. Ask before planning
|
|
||||||
|
|
||||||
Ask with the question tool, never as prose the user has to answer in a
|
|
||||||
paragraph. Ask ONLY what changes the plan — a question whose every answer
|
|
||||||
leads to the same design is noise, and four is the ceiling per round.
|
|
||||||
|
|
||||||
What usually forks the design, in this repository:
|
|
||||||
|
|
||||||
- **Which menu.** Git, Code, Database, Config, Network, Process, Test, Update,
|
|
||||||
Run, Doc, Security — the parent decides who finds the entry.
|
|
||||||
- **Pattern A or B.** A hard-coded method when the entry prompts, branches or
|
|
||||||
reads state; a `todo.json` entry when it is one bash command or one make
|
|
||||||
target. Guessing wrong costs a rewrite, not an edit.
|
|
||||||
- **What it does on failure.** An entry that stops at the first error, one that
|
|
||||||
carries on and reports at the end, and one that asks before each step are
|
|
||||||
three different features wearing one name.
|
|
||||||
- **Whether it destroys anything.** A command that drops a database, deletes a
|
|
||||||
VM or overwrites a file needs a confirmation prompt and a name typed in full;
|
|
||||||
the repository already writes them that way.
|
|
||||||
- **Whether it touches customer data.** Only `private/` may hold it. If the
|
|
||||||
answer is yes, the plan says where the data lives and what never leaves it.
|
|
||||||
|
|
||||||
Take the answers as given. When one contradicts what the code does, say so in
|
|
||||||
a sentence and plan what was asked for.
|
|
||||||
|
|
||||||
### 2. Plan with superpowers when it is installed
|
|
||||||
|
|
||||||
Read the context block above. When `superpowers` appears among the installed
|
|
||||||
plugins, use it: its brainstorming skill for the design, its subagent-driven
|
|
||||||
development and code-review skills for the shape of the work, its systematic
|
|
||||||
debugging skill when the entry wraps something that already misbehaves.
|
|
||||||
|
|
||||||
When it is absent, say so in one line and plan without it — plan mode, the
|
|
||||||
repository's own conventions, and the menus already written. Do not install it
|
|
||||||
from here: `TODO › Execute › GPT code › Plugins Claude Code` carries the
|
|
||||||
ERPLibre list and the install is the user's decision, not a side effect of
|
|
||||||
asking for a plan.
|
|
||||||
|
|
||||||
### 3. Read what already exists
|
|
||||||
|
|
||||||
A menu of this size has almost always solved the problem next door. Before
|
|
||||||
designing anything, find the two or three closest entries and read them —
|
|
||||||
`grep -n "def prompt_execute" script/todo/todo.py` for the parents, then the
|
|
||||||
private methods under them. Copy the shape that is there: the same
|
|
||||||
confirmation prompt, the same `t()` keys, the same way of running a command.
|
|
||||||
An entry that behaves like its neighbours needs no explaining.
|
|
||||||
|
|
||||||
### 4. What the plan contains
|
|
||||||
|
|
||||||
Write it to `tasks/todo.md` — `tasks/` is not versioned, which is why the
|
|
||||||
convention sends working material there — as checkable items, and state:
|
|
||||||
|
|
||||||
- the parent menu and the position of the entry in it;
|
|
||||||
- pattern A or B, and why the other was rejected;
|
|
||||||
- the exact i18n keys, with their French and English text, both mandatory;
|
|
||||||
- for pattern A, the method name and its signature; for pattern B, the
|
|
||||||
`todo.json` section and the command line;
|
|
||||||
- what the entry prints on success and on failure, and every confirmation it
|
|
||||||
asks for;
|
|
||||||
- how to verify it: the syntax checks, the unit test to add under `test/`, and
|
|
||||||
what to run by hand. A real machine goes to `long_test/`, never to `test/`,
|
|
||||||
which stays runnable in seconds.
|
|
||||||
|
|
||||||
Then stop and hand it over. The plan is checked before code is written — that
|
|
||||||
is the whole point of planning at this tier. When it is approved,
|
|
||||||
`/todo_add_command` implements it.
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"preset": "example_ssl_vpn",
|
|
||||||
"label": "Example campus SSL VPN (template)",
|
|
||||||
"hint": "Cisco AnyConnect gateway with an authentication group",
|
|
||||||
"driver": "openconnect",
|
|
||||||
"server": "ssl.vpn.example-campus.net",
|
|
||||||
"port": 443,
|
|
||||||
"oc_protocol": "anyconnect",
|
|
||||||
"oc_authgroup": "CampusSSL",
|
|
||||||
"oc_sso": false,
|
|
||||||
"oc_password_len": 0,
|
|
||||||
"routes": [],
|
|
||||||
"default_route": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"matrix": [
|
"matrix": [
|
||||||
{
|
{
|
||||||
"mobile": "2026.03.29.01",
|
"mobile": "2026.03.29.01",
|
||||||
"erplibre": "1.7.0",
|
"erplibre": "1.6.0",
|
||||||
"odoo": "18.0",
|
"odoo": "18.0",
|
||||||
"sync_api_version": "1.0",
|
"sync_api_version": "1.0",
|
||||||
"modules_required": [
|
"modules_required": [
|
||||||
|
|
|
||||||
|
|
@ -1042,70 +1042,3 @@ Tester les générations des templates :
|
||||||
```bash
|
```bash
|
||||||
make test_code_generator_template
|
make test_code_generator_template
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Comments in generated code
|
|
||||||
|
|
||||||
Generated code carries comments like any other, and the same rule applies: a
|
|
||||||
comment says HOW the code works. It never carries identifying data — no
|
|
||||||
customer or third-party organisation, no real database name, no machine, no
|
|
||||||
address, no label or figure taken from a customer's data — and it does not
|
|
||||||
tell the story of the investigation that produced it.
|
|
||||||
|
|
||||||
A module generated from an existing database inherits what that database
|
|
||||||
holds: re-read its comments and docstrings before committing. A customer or
|
|
||||||
database name gets in on its own.
|
|
||||||
|
|
||||||
## Cleaning up as you go
|
|
||||||
|
|
||||||
Nothing gets cleaned in one sweep. You fix the comments of the file you are
|
|
||||||
touching, at the moment you touch it. A `pre-commit` hook lists what is worth
|
|
||||||
re-reading in the files you staged, and never blocks the commit:
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Les commentaires du code produit
|
|
||||||
|
|
||||||
Le code généré porte des commentaires comme le reste, et la même règle
|
|
||||||
s'applique : un commentaire dit COMMENT le code marche. Il ne porte jamais de
|
|
||||||
donnée identifiante — ni client ni organisation tierce, ni nom de base réelle,
|
|
||||||
ni machine, ni adresse, ni libellé ou chiffre tiré des données d'un client — et
|
|
||||||
il ne raconte pas l'enquête qui l'a produit.
|
|
||||||
|
|
||||||
Un module généré à partir d'une base existante hérite de ce qu'elle contient :
|
|
||||||
relire ses commentaires et ses docstrings avant de committer. Un nom de client
|
|
||||||
ou de base y arrive tout seul.
|
|
||||||
|
|
||||||
## Le nettoyage au fur et à mesure
|
|
||||||
|
|
||||||
Rien ne se nettoie en une passe. On corrige les commentaires du fichier qu'on
|
|
||||||
touche, au moment où on le touche. Un hook `pre-commit` liste ce qui est à
|
|
||||||
relire dans les fichiers indexés, et ne bloque jamais le commit :
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
```bash
|
|
||||||
git config core.hooksPath script/git/hooks
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
The same tool runs by hand, on a file, a directory or the index. It reports
|
|
||||||
`identifiant` findings — an address, an e-mail, an account path — which are to
|
|
||||||
be removed, and `récit` signals —
|
|
||||||
a witness marker, a date, the first person — which are to be RE-READ: a
|
|
||||||
durable fact stays, the incident where it was observed goes. Exit codes follow
|
|
||||||
the repository convention: 0 nothing to report, 1 findings, 2 the tool failed.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
Le même outil se lance à la main, sur un fichier, un répertoire ou l'index. Il
|
|
||||||
signale les trouvailles `identifiant` — adresse, courriel, chemin de compte —
|
|
||||||
qui sont à retirer, et les signaux
|
|
||||||
`récit` — marqueur de témoignage, date, première personne — qui sont à RELIRE :
|
|
||||||
un fait durable reste, l'incident où on l'a observé part. Les codes de sortie
|
|
||||||
suivent la convention du dépôt : 0 rien à signaler, 1 des trouvailles, 2
|
|
||||||
l'outil a échoué.
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
```bash
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
|
||||||
python3 script/analyse/check_comment_hygiene.py --staged
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -649,39 +649,3 @@ Tester les générations des templates :
|
||||||
```bash
|
```bash
|
||||||
make test_code_generator_template
|
make test_code_generator_template
|
||||||
```
|
```
|
||||||
|
|
||||||
## Les commentaires du code produit
|
|
||||||
|
|
||||||
Le code généré porte des commentaires comme le reste, et la même règle
|
|
||||||
s'applique : un commentaire dit COMMENT le code marche. Il ne porte jamais de
|
|
||||||
donnée identifiante — ni client ni organisation tierce, ni nom de base réelle,
|
|
||||||
ni machine, ni adresse, ni libellé ou chiffre tiré des données d'un client — et
|
|
||||||
il ne raconte pas l'enquête qui l'a produit.
|
|
||||||
|
|
||||||
Un module généré à partir d'une base existante hérite de ce qu'elle contient :
|
|
||||||
relire ses commentaires et ses docstrings avant de committer. Un nom de client
|
|
||||||
ou de base y arrive tout seul.
|
|
||||||
|
|
||||||
## Le nettoyage au fur et à mesure
|
|
||||||
|
|
||||||
Rien ne se nettoie en une passe. On corrige les commentaires du fichier qu'on
|
|
||||||
touche, au moment où on le touche. Un hook `pre-commit` liste ce qui est à
|
|
||||||
relire dans les fichiers indexés, et ne bloque jamais le commit :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config core.hooksPath script/git/hooks
|
|
||||||
```
|
|
||||||
|
|
||||||
Le même outil se lance à la main, sur un fichier, un répertoire ou l'index. Il
|
|
||||||
signale les trouvailles `identifiant` — adresse, courriel, chemin de compte —
|
|
||||||
qui sont à retirer, et les signaux
|
|
||||||
`récit` — marqueur de témoignage, date, première personne — qui sont à RELIRE :
|
|
||||||
un fait durable reste, l'incident où on l'a observé part. Les codes de sortie
|
|
||||||
suivent la convention du dépôt : 0 rien à signaler, 1 des trouvailles, 2
|
|
||||||
l'outil a échoué.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
|
||||||
python3 script/analyse/check_comment_hygiene.py --staged
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
|
||||||
```
|
|
||||||
|
|
@ -414,38 +414,3 @@ TODO
|
||||||
```bash
|
```bash
|
||||||
make test_code_generator_template
|
make test_code_generator_template
|
||||||
```
|
```
|
||||||
|
|
||||||
## Comments in generated code
|
|
||||||
|
|
||||||
Generated code carries comments like any other, and the same rule applies: a
|
|
||||||
comment says HOW the code works. It never carries identifying data — no
|
|
||||||
customer or third-party organisation, no real database name, no machine, no
|
|
||||||
address, no label or figure taken from a customer's data — and it does not
|
|
||||||
tell the story of the investigation that produced it.
|
|
||||||
|
|
||||||
A module generated from an existing database inherits what that database
|
|
||||||
holds: re-read its comments and docstrings before committing. A customer or
|
|
||||||
database name gets in on its own.
|
|
||||||
|
|
||||||
## Cleaning up as you go
|
|
||||||
|
|
||||||
Nothing gets cleaned in one sweep. You fix the comments of the file you are
|
|
||||||
touching, at the moment you touch it. A `pre-commit` hook lists what is worth
|
|
||||||
re-reading in the files you staged, and never blocks the commit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config core.hooksPath script/git/hooks
|
|
||||||
```
|
|
||||||
|
|
||||||
The same tool runs by hand, on a file, a directory or the index. It reports
|
|
||||||
`identifiant` findings — an address, an e-mail, an account path — which are to
|
|
||||||
be removed, and `récit` signals —
|
|
||||||
a witness marker, a date, the first person — which are to be RE-READ: a
|
|
||||||
durable fact stays, the incident where it was observed goes. Exit codes follow
|
|
||||||
the repository convention: 0 nothing to report, 1 findings, 2 the tool failed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
|
||||||
python3 script/analyse/check_comment_hygiene.py --staged
|
|
||||||
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
|
||||||
```
|
|
||||||
|
|
@ -1,678 +0,0 @@
|
||||||
<!---------------------------->
|
|
||||||
<!-- multilingual suffix: en, fr -->
|
|
||||||
<!-- no suffix: en -->
|
|
||||||
<!---------------------------->
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
# Mail client
|
|
||||||
|
|
||||||
A mail client built into the TODO CLI: several accounts, IMAP + SMTP, and a
|
|
||||||
local cache — so you can read and answer email without leaving
|
|
||||||
`./script/todo/todo.py`.
|
|
||||||
|
|
||||||
Every `Mail > ...` path below is shorthand for
|
|
||||||
`TODO > [3] Assistant > [2] Mail - Read and send email > ...` — the full path
|
|
||||||
is spelled out once, in "Adding an account".
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
# Client courriel
|
|
||||||
|
|
||||||
Un client courriel intégré au CLI TODO : plusieurs comptes, IMAP + SMTP, et
|
|
||||||
un cache local — pour lire et répondre à son courriel sans quitter
|
|
||||||
`./script/todo/todo.py`.
|
|
||||||
|
|
||||||
Chaque chemin `Courriel > ...` ci-dessous est un raccourci pour
|
|
||||||
`TODO > [3] Assistant > [2] Courriel - Lire et envoyer du courriel > ...` — le
|
|
||||||
chemin complet est écrit une fois, dans « Ajouter un compte ».
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Four Python packages, already listed in `requirement/erplibre_require-ments.txt`
|
|
||||||
(the `.venv.erplibre` environment, not an Odoo venv):
|
|
||||||
|
|
||||||
- `cryptography` — seals the local cache in `encrypted` and `ephemeral` mode.
|
|
||||||
- `keyring` — the system keyring, one of the two places a password can live.
|
|
||||||
- `pykeepass` — the KDBX vault, the other place, and the one the client tries
|
|
||||||
first.
|
|
||||||
- `textual` — the terminal UI itself. Without it, "Open the mail client
|
|
||||||
(TUI)" prints a message and does nothing; the rest of the menu (accounts,
|
|
||||||
sync, cache) still works.
|
|
||||||
|
|
||||||
Install them with:
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Prérequis
|
|
||||||
|
|
||||||
Quatre paquets Python, déjà listés dans
|
|
||||||
`requirement/erplibre_require-ments.txt` (l'environnement `.venv.erplibre`,
|
|
||||||
pas un venv Odoo) :
|
|
||||||
|
|
||||||
- `cryptography` — scelle le cache local en mode `encrypted` et `ephemeral`.
|
|
||||||
- `keyring` — le trousseau système, l'un des deux endroits où peut vivre un
|
|
||||||
mot de passe.
|
|
||||||
- `pykeepass` — le coffre KDBX, l'autre endroit, celui que le client essaie
|
|
||||||
en premier.
|
|
||||||
- `textual` — l'interface terminal elle-même. Sans lui, « Ouvrir le client
|
|
||||||
courriel (TUI) » affiche un message et ne fait rien ; le reste du menu
|
|
||||||
(comptes, synchronisation, cache) fonctionne quand même.
|
|
||||||
|
|
||||||
Installez-les avec :
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
```bash
|
|
||||||
.venv.erplibre/bin/pip install -r requirement/erplibre_require-ments.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
### App passwords for Gmail, Outlook and iCloud
|
|
||||||
|
|
||||||
Phase 1 speaks plain IMAP/SMTP login only — no OAuth yet (that is phase 2).
|
|
||||||
Gmail, Outlook and iCloud have all closed that door to the account's real
|
|
||||||
password, so each of these three presets requires an **app password**
|
|
||||||
instead:
|
|
||||||
|
|
||||||
| Provider | Where to generate it |
|
|
||||||
|---|---|
|
|
||||||
| Gmail | Enable 2-step verification, then [myaccount.google.com](https://myaccount.google.com/security) > Security > App passwords |
|
|
||||||
| Outlook / Microsoft 365 | [account.microsoft.com](https://account.microsoft.com/security) > Security > Advanced security options > App passwords |
|
|
||||||
| iCloud | [account.apple.com](https://account.apple.com/) > Sign-In and Security > App-Specific Passwords |
|
|
||||||
|
|
||||||
Use that generated password when account setup asks for one — never the
|
|
||||||
account's normal password. The "Standard server" preset (generic IMAP/SMTP)
|
|
||||||
does not need one.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
### Mots de passe d'application pour Gmail, Outlook et iCloud
|
|
||||||
|
|
||||||
La phase 1 ne parle qu'IMAP/SMTP en authentification simple — pas encore
|
|
||||||
OAuth (ça, c'est la phase 2). Gmail, Outlook et iCloud ont tous les trois
|
|
||||||
fermé cette porte au vrai mot de passe du compte : chacun de ces trois
|
|
||||||
préréglages exige donc un **mot de passe d'application** à la place :
|
|
||||||
|
|
||||||
| Fournisseur | Où le générer |
|
|
||||||
|---|---|
|
|
||||||
| Gmail | Activez la validation en deux étapes, puis [myaccount.google.com](https://myaccount.google.com/security) > Sécurité > Mots de passe des applications |
|
|
||||||
| Outlook / Microsoft 365 | [account.microsoft.com](https://account.microsoft.com/security) > Sécurité > Options de sécurité avancées > Mots de passe d'application |
|
|
||||||
| iCloud | [account.apple.com](https://account.apple.com/) > Connexion et sécurité > Mots de passe spécifiques aux applications |
|
|
||||||
|
|
||||||
Utilisez ce mot de passe généré quand la configuration du compte en demande
|
|
||||||
un — jamais le mot de passe normal du compte. Le préréglage « Serveur
|
|
||||||
standard » (IMAP/SMTP générique) n'en a pas besoin.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Adding an account
|
|
||||||
|
|
||||||
Menu path: `TODO > [3] Assistant > [2] Mail - Read and send email > [2]
|
|
||||||
Accounts > [2] Add an account`.
|
|
||||||
|
|
||||||
The prompts, in order:
|
|
||||||
|
|
||||||
1. **Short account name** — becomes both the folder name under
|
|
||||||
`~/.erplibre/mail/` and the vault reference, so it cannot contain `/` or
|
|
||||||
start with a dot.
|
|
||||||
2. **Email address**.
|
|
||||||
3. **Display name** (optional) — shown in the `From:` header as
|
|
||||||
`Display Name <email>`.
|
|
||||||
4. **Provider** — a number from the printed list: Gmail, Outlook, iCloud, or
|
|
||||||
"Standard server" (generic IMAP/SMTP).
|
|
||||||
5. If you picked "Standard server", the **IMAP host** and **SMTP host** are
|
|
||||||
asked next; the other presets fill these in for you.
|
|
||||||
6. If the preset requires an app password, its note is printed here as a
|
|
||||||
reminder.
|
|
||||||
7. **Password** — typed hidden (`getpass`), then stored — never written to
|
|
||||||
`accounts.json`.
|
|
||||||
|
|
||||||
Where the password goes: at the password step, the client hands off to the
|
|
||||||
CLI's shared **KDBX manager** — the same one already used for the OpenAI
|
|
||||||
key and Odoo credentials. It reads `kdbx.path` / `kdbx.password` from the
|
|
||||||
TODO config (`script/todo/todo.json`, overridable in
|
|
||||||
`private/todo/todo_override.json` / `private/todo/todo_override_private.json`).
|
|
||||||
If `kdbx.path` isn't set yet, a graphical file picker pops up asking you to
|
|
||||||
choose an existing `.kdbx` file — it needs a display, and cancelling it (or
|
|
||||||
running headless) fails account creation with "le fichier kdbx n'a pas pu
|
|
||||||
être ouvert" (French — see Troubleshooting). **Set `kdbx.path` (and
|
|
||||||
`kdbx.password`, to skip the prompt) before adding your first account**,
|
|
||||||
pointing at a `.kdbx` vault you already have (create one with KeePassXC or
|
|
||||||
similar). The system keyring is only ever used for an account whose
|
|
||||||
`secret_ref` already points at one — the menu itself always writes new
|
|
||||||
accounts into the KDBX vault.
|
|
||||||
|
|
||||||
`accounts.json` (at `~/.erplibre/mail/accounts.json`) only ever holds a
|
|
||||||
`secret_ref` such as `kdbx:ERPLibre/Mail/perso` — a pointer, never the
|
|
||||||
secret. It is safe to read, edit by hand, or check into a private backup.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Ajouter un compte
|
|
||||||
|
|
||||||
Chemin de menu : `TODO > [3] Assistant > [2] Courriel - Lire et envoyer du
|
|
||||||
courriel > [2] Comptes > [2] Ajouter un compte`.
|
|
||||||
|
|
||||||
Les questions, dans l'ordre :
|
|
||||||
|
|
||||||
1. **Nom court du compte** — devient à la fois le nom de dossier sous
|
|
||||||
`~/.erplibre/mail/` et la référence dans le coffre : il ne peut donc pas
|
|
||||||
contenir `/` ni commencer par un point.
|
|
||||||
2. **Adresse courriel**.
|
|
||||||
3. **Nom affiché** (facultatif) — apparaît dans l'en-tête `De :` comme
|
|
||||||
`Nom affiché <email>`.
|
|
||||||
4. **Fournisseur** — un numéro dans la liste affichée : Gmail, Outlook,
|
|
||||||
iCloud, ou « Serveur standard » (IMAP/SMTP générique).
|
|
||||||
5. Si vous choisissez « Serveur standard », le **serveur IMAP** puis le
|
|
||||||
**serveur SMTP** sont demandés ensuite ; les autres préréglages les
|
|
||||||
remplissent déjà pour vous.
|
|
||||||
6. Si le préréglage exige un mot de passe d'application, sa note s'affiche
|
|
||||||
ici en rappel.
|
|
||||||
7. **Mot de passe** — saisi masqué (`getpass`), puis rangé dans le coffre —
|
|
||||||
jamais écrit dans `accounts.json`.
|
|
||||||
|
|
||||||
Où va le mot de passe : à l'étape du mot de passe, le client passe par le
|
|
||||||
**gestionnaire KDBX** partagé du CLI — le même que pour la clé OpenAI et
|
|
||||||
les identifiants Odoo. Il lit `kdbx.path` / `kdbx.password` dans la
|
|
||||||
configuration TODO (`script/todo/todo.json`, surchargeable dans
|
|
||||||
`private/todo/todo_override.json` / `private/todo/todo_override_private.json`).
|
|
||||||
Si `kdbx.path` n'est pas encore réglé, une fenêtre de sélection de fichier
|
|
||||||
s'ouvre pour choisir un `.kdbx` existant — il faut un affichage graphique,
|
|
||||||
et l'annuler (ou lancer le CLI sans affichage) fait échouer la création du
|
|
||||||
compte avec « le fichier kdbx n'a pas pu être ouvert » (voir Dépannage).
|
|
||||||
**Réglez `kdbx.path` (et `kdbx.password`, pour éviter l'invite) avant
|
|
||||||
d'ajouter votre premier compte**, en pointant vers un coffre `.kdbx` que
|
|
||||||
vous avez déjà (créez-en un avec KeePassXC ou équivalent). Le trousseau
|
|
||||||
système ne sert que pour un compte dont la `secret_ref` le désigne déjà —
|
|
||||||
le menu écrit toujours les nouveaux comptes dans le coffre KDBX.
|
|
||||||
|
|
||||||
`accounts.json` (dans `~/.erplibre/mail/accounts.json`) ne contient jamais
|
|
||||||
qu'une `secret_ref` du genre `kdbx:ERPLibre/Mail/perso` — une référence,
|
|
||||||
jamais le secret. Il est sans danger à lire, à éditer à la main, ou à
|
|
||||||
mettre dans une sauvegarde privée.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## The three cache modes
|
|
||||||
|
|
||||||
Every account keeps a local cache — a small SQLite database plus one file
|
|
||||||
per downloaded message — so the inbox stays readable offline. Three modes
|
|
||||||
control what that cache leaves on disk:
|
|
||||||
|
|
||||||
| Mode | What's on disk | Encryption key |
|
|
||||||
|---|---|---|
|
|
||||||
| `clear` (default) | `~/.erplibre/mail/<account>/cache.db` and `.eml` files, readable as plain text | none |
|
|
||||||
| `encrypted` | same location, but sender, recipients, subject, snippet, message-id and message bodies are sealed with AES-256-GCM | generated once, stored in the vault next to the password (`.../cache-key`) |
|
|
||||||
| `ephemeral` | under `/dev/shm/erplibre-mail-<pid>/<account>/` (or the system temp dir if `/dev/shm` isn't writable), sealed the same way as `encrypted` | generated fresh in RAM at every run, never written anywhere, and the whole directory is removed when the session closes |
|
|
||||||
|
|
||||||
Even in `clear` mode, the technical fields the SQL needs to sort and
|
|
||||||
filter — UID, folder, date, flags, size — are always plain; only the
|
|
||||||
person-identifying fields (and the message body) are ever sealed, and only
|
|
||||||
in `encrypted`/`ephemeral`.
|
|
||||||
|
|
||||||
Set the **general default** at `Mail > [4] Cache > [1] Default cache mode`;
|
|
||||||
it is the `mail_cache_mode` preference (default `clear`). **Override it per
|
|
||||||
account** at `Mail > [4] Cache > [2] Cache mode of one account` — this
|
|
||||||
writes the account's `cache_mode` field in `accounts.json`; leaving it at
|
|
||||||
`null` there means "inherit the general default."
|
|
||||||
|
|
||||||
`Mail > [4] Cache > [3] Cache size and purge` lists every account's
|
|
||||||
effective mode and disk usage, and can erase one account's cache entirely
|
|
||||||
(after confirmation) — the next sync rebuilds it from scratch.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Les trois modes de cache
|
|
||||||
|
|
||||||
Chaque compte garde un cache local — une petite base SQLite plus un fichier
|
|
||||||
par message téléchargé — pour que la boîte de réception reste lisible hors
|
|
||||||
ligne. Trois modes contrôlent ce que ce cache laisse sur le disque :
|
|
||||||
|
|
||||||
| Mode | Ce qui reste sur le disque | Clé de chiffrement |
|
|
||||||
|---|---|---|
|
|
||||||
| `clear` (par défaut) | `~/.erplibre/mail/<compte>/cache.db` et les fichiers `.eml`, lisibles en clair | aucune |
|
|
||||||
| `encrypted` | même emplacement, mais l'expéditeur, les destinataires, le sujet, l'extrait, le Message-ID et le corps des messages sont scellés en AES-256-GCM | générée une fois, rangée dans le coffre à côté du mot de passe (`.../cache-key`) |
|
|
||||||
| `ephemeral` | sous `/dev/shm/erplibre-mail-<pid>/<compte>/` (ou le dossier temporaire système si `/dev/shm` n'est pas inscriptible), scellé comme `encrypted` | tirée en RAM à chaque lancement, jamais écrite nulle part, et tout le dossier est effacé à la fermeture de la session |
|
|
||||||
|
|
||||||
Même en mode `clear`, les champs techniques dont le SQL a besoin pour trier
|
|
||||||
et filtrer — UID, dossier, date, drapeaux, taille — restent toujours en
|
|
||||||
clair ; seuls les champs qui identifient des personnes (et le corps du
|
|
||||||
message) sont scellés, et seulement en `encrypted`/`ephemeral`.
|
|
||||||
|
|
||||||
Réglez le **défaut général** dans `Courriel > [4] Cache > [1] Mode de cache
|
|
||||||
par défaut` ; c'est la préférence `mail_cache_mode` (défaut `clear`).
|
|
||||||
**Surchargez-le par compte** dans `Courriel > [4] Cache > [2] Mode de cache
|
|
||||||
d'un compte` — ceci écrit le champ `cache_mode` du compte dans
|
|
||||||
`accounts.json` ; le laisser à `null` là-bas veut dire « hérite du défaut
|
|
||||||
général ».
|
|
||||||
|
|
||||||
`Courriel > [4] Cache > [3] Taille du cache et purge` liste le mode
|
|
||||||
effectif et l'espace disque de chaque compte, et peut effacer entièrement le
|
|
||||||
cache d'un compte (après confirmation) — la prochaine synchronisation le
|
|
||||||
reconstruit à partir de zéro.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## The TUI
|
|
||||||
|
|
||||||
`Mail > [1] Open the mail client (TUI)` opens a three-pane screen: an
|
|
||||||
account/folder tree on the left, the message list in the middle, and a
|
|
||||||
preview pane on the right, with a status line at the bottom.
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|---|---|
|
|
||||||
| `↑` `↓` `Tab` | move within a pane / move focus between panes (Textual defaults) |
|
|
||||||
| `h` | open the help window: every shortcut plus a few notes, closed with `Escape` |
|
|
||||||
| `z` | toggle full-screen preview (hides the folder tree and the message list) |
|
|
||||||
| `Escape` | leave full-screen |
|
|
||||||
| `v` | cycle the layout: columns, split, stacked |
|
|
||||||
| `+` / `-` | grow / shrink the pane that has focus |
|
|
||||||
| `0` | back to the default pane sizes |
|
|
||||||
| `r` | sync the account of the currently selected folder (all its folders) |
|
|
||||||
| `Shift+R` | sync every account |
|
|
||||||
| `/` | open the search field (filters the currently visible list only — locally, over subject/from/to/snippet; it does not search the server) |
|
|
||||||
| `s` / `u` | mark the selected message seen / unseen |
|
|
||||||
| `c` | compose a new message |
|
|
||||||
| `a` / `Shift+A` | reply / reply all |
|
|
||||||
| `f` | forward |
|
|
||||||
| `w` | save the message's **first** attachment to `~/Téléchargements` (created if missing) |
|
|
||||||
| `n` | add an account without leaving the client |
|
|
||||||
| `l` | show the tail of `~/.erplibre/mail.log` and this session's sync errors |
|
|
||||||
| `q` | quit |
|
|
||||||
|
|
||||||
This table is written by hand and can fall behind the code; the `h` window
|
|
||||||
cannot. It builds its list from the application's own key bindings every time
|
|
||||||
it opens, so it is the reference if the two ever disagree.
|
|
||||||
|
|
||||||
The bars between the panes can also be dragged with the mouse, and pane sizes
|
|
||||||
are remembered per layout.
|
|
||||||
|
|
||||||
The footer's key hints, like the help window, follow the CLI's chosen
|
|
||||||
language, as do the account tree, the message list and the preview text.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Le TUI
|
|
||||||
|
|
||||||
`Courriel > [1] Ouvrir le client courriel (TUI)` ouvre un écran en trois
|
|
||||||
volets : l'arbre comptes/dossiers à gauche, la liste des messages au
|
|
||||||
centre, et un aperçu à droite, avec une ligne de statut en bas.
|
|
||||||
|
|
||||||
| Touche | Action |
|
|
||||||
|---|---|
|
|
||||||
| `↑` `↓` `Tab` | se déplacer dans un volet / changer de volet (comportement par défaut de Textual) |
|
|
||||||
| `h` | ouvre la fenêtre d'aide : tous les raccourcis et quelques repères, fermée par `Échap` |
|
|
||||||
| `z` | plein écran sur l'aperçu (masque l'arbre et la liste) |
|
|
||||||
| `Échap` | quitter le plein écran |
|
|
||||||
| `v` | change de disposition : colonnes, partagée, empilée |
|
|
||||||
| `+` / `-` | agrandir / rétrécir le volet qui a le focus |
|
|
||||||
| `0` | revenir aux tailles de volets par défaut |
|
|
||||||
| `r` | synchronise le compte du dossier actuellement sélectionné (tous ses dossiers) |
|
|
||||||
| `Shift+R` | synchronise tous les comptes |
|
|
||||||
| `/` | ouvre le champ de recherche (filtre seulement la liste déjà affichée — localement, sur sujet/de/à/extrait ; ne cherche pas sur le serveur) |
|
|
||||||
| `s` / `u` | marquer le message sélectionné lu / non lu |
|
|
||||||
| `c` | écrire un nouveau message |
|
|
||||||
| `a` / `Shift+A` | répondre / répondre à tous |
|
|
||||||
| `f` | transférer |
|
|
||||||
| `w` | enregistrer la **première** pièce jointe du message dans `~/Téléchargements` (créé s'il n'existe pas) |
|
|
||||||
| `n` | ajouter un compte sans quitter le client |
|
|
||||||
| `l` | affiche la fin de `~/.erplibre/mail.log` et les erreurs de synchronisation de la session |
|
|
||||||
| `q` | quitter |
|
|
||||||
|
|
||||||
Ce tableau est écrit à la main et peut prendre du retard sur le code ; la
|
|
||||||
fenêtre `h`, elle, ne le peut pas : elle construit sa liste depuis les
|
|
||||||
liaisons de l'application à chaque ouverture. En cas de désaccord entre les
|
|
||||||
deux, c'est elle qui a raison.
|
|
||||||
|
|
||||||
Les barres entre les volets se glissent aussi à la souris, et les tailles
|
|
||||||
sont retenues par disposition.
|
|
||||||
|
|
||||||
Les indices de touches du pied d'écran, comme la fenêtre d'aide, suivent la
|
|
||||||
langue choisie dans le CLI, tout comme l'arbre des comptes, la liste et le
|
|
||||||
texte d'aperçu.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Writing a message
|
|
||||||
|
|
||||||
`c` opens the compose form: `To`, `Cc`, `Subject`, an `Attachments` field
|
|
||||||
(semicolon-separated file paths — a comma is legal in a filename, so only
|
|
||||||
`;` splits entries; there is no file picker, type the paths), and a
|
|
||||||
multi-line body. `e` sends the body out to `$EDITOR` (or
|
|
||||||
`nano` if unset) and reads it back; if the editor is missing or exits with
|
|
||||||
an error, the body you had is kept untouched. `Ctrl+S` (or the Send button)
|
|
||||||
delivers the message; `Escape` discards the draft — there is no
|
|
||||||
save-as-draft.
|
|
||||||
|
|
||||||
`a` (reply) and `Shift+A` (reply all) prefill `To`/`Cc`/`Subject`/
|
|
||||||
`In-Reply-To`/`References` and quote the original message in the body. `f`
|
|
||||||
(forward) prefills the `Fwd:` subject and **attaches the original message**
|
|
||||||
automatically, as a `message/rfc822` attachment; the body itself starts
|
|
||||||
empty — write your own note above the attached original.
|
|
||||||
|
|
||||||
Reply, reply-all and forward all need the original message's body
|
|
||||||
available — from the cache, or fetched live if the account is online; with
|
|
||||||
neither, you get "No message selected." / "No message to forward."
|
|
||||||
|
|
||||||
Sending requires the account to be online (composing offline fails with
|
|
||||||
"Account offline: cannot send." — there is no offline outbox). Once sent, a
|
|
||||||
copy is filed into the account's Sent folder over IMAP; if that filing step
|
|
||||||
fails, the status line says so, but the message has already left — it is
|
|
||||||
not resent.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Écrire un message
|
|
||||||
|
|
||||||
`c` ouvre le formulaire : `À`, `Cc`, `Objet`, un champ `Pièces jointes`
|
|
||||||
(chemins de fichiers séparés par un point-virgule — une virgule est légale
|
|
||||||
dans un nom de fichier, donc seul `;` sépare les entrées ; il n'y a pas de
|
|
||||||
sélecteur de fichier, tapez les chemins), et un corps multi-lignes.
|
|
||||||
`e` envoie le corps vers `$EDITOR` (ou `nano` si non défini) et le relit ;
|
|
||||||
si l'éditeur manque ou sort en erreur, le texte de départ est conservé tel
|
|
||||||
quel. `Ctrl+S` (ou le bouton Envoyer) remet le message ; `Échap` abandonne
|
|
||||||
le brouillon — il n'y a pas d'enregistrement en brouillon.
|
|
||||||
|
|
||||||
`a` (répondre) et `Shift+A` (répondre à tous) préremplissent `À`/`Cc`/
|
|
||||||
`Objet`/`In-Reply-To`/`References` et citent le message d'origine dans le
|
|
||||||
corps. `f` (transférer) préremplit l'objet en `Fwd:` et **rattache le
|
|
||||||
message d'origine** automatiquement, en pièce jointe `message/rfc822` ; le
|
|
||||||
corps, lui, part vide — écrivez votre propre mot au-dessus du message
|
|
||||||
joint.
|
|
||||||
|
|
||||||
Répondre, répondre à tous et transférer ont tous besoin du corps du message
|
|
||||||
d'origine — depuis le cache, ou récupéré en direct si le compte est en
|
|
||||||
ligne ; sans l'un ou l'autre, vous obtenez « Aucun message sélectionné. » /
|
|
||||||
« Aucun message à transférer. ».
|
|
||||||
|
|
||||||
Envoyer exige que le compte soit en ligne (écrire hors ligne échoue avec
|
|
||||||
« Compte hors ligne : envoi impossible. » — il n'y a pas de file d'attente
|
|
||||||
hors ligne). Une fois envoyé, une copie est classée dans le dossier
|
|
||||||
Envoyés du compte par IMAP ; si ce classement échoue, la ligne de statut le
|
|
||||||
dit, mais le message est déjà parti — il n'est pas renvoyé.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Synchronization
|
|
||||||
|
|
||||||
A sync pass is incremental: only UIDs above the last known one are
|
|
||||||
fetched, message bodies are never downloaded during a pass (only headers),
|
|
||||||
and bodies are fetched on demand when you open a message. Flags
|
|
||||||
(read/unread, etc.) of already-known messages are re-checked on every
|
|
||||||
pass, so a message read elsewhere shows up correctly here too.
|
|
||||||
|
|
||||||
Sync happens:
|
|
||||||
|
|
||||||
- **At launch** — opening the TUI kicks off one background sync of every
|
|
||||||
account.
|
|
||||||
- **On demand** — `r` (current account) / `Shift+R` (all accounts) inside
|
|
||||||
the TUI, or `Mail > [3] Synchronise now` from the CLI menu (prints a
|
|
||||||
per-account summary to the terminal).
|
|
||||||
- **Automatically, every `mail_refresh_sec` seconds** (default 300 = 5
|
|
||||||
minutes; 0 disables it) — **but only while the TUI is open**. Close it
|
|
||||||
and the timer goes with it; nothing syncs in the background afterward.
|
|
||||||
|
|
||||||
If the server reports a changed `UIDVALIDITY` for a folder (its UIDs no
|
|
||||||
longer mean what they used to — typically after a server-side migration),
|
|
||||||
that folder's cache is purged and resynced from scratch automatically;
|
|
||||||
there is currently no on-screen notice when this happens beyond the folder
|
|
||||||
briefly emptying and refilling.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Synchronisation
|
|
||||||
|
|
||||||
Une passe de synchronisation est incrémentale : seuls les UID supérieurs au
|
|
||||||
dernier connu sont demandés, le corps des messages n'est jamais téléchargé
|
|
||||||
pendant une passe (seulement les en-têtes), et les corps sont récupérés à
|
|
||||||
la demande à l'ouverture d'un message. Les drapeaux (lu/non lu, etc.) des
|
|
||||||
messages déjà connus sont revérifiés à chaque passe, donc un message lu
|
|
||||||
ailleurs apparaît correctement lu ici aussi.
|
|
||||||
|
|
||||||
La synchronisation a lieu :
|
|
||||||
|
|
||||||
- **Au lancement** — ouvrir le TUI déclenche une synchronisation de tous
|
|
||||||
les comptes en arrière-plan.
|
|
||||||
- **À la demande** — `r` (compte courant) / `Shift+R` (tous les comptes)
|
|
||||||
dans le TUI, ou `Courriel > [3] Synchroniser maintenant` depuis le menu
|
|
||||||
CLI (affiche un résumé par compte dans le terminal).
|
|
||||||
- **Automatiquement, toutes les `mail_refresh_sec` secondes** (défaut 300 =
|
|
||||||
5 minutes ; 0 la désactive) — **mais seulement tant que le TUI est
|
|
||||||
ouvert**. Fermez-le et la minuterie part avec lui ; rien ne se
|
|
||||||
synchronise en arrière-plan ensuite.
|
|
||||||
|
|
||||||
Si le serveur annonce un `UIDVALIDITY` changé pour un dossier (ses UID ne
|
|
||||||
veulent plus dire ce qu'ils disaient — typiquement après une migration
|
|
||||||
côté serveur), le cache de ce dossier est purgé et resynchronisé à partir
|
|
||||||
de zéro automatiquement ; il n'y a actuellement aucun avis à l'écran
|
|
||||||
au-delà du dossier qui se vide puis se remplit à nouveau brièvement.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Where the files live
|
|
||||||
|
|
||||||
| Path | Contents |
|
|
||||||
|---|---|
|
|
||||||
| `~/.erplibre/mail/accounts.json` | account list — servers, presets, cache mode, and a `secret_ref` pointer; never a password (mode 0600) |
|
|
||||||
| `~/.erplibre/mail/<account>/cache.db` | that account's SQLite cache (mode 0600, parent directory 0700) |
|
|
||||||
| `~/.erplibre/mail/<account>/<folder>/<uid>.eml` (or `.eml.enc` when sealed) | one file per downloaded message body |
|
|
||||||
| `/dev/shm/erplibre-mail-<pid>/<account>/` | an `ephemeral` account's cache while the process is alive; removed when it exits (a sweep at every startup also clears directories left behind by a killed process) |
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Où sont les fichiers
|
|
||||||
|
|
||||||
| Chemin | Contenu |
|
|
||||||
|---|---|
|
|
||||||
| `~/.erplibre/mail/accounts.json` | la liste des comptes — serveurs, préréglages, mode de cache, et une référence `secret_ref` ; jamais un mot de passe (mode 0600) |
|
|
||||||
| `~/.erplibre/mail/<compte>/cache.db` | le cache SQLite de ce compte (mode 0600, dossier parent 0700) |
|
|
||||||
| `~/.erplibre/mail/<compte>/<dossier>/<uid>.eml` (ou `.eml.enc` s'il est scellé) | un fichier par corps de message téléchargé |
|
|
||||||
| `/dev/shm/erplibre-mail-<pid>/<compte>/` | le cache d'un compte `ephemeral` pendant que le processus vit ; effacé à sa sortie (un balayage au démarrage nettoie aussi ce qu'un processus tué aurait laissé) |
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Error messages raised by the mail package itself (`secrets.py`,
|
|
||||||
`store.py`, `crypto.py`, `accounts.py`, `smtp_send.py`,
|
|
||||||
`imap_transport.py`, `imap_sync.py`) now go through the CLI's translation
|
|
||||||
layer, the same as the menu prompts and TUI labels: running the CLI in
|
|
||||||
English shows them in English. The wording below is quoted in French, this
|
|
||||||
document's reference language; expect the matching English wording when
|
|
||||||
`EL_LANG=en`.
|
|
||||||
|
|
||||||
**"Connection failed: ..." when adding or testing an account.**
|
|
||||||
`Mail > [2] Accounts > [5] Test an account connection` prints the server's
|
|
||||||
exact error and then asks for the password again — up to 3 attempts. The
|
|
||||||
password in the vault is only overwritten *after* a successful connection,
|
|
||||||
so a typo never destroys a working password. If the account is Gmail,
|
|
||||||
Outlook or iCloud, check first that you used an app password (see
|
|
||||||
"Prerequisites" above), not the account's normal one. Opening the TUI
|
|
||||||
itself does not retry automatically: an account with a rejected password
|
|
||||||
gets a ⚠ marker; if it had synced successfully before, its already-cached
|
|
||||||
folders stay visible and readable, they just stop refreshing — only a
|
|
||||||
brand-new account (nothing synced yet) shows no folders at all. Either
|
|
||||||
way, go run "Test an account connection" to fix it.
|
|
||||||
|
|
||||||
**"le fichier kdbx n'a pas pu être ouvert" when adding an account.**
|
|
||||||
The shared KDBX vault isn't configured yet, its file picker was cancelled,
|
|
||||||
or the CLI is running without a display to show that picker. Set
|
|
||||||
`kdbx.path` (and `kdbx.password`) as described in "Adding an account"
|
|
||||||
above, then try again.
|
|
||||||
|
|
||||||
**"le trousseau du système écrirait le mot de passe en clair (backend
|
|
||||||
...)".**
|
|
||||||
`keyring`'s active backend isn't one of the ones known to actually
|
|
||||||
encrypt — this happens over SSH, in a container, or on a machine with no
|
|
||||||
desktop session, where `keyring` silently falls back to a plaintext file
|
|
||||||
store. The client refuses rather than pretend that's safe. Use the KDBX
|
|
||||||
vault instead (see above), or run somewhere a real keyring is unlocked.
|
|
||||||
|
|
||||||
**"Install textual for the mail client (pip)."**
|
|
||||||
`textual` isn't installed. `Mail > [1] Open the mail client (TUI)` just
|
|
||||||
prints this and returns; every other menu entry (accounts, sync, cache)
|
|
||||||
still works without it.
|
|
||||||
|
|
||||||
**The folder cache says it changed (`UIDVALIDITY`).**
|
|
||||||
Nothing to do — the client purges and resyncs that folder by itself the
|
|
||||||
next time it syncs. Expect the message list to empty briefly and refill.
|
|
||||||
|
|
||||||
**"cache illisible, purgez-le et resynchronisez : ...".**
|
|
||||||
The account's `cache.db` is corrupt. `Mail > [4] Cache > [3] Cache size and
|
|
||||||
purge` may itself fail to open the same broken file; if so, delete the
|
|
||||||
account's cache directory by hand and resync:
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Dépannage
|
|
||||||
|
|
||||||
Les messages d'erreur qui viennent du paquet courriel lui-même
|
|
||||||
(`secrets.py`, `store.py`, `crypto.py`, `accounts.py`, `smtp_send.py`,
|
|
||||||
`imap_transport.py`, `imap_sync.py`) passent maintenant par la couche de
|
|
||||||
traduction du CLI, comme les invites de menu et les libellés du TUI :
|
|
||||||
lancer le CLI en anglais les affiche en anglais. Le libellé ci-dessous est
|
|
||||||
cité en français, la langue de référence de ce document ; attendez-vous au
|
|
||||||
libellé anglais correspondant avec `EL_LANG=en`.
|
|
||||||
|
|
||||||
**« Connexion échouée : ... » en ajoutant ou en testant un compte.**
|
|
||||||
`Courriel > [2] Comptes > [5] Tester la connexion d'un compte` affiche
|
|
||||||
l'erreur exacte du serveur puis redemande le mot de passe — jusqu'à 3
|
|
||||||
tentatives. Le mot de passe dans le coffre n'est écrasé qu'*après* une
|
|
||||||
connexion réussie, donc une faute de frappe ne détruit jamais un mot de
|
|
||||||
passe qui fonctionnait. Si le compte est Gmail, Outlook ou iCloud,
|
|
||||||
vérifiez d'abord que vous avez utilisé un mot de passe d'application (voir
|
|
||||||
« Prérequis » plus haut), pas le mot de passe normal du compte. Ouvrir le
|
|
||||||
TUI lui-même ne relance pas cette demande automatiquement : un compte au
|
|
||||||
mot de passe refusé porte un ⚠ ; s'il avait déjà synchronisé avec succès,
|
|
||||||
ses dossiers déjà en cache restent visibles et lisibles, ils cessent
|
|
||||||
seulement de se rafraîchir — seul un compte tout neuf (rien de
|
|
||||||
synchronisé encore) n'affiche aucun dossier du tout. Dans tous les cas,
|
|
||||||
passez par « Tester la connexion d'un compte » pour corriger.
|
|
||||||
|
|
||||||
**« le fichier kdbx n'a pas pu être ouvert » en ajoutant un compte.**
|
|
||||||
Le coffre KDBX partagé n'est pas encore configuré, sa fenêtre de sélection
|
|
||||||
de fichier a été annulée, ou le CLI tourne sans affichage pour la montrer.
|
|
||||||
Réglez `kdbx.path` (et `kdbx.password`) comme décrit dans « Ajouter un
|
|
||||||
compte » plus haut, puis réessayez.
|
|
||||||
|
|
||||||
**« le trousseau du système écrirait le mot de passe en clair (backend
|
|
||||||
...) ».**
|
|
||||||
Le backend actif de `keyring` n'est pas de ceux qu'on sait vraiment
|
|
||||||
chiffrer — ça arrive en SSH, dans un conteneur, ou sur une machine sans
|
|
||||||
session graphique, où `keyring` retombe silencieusement sur un fichier en
|
|
||||||
clair. Le client refuse plutôt que de faire semblant que c'est sûr.
|
|
||||||
Utilisez le coffre KDBX à la place (voir plus haut), ou lancez-le là où un
|
|
||||||
vrai trousseau est déverrouillé.
|
|
||||||
|
|
||||||
**« Installez textual pour le client courriel (pip). »**
|
|
||||||
`textual` n'est pas installé. `Courriel > [1] Ouvrir le client courriel
|
|
||||||
(TUI)` affiche seulement ce message et revient au menu ; tout le reste
|
|
||||||
(comptes, synchronisation, cache) fonctionne quand même sans lui.
|
|
||||||
|
|
||||||
**Le cache d'un dossier signale qu'il a changé (`UIDVALIDITY`).**
|
|
||||||
Rien à faire — le client purge et resynchronise ce dossier tout seul à la
|
|
||||||
prochaine synchronisation. La liste des messages se vide puis se remplit
|
|
||||||
brièvement.
|
|
||||||
|
|
||||||
**« cache illisible, purgez-le et resynchronisez : ... ».**
|
|
||||||
Le `cache.db` du compte est corrompu. `Courriel > [4] Cache > [3] Taille
|
|
||||||
du cache et purge` peut lui-même échouer à ouvrir ce même fichier cassé ;
|
|
||||||
le cas échéant, effacez à la main le dossier de cache du compte et
|
|
||||||
resynchronisez :
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
```bash
|
|
||||||
rm -rf ~/.erplibre/mail/<account>/
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Testing against a real server
|
|
||||||
|
|
||||||
Almost every mail test uses an in-memory double. A double only produces what
|
|
||||||
its author imagined, which is how three protocol bugs reached users. So there
|
|
||||||
is also a **sandbox**: a real IMAP server (Twisted) and a real SMTP server
|
|
||||||
(aiosmtpd) that a test starts on an ephemeral loopback port, talks to over
|
|
||||||
real TCP, and kills when it finishes — pass or fail.
|
|
||||||
|
|
||||||
The point is not conformance. A well-behaved server proves little; this one
|
|
||||||
can **misbehave on purpose**. A test declares the exact bytes a message is
|
|
||||||
made of — raw 8-bit header bytes, an `unknown-8bit` charset — and can drop the
|
|
||||||
connection or refuse a command mid-sync. Adding a new hostile behaviour is a
|
|
||||||
small subclass in `test/mail_sandbox.py`, not a new server.
|
|
||||||
|
|
||||||
These tests do **not** run in the fast loop. Without `twisted` and `aiosmtpd`
|
|
||||||
the whole file skips visibly. Run them deliberately:
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Tester contre un vrai serveur
|
|
||||||
|
|
||||||
Presque tous les tests courriel passent par un double en mémoire. Un double ne
|
|
||||||
produit que ce que son auteur avait imaginé — c'est par là que trois bugs de
|
|
||||||
protocole sont arrivés jusqu'aux utilisateurs. D'où un **bac à sable** : un
|
|
||||||
vrai serveur IMAP (Twisted) et un vrai serveur SMTP (aiosmtpd), qu'un test
|
|
||||||
démarre sur un port éphémère de la boucle locale, à qui il parle en vrai TCP,
|
|
||||||
et qu'il tue en terminant — qu'il réussisse ou qu'il échoue.
|
|
||||||
|
|
||||||
Le but n'est pas la conformité. Un serveur poli ne prouve pas grand-chose ;
|
|
||||||
celui-ci sait **se conduire mal exprès**. Un test déclare les octets exacts
|
|
||||||
d'un message — en-tête en 8 bits bruts, charset `unknown-8bit` — et peut
|
|
||||||
couper la connexion ou refuser une commande en pleine synchronisation.
|
|
||||||
Ajouter une nouvelle méchanceté est une petite sous-classe dans
|
|
||||||
`test/mail_sandbox.py`, pas un nouveau serveur.
|
|
||||||
|
|
||||||
Ces tests ne tournent **pas** dans la boucle rapide. Sans `twisted` ni
|
|
||||||
`aiosmtpd`, tout le fichier se saute visiblement. Pour les lancer
|
|
||||||
volontairement :
|
|
||||||
|
|
||||||
<!-- [common] -->
|
|
||||||
```bash
|
|
||||||
.venv.erplibre/bin/python -m unittest discover -s test \
|
|
||||||
-p test_mail_live_server.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
What it does **not** cover, and will not pretend to:
|
|
||||||
|
|
||||||
- **`SPECIAL-USE`** — Twisted announces only `IMAP4REV1 NAMESPACE IDLE`. The
|
|
||||||
bug where a sent message was filed under a guessed folder name instead of
|
|
||||||
the one the server announced is therefore out of reach. Implementing the
|
|
||||||
extension in the sandbox would only test our own assumption about it, which
|
|
||||||
is the exact failure this sandbox exists to escape.
|
|
||||||
- **No provider quirk** — Gmail's label-as-folder model, Microsoft's OAuth,
|
|
||||||
Apple app passwords: none of it is exercised. The sandbox is a plain
|
|
||||||
RFC 3501 server, not a stand-in for a specific provider.
|
|
||||||
- **No TLS** — the sandbox talks in the clear on `127.0.0.1`. `starttls` and
|
|
||||||
`ssl` code paths are not exercised here.
|
|
||||||
- **Nothing leaves the machine** — no external host, no OS keyring, no
|
|
||||||
`~/.erplibre`, no real credentials, and never a fixed port.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
Ce qu'il ne couvre **pas**, et ne fera pas semblant de couvrir :
|
|
||||||
|
|
||||||
- **`SPECIAL-USE`** — Twisted n'annonce que `IMAP4REV1 NAMESPACE IDLE`. Le bug
|
|
||||||
du message classé sous un nom de dossier deviné plutôt que sous celui
|
|
||||||
annoncé par le serveur reste donc hors de portée. Implémenter l'extension
|
|
||||||
dans le bac à sable ne testerait que notre propre supposition à son sujet —
|
|
||||||
précisément l'erreur que ce bac à sable existe pour éviter.
|
|
||||||
- **Aucune particularité de fournisseur** — les dossiers-étiquettes de Gmail,
|
|
||||||
OAuth chez Microsoft, les mots de passe d'application d'Apple : rien de tout
|
|
||||||
cela n'est exercé. Le bac à sable est un serveur RFC 3501 ordinaire, pas la
|
|
||||||
doublure d'un fournisseur précis.
|
|
||||||
- **Pas de TLS** — le bac à sable parle en clair sur `127.0.0.1`. Les chemins
|
|
||||||
`starttls` et `ssl` ne sont pas exercés ici.
|
|
||||||
- **Rien ne quitte la machine** — aucun hôte externe, aucun trousseau système,
|
|
||||||
aucun `~/.erplibre`, aucun identifiant réel, et jamais un port fixe.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
## Phase 1 limits
|
|
||||||
|
|
||||||
- **No OAuth** — Gmail, Outlook and iCloud need an app password (see
|
|
||||||
above); OAuth is phase 2.
|
|
||||||
- **No statistics** — no read/unread counters or activity dashboards beyond
|
|
||||||
the per-folder unseen count shown in the folder tree.
|
|
||||||
- **No server-side search** — `/` filters only what's already synced to the
|
|
||||||
local cache.
|
|
||||||
- **No offline outbox** — sending requires the account to be online; there
|
|
||||||
is no queue that flushes once you're back online.
|
|
||||||
|
|
||||||
See the [design spec](../docs/superpowers/specs/2026-08-02-email-tui-design.md)
|
|
||||||
for what the following phases add.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
## Limites de la phase 1
|
|
||||||
|
|
||||||
- **Pas d'OAuth** — Gmail, Outlook et iCloud demandent un mot de passe
|
|
||||||
d'application (voir plus haut) ; OAuth arrive en phase 2.
|
|
||||||
- **Pas de statistiques** — aucun compteur lu/non lu global ni tableau de
|
|
||||||
bord d'activité, au-delà du compte de non-lus par dossier affiché dans
|
|
||||||
l'arbre.
|
|
||||||
- **Pas de recherche côté serveur** — `/` ne filtre que ce qui est déjà
|
|
||||||
synchronisé dans le cache local.
|
|
||||||
- **Pas de file d'attente hors ligne** — l'envoi exige que le compte soit
|
|
||||||
en ligne ; rien ne se met en attente pour partir au retour du réseau.
|
|
||||||
|
|
||||||
Voir le [spec de conception](../docs/superpowers/specs/2026-08-02-email-tui-design.md)
|
|
||||||
pour ce qu'apportent les phases suivantes.
|
|
||||||
340
doc/EMAIL.fr.md
340
doc/EMAIL.fr.md
|
|
@ -1,340 +0,0 @@
|
||||||
|
|
||||||
# Client courriel
|
|
||||||
|
|
||||||
Un client courriel intégré au CLI TODO : plusieurs comptes, IMAP + SMTP, et
|
|
||||||
un cache local — pour lire et répondre à son courriel sans quitter
|
|
||||||
`./script/todo/todo.py`.
|
|
||||||
|
|
||||||
Chaque chemin `Courriel > ...` ci-dessous est un raccourci pour
|
|
||||||
`TODO > [3] Assistant > [2] Courriel - Lire et envoyer du courriel > ...` — le
|
|
||||||
chemin complet est écrit une fois, dans « Ajouter un compte ».
|
|
||||||
|
|
||||||
## Prérequis
|
|
||||||
|
|
||||||
Quatre paquets Python, déjà listés dans
|
|
||||||
`requirement/erplibre_require-ments.txt` (l'environnement `.venv.erplibre`,
|
|
||||||
pas un venv Odoo) :
|
|
||||||
|
|
||||||
- `cryptography` — scelle le cache local en mode `encrypted` et `ephemeral`.
|
|
||||||
- `keyring` — le trousseau système, l'un des deux endroits où peut vivre un
|
|
||||||
mot de passe.
|
|
||||||
- `pykeepass` — le coffre KDBX, l'autre endroit, celui que le client essaie
|
|
||||||
en premier.
|
|
||||||
- `textual` — l'interface terminal elle-même. Sans lui, « Ouvrir le client
|
|
||||||
courriel (TUI) » affiche un message et ne fait rien ; le reste du menu
|
|
||||||
(comptes, synchronisation, cache) fonctionne quand même.
|
|
||||||
|
|
||||||
Installez-les avec :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.venv.erplibre/bin/pip install -r requirement/erplibre_require-ments.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mots de passe d'application pour Gmail, Outlook et iCloud
|
|
||||||
|
|
||||||
La phase 1 ne parle qu'IMAP/SMTP en authentification simple — pas encore
|
|
||||||
OAuth (ça, c'est la phase 2). Gmail, Outlook et iCloud ont tous les trois
|
|
||||||
fermé cette porte au vrai mot de passe du compte : chacun de ces trois
|
|
||||||
préréglages exige donc un **mot de passe d'application** à la place :
|
|
||||||
|
|
||||||
| Fournisseur | Où le générer |
|
|
||||||
|---|---|
|
|
||||||
| Gmail | Activez la validation en deux étapes, puis [myaccount.google.com](https://myaccount.google.com/security) > Sécurité > Mots de passe des applications |
|
|
||||||
| Outlook / Microsoft 365 | [account.microsoft.com](https://account.microsoft.com/security) > Sécurité > Options de sécurité avancées > Mots de passe d'application |
|
|
||||||
| iCloud | [account.apple.com](https://account.apple.com/) > Connexion et sécurité > Mots de passe spécifiques aux applications |
|
|
||||||
|
|
||||||
Utilisez ce mot de passe généré quand la configuration du compte en demande
|
|
||||||
un — jamais le mot de passe normal du compte. Le préréglage « Serveur
|
|
||||||
standard » (IMAP/SMTP générique) n'en a pas besoin.
|
|
||||||
|
|
||||||
## Ajouter un compte
|
|
||||||
|
|
||||||
Chemin de menu : `TODO > [3] Assistant > [2] Courriel - Lire et envoyer du
|
|
||||||
courriel > [2] Comptes > [2] Ajouter un compte`.
|
|
||||||
|
|
||||||
Les questions, dans l'ordre :
|
|
||||||
|
|
||||||
1. **Nom court du compte** — devient à la fois le nom de dossier sous
|
|
||||||
`~/.erplibre/mail/` et la référence dans le coffre : il ne peut donc pas
|
|
||||||
contenir `/` ni commencer par un point.
|
|
||||||
2. **Adresse courriel**.
|
|
||||||
3. **Nom affiché** (facultatif) — apparaît dans l'en-tête `De :` comme
|
|
||||||
`Nom affiché <email>`.
|
|
||||||
4. **Fournisseur** — un numéro dans la liste affichée : Gmail, Outlook,
|
|
||||||
iCloud, ou « Serveur standard » (IMAP/SMTP générique).
|
|
||||||
5. Si vous choisissez « Serveur standard », le **serveur IMAP** puis le
|
|
||||||
**serveur SMTP** sont demandés ensuite ; les autres préréglages les
|
|
||||||
remplissent déjà pour vous.
|
|
||||||
6. Si le préréglage exige un mot de passe d'application, sa note s'affiche
|
|
||||||
ici en rappel.
|
|
||||||
7. **Mot de passe** — saisi masqué (`getpass`), puis rangé dans le coffre —
|
|
||||||
jamais écrit dans `accounts.json`.
|
|
||||||
|
|
||||||
Où va le mot de passe : à l'étape du mot de passe, le client passe par le
|
|
||||||
**gestionnaire KDBX** partagé du CLI — le même que pour la clé OpenAI et
|
|
||||||
les identifiants Odoo. Il lit `kdbx.path` / `kdbx.password` dans la
|
|
||||||
configuration TODO (`script/todo/todo.json`, surchargeable dans
|
|
||||||
`private/todo/todo_override.json` / `private/todo/todo_override_private.json`).
|
|
||||||
Si `kdbx.path` n'est pas encore réglé, une fenêtre de sélection de fichier
|
|
||||||
s'ouvre pour choisir un `.kdbx` existant — il faut un affichage graphique,
|
|
||||||
et l'annuler (ou lancer le CLI sans affichage) fait échouer la création du
|
|
||||||
compte avec « le fichier kdbx n'a pas pu être ouvert » (voir Dépannage).
|
|
||||||
**Réglez `kdbx.path` (et `kdbx.password`, pour éviter l'invite) avant
|
|
||||||
d'ajouter votre premier compte**, en pointant vers un coffre `.kdbx` que
|
|
||||||
vous avez déjà (créez-en un avec KeePassXC ou équivalent). Le trousseau
|
|
||||||
système ne sert que pour un compte dont la `secret_ref` le désigne déjà —
|
|
||||||
le menu écrit toujours les nouveaux comptes dans le coffre KDBX.
|
|
||||||
|
|
||||||
`accounts.json` (dans `~/.erplibre/mail/accounts.json`) ne contient jamais
|
|
||||||
qu'une `secret_ref` du genre `kdbx:ERPLibre/Mail/perso` — une référence,
|
|
||||||
jamais le secret. Il est sans danger à lire, à éditer à la main, ou à
|
|
||||||
mettre dans une sauvegarde privée.
|
|
||||||
|
|
||||||
## Les trois modes de cache
|
|
||||||
|
|
||||||
Chaque compte garde un cache local — une petite base SQLite plus un fichier
|
|
||||||
par message téléchargé — pour que la boîte de réception reste lisible hors
|
|
||||||
ligne. Trois modes contrôlent ce que ce cache laisse sur le disque :
|
|
||||||
|
|
||||||
| Mode | Ce qui reste sur le disque | Clé de chiffrement |
|
|
||||||
|---|---|---|
|
|
||||||
| `clear` (par défaut) | `~/.erplibre/mail/<compte>/cache.db` et les fichiers `.eml`, lisibles en clair | aucune |
|
|
||||||
| `encrypted` | même emplacement, mais l'expéditeur, les destinataires, le sujet, l'extrait, le Message-ID et le corps des messages sont scellés en AES-256-GCM | générée une fois, rangée dans le coffre à côté du mot de passe (`.../cache-key`) |
|
|
||||||
| `ephemeral` | sous `/dev/shm/erplibre-mail-<pid>/<compte>/` (ou le dossier temporaire système si `/dev/shm` n'est pas inscriptible), scellé comme `encrypted` | tirée en RAM à chaque lancement, jamais écrite nulle part, et tout le dossier est effacé à la fermeture de la session |
|
|
||||||
|
|
||||||
Même en mode `clear`, les champs techniques dont le SQL a besoin pour trier
|
|
||||||
et filtrer — UID, dossier, date, drapeaux, taille — restent toujours en
|
|
||||||
clair ; seuls les champs qui identifient des personnes (et le corps du
|
|
||||||
message) sont scellés, et seulement en `encrypted`/`ephemeral`.
|
|
||||||
|
|
||||||
Réglez le **défaut général** dans `Courriel > [4] Cache > [1] Mode de cache
|
|
||||||
par défaut` ; c'est la préférence `mail_cache_mode` (défaut `clear`).
|
|
||||||
**Surchargez-le par compte** dans `Courriel > [4] Cache > [2] Mode de cache
|
|
||||||
d'un compte` — ceci écrit le champ `cache_mode` du compte dans
|
|
||||||
`accounts.json` ; le laisser à `null` là-bas veut dire « hérite du défaut
|
|
||||||
général ».
|
|
||||||
|
|
||||||
`Courriel > [4] Cache > [3] Taille du cache et purge` liste le mode
|
|
||||||
effectif et l'espace disque de chaque compte, et peut effacer entièrement le
|
|
||||||
cache d'un compte (après confirmation) — la prochaine synchronisation le
|
|
||||||
reconstruit à partir de zéro.
|
|
||||||
|
|
||||||
## Le TUI
|
|
||||||
|
|
||||||
`Courriel > [1] Ouvrir le client courriel (TUI)` ouvre un écran en trois
|
|
||||||
volets : l'arbre comptes/dossiers à gauche, la liste des messages au
|
|
||||||
centre, et un aperçu à droite, avec une ligne de statut en bas.
|
|
||||||
|
|
||||||
| Touche | Action |
|
|
||||||
|---|---|
|
|
||||||
| `↑` `↓` `Tab` | se déplacer dans un volet / changer de volet (comportement par défaut de Textual) |
|
|
||||||
| `h` | ouvre la fenêtre d'aide : tous les raccourcis et quelques repères, fermée par `Échap` |
|
|
||||||
| `z` | plein écran sur l'aperçu (masque l'arbre et la liste) |
|
|
||||||
| `Échap` | quitter le plein écran |
|
|
||||||
| `v` | change de disposition : colonnes, partagée, empilée |
|
|
||||||
| `+` / `-` | agrandir / rétrécir le volet qui a le focus |
|
|
||||||
| `0` | revenir aux tailles de volets par défaut |
|
|
||||||
| `r` | synchronise le compte du dossier actuellement sélectionné (tous ses dossiers) |
|
|
||||||
| `Shift+R` | synchronise tous les comptes |
|
|
||||||
| `/` | ouvre le champ de recherche (filtre seulement la liste déjà affichée — localement, sur sujet/de/à/extrait ; ne cherche pas sur le serveur) |
|
|
||||||
| `s` / `u` | marquer le message sélectionné lu / non lu |
|
|
||||||
| `c` | écrire un nouveau message |
|
|
||||||
| `a` / `Shift+A` | répondre / répondre à tous |
|
|
||||||
| `f` | transférer |
|
|
||||||
| `w` | enregistrer la **première** pièce jointe du message dans `~/Téléchargements` (créé s'il n'existe pas) |
|
|
||||||
| `n` | ajouter un compte sans quitter le client |
|
|
||||||
| `l` | affiche la fin de `~/.erplibre/mail.log` et les erreurs de synchronisation de la session |
|
|
||||||
| `q` | quitter |
|
|
||||||
|
|
||||||
Ce tableau est écrit à la main et peut prendre du retard sur le code ; la
|
|
||||||
fenêtre `h`, elle, ne le peut pas : elle construit sa liste depuis les
|
|
||||||
liaisons de l'application à chaque ouverture. En cas de désaccord entre les
|
|
||||||
deux, c'est elle qui a raison.
|
|
||||||
|
|
||||||
Les barres entre les volets se glissent aussi à la souris, et les tailles
|
|
||||||
sont retenues par disposition.
|
|
||||||
|
|
||||||
Les indices de touches du pied d'écran, comme la fenêtre d'aide, suivent la
|
|
||||||
langue choisie dans le CLI, tout comme l'arbre des comptes, la liste et le
|
|
||||||
texte d'aperçu.
|
|
||||||
|
|
||||||
## Écrire un message
|
|
||||||
|
|
||||||
`c` ouvre le formulaire : `À`, `Cc`, `Objet`, un champ `Pièces jointes`
|
|
||||||
(chemins de fichiers séparés par un point-virgule — une virgule est légale
|
|
||||||
dans un nom de fichier, donc seul `;` sépare les entrées ; il n'y a pas de
|
|
||||||
sélecteur de fichier, tapez les chemins), et un corps multi-lignes.
|
|
||||||
`e` envoie le corps vers `$EDITOR` (ou `nano` si non défini) et le relit ;
|
|
||||||
si l'éditeur manque ou sort en erreur, le texte de départ est conservé tel
|
|
||||||
quel. `Ctrl+S` (ou le bouton Envoyer) remet le message ; `Échap` abandonne
|
|
||||||
le brouillon — il n'y a pas d'enregistrement en brouillon.
|
|
||||||
|
|
||||||
`a` (répondre) et `Shift+A` (répondre à tous) préremplissent `À`/`Cc`/
|
|
||||||
`Objet`/`In-Reply-To`/`References` et citent le message d'origine dans le
|
|
||||||
corps. `f` (transférer) préremplit l'objet en `Fwd:` et **rattache le
|
|
||||||
message d'origine** automatiquement, en pièce jointe `message/rfc822` ; le
|
|
||||||
corps, lui, part vide — écrivez votre propre mot au-dessus du message
|
|
||||||
joint.
|
|
||||||
|
|
||||||
Répondre, répondre à tous et transférer ont tous besoin du corps du message
|
|
||||||
d'origine — depuis le cache, ou récupéré en direct si le compte est en
|
|
||||||
ligne ; sans l'un ou l'autre, vous obtenez « Aucun message sélectionné. » /
|
|
||||||
« Aucun message à transférer. ».
|
|
||||||
|
|
||||||
Envoyer exige que le compte soit en ligne (écrire hors ligne échoue avec
|
|
||||||
« Compte hors ligne : envoi impossible. » — il n'y a pas de file d'attente
|
|
||||||
hors ligne). Une fois envoyé, une copie est classée dans le dossier
|
|
||||||
Envoyés du compte par IMAP ; si ce classement échoue, la ligne de statut le
|
|
||||||
dit, mais le message est déjà parti — il n'est pas renvoyé.
|
|
||||||
|
|
||||||
## Synchronisation
|
|
||||||
|
|
||||||
Une passe de synchronisation est incrémentale : seuls les UID supérieurs au
|
|
||||||
dernier connu sont demandés, le corps des messages n'est jamais téléchargé
|
|
||||||
pendant une passe (seulement les en-têtes), et les corps sont récupérés à
|
|
||||||
la demande à l'ouverture d'un message. Les drapeaux (lu/non lu, etc.) des
|
|
||||||
messages déjà connus sont revérifiés à chaque passe, donc un message lu
|
|
||||||
ailleurs apparaît correctement lu ici aussi.
|
|
||||||
|
|
||||||
La synchronisation a lieu :
|
|
||||||
|
|
||||||
- **Au lancement** — ouvrir le TUI déclenche une synchronisation de tous
|
|
||||||
les comptes en arrière-plan.
|
|
||||||
- **À la demande** — `r` (compte courant) / `Shift+R` (tous les comptes)
|
|
||||||
dans le TUI, ou `Courriel > [3] Synchroniser maintenant` depuis le menu
|
|
||||||
CLI (affiche un résumé par compte dans le terminal).
|
|
||||||
- **Automatiquement, toutes les `mail_refresh_sec` secondes** (défaut 300 =
|
|
||||||
5 minutes ; 0 la désactive) — **mais seulement tant que le TUI est
|
|
||||||
ouvert**. Fermez-le et la minuterie part avec lui ; rien ne se
|
|
||||||
synchronise en arrière-plan ensuite.
|
|
||||||
|
|
||||||
Si le serveur annonce un `UIDVALIDITY` changé pour un dossier (ses UID ne
|
|
||||||
veulent plus dire ce qu'ils disaient — typiquement après une migration
|
|
||||||
côté serveur), le cache de ce dossier est purgé et resynchronisé à partir
|
|
||||||
de zéro automatiquement ; il n'y a actuellement aucun avis à l'écran
|
|
||||||
au-delà du dossier qui se vide puis se remplit à nouveau brièvement.
|
|
||||||
|
|
||||||
## Où sont les fichiers
|
|
||||||
|
|
||||||
| Chemin | Contenu |
|
|
||||||
|---|---|
|
|
||||||
| `~/.erplibre/mail/accounts.json` | la liste des comptes — serveurs, préréglages, mode de cache, et une référence `secret_ref` ; jamais un mot de passe (mode 0600) |
|
|
||||||
| `~/.erplibre/mail/<compte>/cache.db` | le cache SQLite de ce compte (mode 0600, dossier parent 0700) |
|
|
||||||
| `~/.erplibre/mail/<compte>/<dossier>/<uid>.eml` (ou `.eml.enc` s'il est scellé) | un fichier par corps de message téléchargé |
|
|
||||||
| `/dev/shm/erplibre-mail-<pid>/<compte>/` | le cache d'un compte `ephemeral` pendant que le processus vit ; effacé à sa sortie (un balayage au démarrage nettoie aussi ce qu'un processus tué aurait laissé) |
|
|
||||||
|
|
||||||
## Dépannage
|
|
||||||
|
|
||||||
Les messages d'erreur qui viennent du paquet courriel lui-même
|
|
||||||
(`secrets.py`, `store.py`, `crypto.py`, `accounts.py`, `smtp_send.py`,
|
|
||||||
`imap_transport.py`, `imap_sync.py`) passent maintenant par la couche de
|
|
||||||
traduction du CLI, comme les invites de menu et les libellés du TUI :
|
|
||||||
lancer le CLI en anglais les affiche en anglais. Le libellé ci-dessous est
|
|
||||||
cité en français, la langue de référence de ce document ; attendez-vous au
|
|
||||||
libellé anglais correspondant avec `EL_LANG=en`.
|
|
||||||
|
|
||||||
**« Connexion échouée : ... » en ajoutant ou en testant un compte.**
|
|
||||||
`Courriel > [2] Comptes > [5] Tester la connexion d'un compte` affiche
|
|
||||||
l'erreur exacte du serveur puis redemande le mot de passe — jusqu'à 3
|
|
||||||
tentatives. Le mot de passe dans le coffre n'est écrasé qu'*après* une
|
|
||||||
connexion réussie, donc une faute de frappe ne détruit jamais un mot de
|
|
||||||
passe qui fonctionnait. Si le compte est Gmail, Outlook ou iCloud,
|
|
||||||
vérifiez d'abord que vous avez utilisé un mot de passe d'application (voir
|
|
||||||
« Prérequis » plus haut), pas le mot de passe normal du compte. Ouvrir le
|
|
||||||
TUI lui-même ne relance pas cette demande automatiquement : un compte au
|
|
||||||
mot de passe refusé porte un ⚠ ; s'il avait déjà synchronisé avec succès,
|
|
||||||
ses dossiers déjà en cache restent visibles et lisibles, ils cessent
|
|
||||||
seulement de se rafraîchir — seul un compte tout neuf (rien de
|
|
||||||
synchronisé encore) n'affiche aucun dossier du tout. Dans tous les cas,
|
|
||||||
passez par « Tester la connexion d'un compte » pour corriger.
|
|
||||||
|
|
||||||
**« le fichier kdbx n'a pas pu être ouvert » en ajoutant un compte.**
|
|
||||||
Le coffre KDBX partagé n'est pas encore configuré, sa fenêtre de sélection
|
|
||||||
de fichier a été annulée, ou le CLI tourne sans affichage pour la montrer.
|
|
||||||
Réglez `kdbx.path` (et `kdbx.password`) comme décrit dans « Ajouter un
|
|
||||||
compte » plus haut, puis réessayez.
|
|
||||||
|
|
||||||
**« le trousseau du système écrirait le mot de passe en clair (backend
|
|
||||||
...) ».**
|
|
||||||
Le backend actif de `keyring` n'est pas de ceux qu'on sait vraiment
|
|
||||||
chiffrer — ça arrive en SSH, dans un conteneur, ou sur une machine sans
|
|
||||||
session graphique, où `keyring` retombe silencieusement sur un fichier en
|
|
||||||
clair. Le client refuse plutôt que de faire semblant que c'est sûr.
|
|
||||||
Utilisez le coffre KDBX à la place (voir plus haut), ou lancez-le là où un
|
|
||||||
vrai trousseau est déverrouillé.
|
|
||||||
|
|
||||||
**« Installez textual pour le client courriel (pip). »**
|
|
||||||
`textual` n'est pas installé. `Courriel > [1] Ouvrir le client courriel
|
|
||||||
(TUI)` affiche seulement ce message et revient au menu ; tout le reste
|
|
||||||
(comptes, synchronisation, cache) fonctionne quand même sans lui.
|
|
||||||
|
|
||||||
**Le cache d'un dossier signale qu'il a changé (`UIDVALIDITY`).**
|
|
||||||
Rien à faire — le client purge et resynchronise ce dossier tout seul à la
|
|
||||||
prochaine synchronisation. La liste des messages se vide puis se remplit
|
|
||||||
brièvement.
|
|
||||||
|
|
||||||
**« cache illisible, purgez-le et resynchronisez : ... ».**
|
|
||||||
Le `cache.db` du compte est corrompu. `Courriel > [4] Cache > [3] Taille
|
|
||||||
du cache et purge` peut lui-même échouer à ouvrir ce même fichier cassé ;
|
|
||||||
le cas échéant, effacez à la main le dossier de cache du compte et
|
|
||||||
resynchronisez :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -rf ~/.erplibre/mail/<account>/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tester contre un vrai serveur
|
|
||||||
|
|
||||||
Presque tous les tests courriel passent par un double en mémoire. Un double ne
|
|
||||||
produit que ce que son auteur avait imaginé — c'est par là que trois bugs de
|
|
||||||
protocole sont arrivés jusqu'aux utilisateurs. D'où un **bac à sable** : un
|
|
||||||
vrai serveur IMAP (Twisted) et un vrai serveur SMTP (aiosmtpd), qu'un test
|
|
||||||
démarre sur un port éphémère de la boucle locale, à qui il parle en vrai TCP,
|
|
||||||
et qu'il tue en terminant — qu'il réussisse ou qu'il échoue.
|
|
||||||
|
|
||||||
Le but n'est pas la conformité. Un serveur poli ne prouve pas grand-chose ;
|
|
||||||
celui-ci sait **se conduire mal exprès**. Un test déclare les octets exacts
|
|
||||||
d'un message — en-tête en 8 bits bruts, charset `unknown-8bit` — et peut
|
|
||||||
couper la connexion ou refuser une commande en pleine synchronisation.
|
|
||||||
Ajouter une nouvelle méchanceté est une petite sous-classe dans
|
|
||||||
`test/mail_sandbox.py`, pas un nouveau serveur.
|
|
||||||
|
|
||||||
Ces tests ne tournent **pas** dans la boucle rapide. Sans `twisted` ni
|
|
||||||
`aiosmtpd`, tout le fichier se saute visiblement. Pour les lancer
|
|
||||||
volontairement :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.venv.erplibre/bin/python -m unittest discover -s test \
|
|
||||||
-p test_mail_live_server.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Ce qu'il ne couvre **pas**, et ne fera pas semblant de couvrir :
|
|
||||||
|
|
||||||
- **`SPECIAL-USE`** — Twisted n'annonce que `IMAP4REV1 NAMESPACE IDLE`. Le bug
|
|
||||||
du message classé sous un nom de dossier deviné plutôt que sous celui
|
|
||||||
annoncé par le serveur reste donc hors de portée. Implémenter l'extension
|
|
||||||
dans le bac à sable ne testerait que notre propre supposition à son sujet —
|
|
||||||
précisément l'erreur que ce bac à sable existe pour éviter.
|
|
||||||
- **Aucune particularité de fournisseur** — les dossiers-étiquettes de Gmail,
|
|
||||||
OAuth chez Microsoft, les mots de passe d'application d'Apple : rien de tout
|
|
||||||
cela n'est exercé. Le bac à sable est un serveur RFC 3501 ordinaire, pas la
|
|
||||||
doublure d'un fournisseur précis.
|
|
||||||
- **Pas de TLS** — le bac à sable parle en clair sur `127.0.0.1`. Les chemins
|
|
||||||
`starttls` et `ssl` ne sont pas exercés ici.
|
|
||||||
- **Rien ne quitte la machine** — aucun hôte externe, aucun trousseau système,
|
|
||||||
aucun `~/.erplibre`, aucun identifiant réel, et jamais un port fixe.
|
|
||||||
|
|
||||||
## Limites de la phase 1
|
|
||||||
|
|
||||||
- **Pas d'OAuth** — Gmail, Outlook et iCloud demandent un mot de passe
|
|
||||||
d'application (voir plus haut) ; OAuth arrive en phase 2.
|
|
||||||
- **Pas de statistiques** — aucun compteur lu/non lu global ni tableau de
|
|
||||||
bord d'activité, au-delà du compte de non-lus par dossier affiché dans
|
|
||||||
l'arbre.
|
|
||||||
- **Pas de recherche côté serveur** — `/` ne filtre que ce qui est déjà
|
|
||||||
synchronisé dans le cache local.
|
|
||||||
- **Pas de file d'attente hors ligne** — l'envoi exige que le compte soit
|
|
||||||
en ligne ; rien ne se met en attente pour partir au retour du réseau.
|
|
||||||
|
|
||||||
Voir le [spec de conception](../docs/superpowers/specs/2026-08-02-email-tui-design.md)
|
|
||||||
pour ce qu'apportent les phases suivantes.
|
|
||||||
318
doc/EMAIL.md
318
doc/EMAIL.md
|
|
@ -1,318 +0,0 @@
|
||||||
|
|
||||||
# Mail client
|
|
||||||
|
|
||||||
A mail client built into the TODO CLI: several accounts, IMAP + SMTP, and a
|
|
||||||
local cache — so you can read and answer email without leaving
|
|
||||||
`./script/todo/todo.py`.
|
|
||||||
|
|
||||||
Every `Mail > ...` path below is shorthand for
|
|
||||||
`TODO > [3] Assistant > [2] Mail - Read and send email > ...` — the full path
|
|
||||||
is spelled out once, in "Adding an account".
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Four Python packages, already listed in `requirement/erplibre_require-ments.txt`
|
|
||||||
(the `.venv.erplibre` environment, not an Odoo venv):
|
|
||||||
|
|
||||||
- `cryptography` — seals the local cache in `encrypted` and `ephemeral` mode.
|
|
||||||
- `keyring` — the system keyring, one of the two places a password can live.
|
|
||||||
- `pykeepass` — the KDBX vault, the other place, and the one the client tries
|
|
||||||
first.
|
|
||||||
- `textual` — the terminal UI itself. Without it, "Open the mail client
|
|
||||||
(TUI)" prints a message and does nothing; the rest of the menu (accounts,
|
|
||||||
sync, cache) still works.
|
|
||||||
|
|
||||||
Install them with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.venv.erplibre/bin/pip install -r requirement/erplibre_require-ments.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### App passwords for Gmail, Outlook and iCloud
|
|
||||||
|
|
||||||
Phase 1 speaks plain IMAP/SMTP login only — no OAuth yet (that is phase 2).
|
|
||||||
Gmail, Outlook and iCloud have all closed that door to the account's real
|
|
||||||
password, so each of these three presets requires an **app password**
|
|
||||||
instead:
|
|
||||||
|
|
||||||
| Provider | Where to generate it |
|
|
||||||
|---|---|
|
|
||||||
| Gmail | Enable 2-step verification, then [myaccount.google.com](https://myaccount.google.com/security) > Security > App passwords |
|
|
||||||
| Outlook / Microsoft 365 | [account.microsoft.com](https://account.microsoft.com/security) > Security > Advanced security options > App passwords |
|
|
||||||
| iCloud | [account.apple.com](https://account.apple.com/) > Sign-In and Security > App-Specific Passwords |
|
|
||||||
|
|
||||||
Use that generated password when account setup asks for one — never the
|
|
||||||
account's normal password. The "Standard server" preset (generic IMAP/SMTP)
|
|
||||||
does not need one.
|
|
||||||
|
|
||||||
## Adding an account
|
|
||||||
|
|
||||||
Menu path: `TODO > [3] Assistant > [2] Mail - Read and send email > [2]
|
|
||||||
Accounts > [2] Add an account`.
|
|
||||||
|
|
||||||
The prompts, in order:
|
|
||||||
|
|
||||||
1. **Short account name** — becomes both the folder name under
|
|
||||||
`~/.erplibre/mail/` and the vault reference, so it cannot contain `/` or
|
|
||||||
start with a dot.
|
|
||||||
2. **Email address**.
|
|
||||||
3. **Display name** (optional) — shown in the `From:` header as
|
|
||||||
`Display Name <email>`.
|
|
||||||
4. **Provider** — a number from the printed list: Gmail, Outlook, iCloud, or
|
|
||||||
"Standard server" (generic IMAP/SMTP).
|
|
||||||
5. If you picked "Standard server", the **IMAP host** and **SMTP host** are
|
|
||||||
asked next; the other presets fill these in for you.
|
|
||||||
6. If the preset requires an app password, its note is printed here as a
|
|
||||||
reminder.
|
|
||||||
7. **Password** — typed hidden (`getpass`), then stored — never written to
|
|
||||||
`accounts.json`.
|
|
||||||
|
|
||||||
Where the password goes: at the password step, the client hands off to the
|
|
||||||
CLI's shared **KDBX manager** — the same one already used for the OpenAI
|
|
||||||
key and Odoo credentials. It reads `kdbx.path` / `kdbx.password` from the
|
|
||||||
TODO config (`script/todo/todo.json`, overridable in
|
|
||||||
`private/todo/todo_override.json` / `private/todo/todo_override_private.json`).
|
|
||||||
If `kdbx.path` isn't set yet, a graphical file picker pops up asking you to
|
|
||||||
choose an existing `.kdbx` file — it needs a display, and cancelling it (or
|
|
||||||
running headless) fails account creation with "le fichier kdbx n'a pas pu
|
|
||||||
être ouvert" (French — see Troubleshooting). **Set `kdbx.path` (and
|
|
||||||
`kdbx.password`, to skip the prompt) before adding your first account**,
|
|
||||||
pointing at a `.kdbx` vault you already have (create one with KeePassXC or
|
|
||||||
similar). The system keyring is only ever used for an account whose
|
|
||||||
`secret_ref` already points at one — the menu itself always writes new
|
|
||||||
accounts into the KDBX vault.
|
|
||||||
|
|
||||||
`accounts.json` (at `~/.erplibre/mail/accounts.json`) only ever holds a
|
|
||||||
`secret_ref` such as `kdbx:ERPLibre/Mail/perso` — a pointer, never the
|
|
||||||
secret. It is safe to read, edit by hand, or check into a private backup.
|
|
||||||
|
|
||||||
## The three cache modes
|
|
||||||
|
|
||||||
Every account keeps a local cache — a small SQLite database plus one file
|
|
||||||
per downloaded message — so the inbox stays readable offline. Three modes
|
|
||||||
control what that cache leaves on disk:
|
|
||||||
|
|
||||||
| Mode | What's on disk | Encryption key |
|
|
||||||
|---|---|---|
|
|
||||||
| `clear` (default) | `~/.erplibre/mail/<account>/cache.db` and `.eml` files, readable as plain text | none |
|
|
||||||
| `encrypted` | same location, but sender, recipients, subject, snippet, message-id and message bodies are sealed with AES-256-GCM | generated once, stored in the vault next to the password (`.../cache-key`) |
|
|
||||||
| `ephemeral` | under `/dev/shm/erplibre-mail-<pid>/<account>/` (or the system temp dir if `/dev/shm` isn't writable), sealed the same way as `encrypted` | generated fresh in RAM at every run, never written anywhere, and the whole directory is removed when the session closes |
|
|
||||||
|
|
||||||
Even in `clear` mode, the technical fields the SQL needs to sort and
|
|
||||||
filter — UID, folder, date, flags, size — are always plain; only the
|
|
||||||
person-identifying fields (and the message body) are ever sealed, and only
|
|
||||||
in `encrypted`/`ephemeral`.
|
|
||||||
|
|
||||||
Set the **general default** at `Mail > [4] Cache > [1] Default cache mode`;
|
|
||||||
it is the `mail_cache_mode` preference (default `clear`). **Override it per
|
|
||||||
account** at `Mail > [4] Cache > [2] Cache mode of one account` — this
|
|
||||||
writes the account's `cache_mode` field in `accounts.json`; leaving it at
|
|
||||||
`null` there means "inherit the general default."
|
|
||||||
|
|
||||||
`Mail > [4] Cache > [3] Cache size and purge` lists every account's
|
|
||||||
effective mode and disk usage, and can erase one account's cache entirely
|
|
||||||
(after confirmation) — the next sync rebuilds it from scratch.
|
|
||||||
|
|
||||||
## The TUI
|
|
||||||
|
|
||||||
`Mail > [1] Open the mail client (TUI)` opens a three-pane screen: an
|
|
||||||
account/folder tree on the left, the message list in the middle, and a
|
|
||||||
preview pane on the right, with a status line at the bottom.
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|---|---|
|
|
||||||
| `↑` `↓` `Tab` | move within a pane / move focus between panes (Textual defaults) |
|
|
||||||
| `h` | open the help window: every shortcut plus a few notes, closed with `Escape` |
|
|
||||||
| `z` | toggle full-screen preview (hides the folder tree and the message list) |
|
|
||||||
| `Escape` | leave full-screen |
|
|
||||||
| `v` | cycle the layout: columns, split, stacked |
|
|
||||||
| `+` / `-` | grow / shrink the pane that has focus |
|
|
||||||
| `0` | back to the default pane sizes |
|
|
||||||
| `r` | sync the account of the currently selected folder (all its folders) |
|
|
||||||
| `Shift+R` | sync every account |
|
|
||||||
| `/` | open the search field (filters the currently visible list only — locally, over subject/from/to/snippet; it does not search the server) |
|
|
||||||
| `s` / `u` | mark the selected message seen / unseen |
|
|
||||||
| `c` | compose a new message |
|
|
||||||
| `a` / `Shift+A` | reply / reply all |
|
|
||||||
| `f` | forward |
|
|
||||||
| `w` | save the message's **first** attachment to `~/Téléchargements` (created if missing) |
|
|
||||||
| `n` | add an account without leaving the client |
|
|
||||||
| `l` | show the tail of `~/.erplibre/mail.log` and this session's sync errors |
|
|
||||||
| `q` | quit |
|
|
||||||
|
|
||||||
This table is written by hand and can fall behind the code; the `h` window
|
|
||||||
cannot. It builds its list from the application's own key bindings every time
|
|
||||||
it opens, so it is the reference if the two ever disagree.
|
|
||||||
|
|
||||||
The bars between the panes can also be dragged with the mouse, and pane sizes
|
|
||||||
are remembered per layout.
|
|
||||||
|
|
||||||
The footer's key hints, like the help window, follow the CLI's chosen
|
|
||||||
language, as do the account tree, the message list and the preview text.
|
|
||||||
|
|
||||||
## Writing a message
|
|
||||||
|
|
||||||
`c` opens the compose form: `To`, `Cc`, `Subject`, an `Attachments` field
|
|
||||||
(semicolon-separated file paths — a comma is legal in a filename, so only
|
|
||||||
`;` splits entries; there is no file picker, type the paths), and a
|
|
||||||
multi-line body. `e` sends the body out to `$EDITOR` (or
|
|
||||||
`nano` if unset) and reads it back; if the editor is missing or exits with
|
|
||||||
an error, the body you had is kept untouched. `Ctrl+S` (or the Send button)
|
|
||||||
delivers the message; `Escape` discards the draft — there is no
|
|
||||||
save-as-draft.
|
|
||||||
|
|
||||||
`a` (reply) and `Shift+A` (reply all) prefill `To`/`Cc`/`Subject`/
|
|
||||||
`In-Reply-To`/`References` and quote the original message in the body. `f`
|
|
||||||
(forward) prefills the `Fwd:` subject and **attaches the original message**
|
|
||||||
automatically, as a `message/rfc822` attachment; the body itself starts
|
|
||||||
empty — write your own note above the attached original.
|
|
||||||
|
|
||||||
Reply, reply-all and forward all need the original message's body
|
|
||||||
available — from the cache, or fetched live if the account is online; with
|
|
||||||
neither, you get "No message selected." / "No message to forward."
|
|
||||||
|
|
||||||
Sending requires the account to be online (composing offline fails with
|
|
||||||
"Account offline: cannot send." — there is no offline outbox). Once sent, a
|
|
||||||
copy is filed into the account's Sent folder over IMAP; if that filing step
|
|
||||||
fails, the status line says so, but the message has already left — it is
|
|
||||||
not resent.
|
|
||||||
|
|
||||||
## Synchronization
|
|
||||||
|
|
||||||
A sync pass is incremental: only UIDs above the last known one are
|
|
||||||
fetched, message bodies are never downloaded during a pass (only headers),
|
|
||||||
and bodies are fetched on demand when you open a message. Flags
|
|
||||||
(read/unread, etc.) of already-known messages are re-checked on every
|
|
||||||
pass, so a message read elsewhere shows up correctly here too.
|
|
||||||
|
|
||||||
Sync happens:
|
|
||||||
|
|
||||||
- **At launch** — opening the TUI kicks off one background sync of every
|
|
||||||
account.
|
|
||||||
- **On demand** — `r` (current account) / `Shift+R` (all accounts) inside
|
|
||||||
the TUI, or `Mail > [3] Synchronise now` from the CLI menu (prints a
|
|
||||||
per-account summary to the terminal).
|
|
||||||
- **Automatically, every `mail_refresh_sec` seconds** (default 300 = 5
|
|
||||||
minutes; 0 disables it) — **but only while the TUI is open**. Close it
|
|
||||||
and the timer goes with it; nothing syncs in the background afterward.
|
|
||||||
|
|
||||||
If the server reports a changed `UIDVALIDITY` for a folder (its UIDs no
|
|
||||||
longer mean what they used to — typically after a server-side migration),
|
|
||||||
that folder's cache is purged and resynced from scratch automatically;
|
|
||||||
there is currently no on-screen notice when this happens beyond the folder
|
|
||||||
briefly emptying and refilling.
|
|
||||||
|
|
||||||
## Where the files live
|
|
||||||
|
|
||||||
| Path | Contents |
|
|
||||||
|---|---|
|
|
||||||
| `~/.erplibre/mail/accounts.json` | account list — servers, presets, cache mode, and a `secret_ref` pointer; never a password (mode 0600) |
|
|
||||||
| `~/.erplibre/mail/<account>/cache.db` | that account's SQLite cache (mode 0600, parent directory 0700) |
|
|
||||||
| `~/.erplibre/mail/<account>/<folder>/<uid>.eml` (or `.eml.enc` when sealed) | one file per downloaded message body |
|
|
||||||
| `/dev/shm/erplibre-mail-<pid>/<account>/` | an `ephemeral` account's cache while the process is alive; removed when it exits (a sweep at every startup also clears directories left behind by a killed process) |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Error messages raised by the mail package itself (`secrets.py`,
|
|
||||||
`store.py`, `crypto.py`, `accounts.py`, `smtp_send.py`,
|
|
||||||
`imap_transport.py`, `imap_sync.py`) now go through the CLI's translation
|
|
||||||
layer, the same as the menu prompts and TUI labels: running the CLI in
|
|
||||||
English shows them in English. The wording below is quoted in French, this
|
|
||||||
document's reference language; expect the matching English wording when
|
|
||||||
`EL_LANG=en`.
|
|
||||||
|
|
||||||
**"Connection failed: ..." when adding or testing an account.**
|
|
||||||
`Mail > [2] Accounts > [5] Test an account connection` prints the server's
|
|
||||||
exact error and then asks for the password again — up to 3 attempts. The
|
|
||||||
password in the vault is only overwritten *after* a successful connection,
|
|
||||||
so a typo never destroys a working password. If the account is Gmail,
|
|
||||||
Outlook or iCloud, check first that you used an app password (see
|
|
||||||
"Prerequisites" above), not the account's normal one. Opening the TUI
|
|
||||||
itself does not retry automatically: an account with a rejected password
|
|
||||||
gets a ⚠ marker; if it had synced successfully before, its already-cached
|
|
||||||
folders stay visible and readable, they just stop refreshing — only a
|
|
||||||
brand-new account (nothing synced yet) shows no folders at all. Either
|
|
||||||
way, go run "Test an account connection" to fix it.
|
|
||||||
|
|
||||||
**"le fichier kdbx n'a pas pu être ouvert" when adding an account.**
|
|
||||||
The shared KDBX vault isn't configured yet, its file picker was cancelled,
|
|
||||||
or the CLI is running without a display to show that picker. Set
|
|
||||||
`kdbx.path` (and `kdbx.password`) as described in "Adding an account"
|
|
||||||
above, then try again.
|
|
||||||
|
|
||||||
**"le trousseau du système écrirait le mot de passe en clair (backend
|
|
||||||
...)".**
|
|
||||||
`keyring`'s active backend isn't one of the ones known to actually
|
|
||||||
encrypt — this happens over SSH, in a container, or on a machine with no
|
|
||||||
desktop session, where `keyring` silently falls back to a plaintext file
|
|
||||||
store. The client refuses rather than pretend that's safe. Use the KDBX
|
|
||||||
vault instead (see above), or run somewhere a real keyring is unlocked.
|
|
||||||
|
|
||||||
**"Install textual for the mail client (pip)."**
|
|
||||||
`textual` isn't installed. `Mail > [1] Open the mail client (TUI)` just
|
|
||||||
prints this and returns; every other menu entry (accounts, sync, cache)
|
|
||||||
still works without it.
|
|
||||||
|
|
||||||
**The folder cache says it changed (`UIDVALIDITY`).**
|
|
||||||
Nothing to do — the client purges and resyncs that folder by itself the
|
|
||||||
next time it syncs. Expect the message list to empty briefly and refill.
|
|
||||||
|
|
||||||
**"cache illisible, purgez-le et resynchronisez : ...".**
|
|
||||||
The account's `cache.db` is corrupt. `Mail > [4] Cache > [3] Cache size and
|
|
||||||
purge` may itself fail to open the same broken file; if so, delete the
|
|
||||||
account's cache directory by hand and resync:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -rf ~/.erplibre/mail/<account>/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing against a real server
|
|
||||||
|
|
||||||
Almost every mail test uses an in-memory double. A double only produces what
|
|
||||||
its author imagined, which is how three protocol bugs reached users. So there
|
|
||||||
is also a **sandbox**: a real IMAP server (Twisted) and a real SMTP server
|
|
||||||
(aiosmtpd) that a test starts on an ephemeral loopback port, talks to over
|
|
||||||
real TCP, and kills when it finishes — pass or fail.
|
|
||||||
|
|
||||||
The point is not conformance. A well-behaved server proves little; this one
|
|
||||||
can **misbehave on purpose**. A test declares the exact bytes a message is
|
|
||||||
made of — raw 8-bit header bytes, an `unknown-8bit` charset — and can drop the
|
|
||||||
connection or refuse a command mid-sync. Adding a new hostile behaviour is a
|
|
||||||
small subclass in `test/mail_sandbox.py`, not a new server.
|
|
||||||
|
|
||||||
These tests do **not** run in the fast loop. Without `twisted` and `aiosmtpd`
|
|
||||||
the whole file skips visibly. Run them deliberately:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.venv.erplibre/bin/python -m unittest discover -s test \
|
|
||||||
-p test_mail_live_server.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
What it does **not** cover, and will not pretend to:
|
|
||||||
|
|
||||||
- **`SPECIAL-USE`** — Twisted announces only `IMAP4REV1 NAMESPACE IDLE`. The
|
|
||||||
bug where a sent message was filed under a guessed folder name instead of
|
|
||||||
the one the server announced is therefore out of reach. Implementing the
|
|
||||||
extension in the sandbox would only test our own assumption about it, which
|
|
||||||
is the exact failure this sandbox exists to escape.
|
|
||||||
- **No provider quirk** — Gmail's label-as-folder model, Microsoft's OAuth,
|
|
||||||
Apple app passwords: none of it is exercised. The sandbox is a plain
|
|
||||||
RFC 3501 server, not a stand-in for a specific provider.
|
|
||||||
- **No TLS** — the sandbox talks in the clear on `127.0.0.1`. `starttls` and
|
|
||||||
`ssl` code paths are not exercised here.
|
|
||||||
- **Nothing leaves the machine** — no external host, no OS keyring, no
|
|
||||||
`~/.erplibre`, no real credentials, and never a fixed port.
|
|
||||||
|
|
||||||
## Phase 1 limits
|
|
||||||
|
|
||||||
- **No OAuth** — Gmail, Outlook and iCloud need an app password (see
|
|
||||||
above); OAuth is phase 2.
|
|
||||||
- **No statistics** — no read/unread counters or activity dashboards beyond
|
|
||||||
the per-folder unseen count shown in the folder tree.
|
|
||||||
- **No server-side search** — `/` filters only what's already synced to the
|
|
||||||
local cache.
|
|
||||||
- **No offline outbox** — sending requires the account to be online; there
|
|
||||||
is no queue that flushes once you're back online.
|
|
||||||
|
|
||||||
See the [design spec](../docs/superpowers/specs/2026-08-02-email-tui-design.md)
|
|
||||||
for what the following phases add.
|
|
||||||
|
|
@ -33,6 +33,25 @@ key : report.url
|
||||||
value : http://127.0.0.1:8069
|
value : http://127.0.0.1:8069
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- [en] -->
|
||||||
|
### wkthmltopdf installation debian 13 and more
|
||||||
|
|
||||||
|
You need libssl1.1, it's deprecated.
|
||||||
|
|
||||||
|
<!-- [fr] -->
|
||||||
|
### Installation de wkthmltopdf sur Debian 13 et plus
|
||||||
|
|
||||||
|
Vous avez besoin de libssl1.1, qui est déprécié.
|
||||||
|
|
||||||
|
<!-- [common] -->
|
||||||
|
```bash
|
||||||
|
echo "deb http://security.debian.org/debian-security bullseye-security main" | sudo tee /etc/apt/sources.list.d/bullseye-security.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libssl1.1
|
||||||
|
sudo rm /etc/apt/sources.list.d/bullseye-security.list
|
||||||
|
sudo apt update
|
||||||
|
```
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
### wkthmltopdf installation alternative
|
### wkthmltopdf installation alternative
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,18 @@ key : report.url
|
||||||
value : http://127.0.0.1:8069
|
value : http://127.0.0.1:8069
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Installation de wkthmltopdf sur Debian 13 et plus
|
||||||
|
|
||||||
|
Vous avez besoin de libssl1.1, qui est déprécié.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "deb http://security.debian.org/debian-security bullseye-security main" | sudo tee /etc/apt/sources.list.d/bullseye-security.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libssl1.1
|
||||||
|
sudo rm /etc/apt/sources.list.d/bullseye-security.list
|
||||||
|
sudo apt update
|
||||||
|
```
|
||||||
|
|
||||||
### Installation alternative de wkthmltopdf
|
### Installation alternative de wkthmltopdf
|
||||||
|
|
||||||
Si vous ne pouvez pas installer wkhtmltopdf, vous pouvez exécuter un Docker et mettre à jour la configuration, consultez le dépôt https://github.com/acsone/kwkhtmltopdf
|
Si vous ne pouvez pas installer wkhtmltopdf, vous pouvez exécuter un Docker et mettre à jour la configuration, consultez le dépôt https://github.com/acsone/kwkhtmltopdf
|
||||||
|
|
|
||||||
12
doc/FAQ.md
12
doc/FAQ.md
|
|
@ -15,6 +15,18 @@ key : report.url
|
||||||
value : http://127.0.0.1:8069
|
value : http://127.0.0.1:8069
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### wkthmltopdf installation debian 13 and more
|
||||||
|
|
||||||
|
You need libssl1.1, it's deprecated.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "deb http://security.debian.org/debian-security bullseye-security main" | sudo tee /etc/apt/sources.list.d/bullseye-security.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libssl1.1
|
||||||
|
sudo rm /etc/apt/sources.list.d/bullseye-security.list
|
||||||
|
sudo apt update
|
||||||
|
```
|
||||||
|
|
||||||
### wkthmltopdf installation alternative
|
### wkthmltopdf installation alternative
|
||||||
|
|
||||||
If you cannot install wkhtmltopdf, you can run a docker and update configuration, check repo https://github.com/acsone/kwkhtmltopdf
|
If you cannot install wkhtmltopdf, you can run a docker and update configuration, check repo https://github.com/acsone/kwkhtmltopdf
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,14 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
A service is running by SystemD. You can access it with the DNS name found in `env_var.sh`
|
A service is running by SystemD. You can access it with the DNS name found in `env_var.sh`
|
||||||
|
|
||||||
#### Ubuntu 24.04 server
|
#### Ubuntu 20.04 server
|
||||||
|
|
||||||
Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh`
|
Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh`
|
||||||
|
|
||||||
#### Serveur Ubuntu 24.04
|
#### Serveur Ubuntu 20.04
|
||||||
|
|
||||||
Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
||||||
|
|
||||||
Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh`
|
Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh`
|
||||||
|
|
||||||
#### Serveur Ubuntu 24.04
|
#### Serveur Ubuntu 20.04
|
||||||
|
|
||||||
Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
||||||
|
|
||||||
A service is running by SystemD. You can access it with the DNS name found in `env_var.sh`
|
A service is running by SystemD. You can access it with the DNS name found in `env_var.sh`
|
||||||
|
|
||||||
#### Ubuntu 24.04 server
|
#### Ubuntu 20.04 server
|
||||||
|
|
||||||
Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ Rechercher l'ancienne version, comme :
|
||||||
|
|
||||||
<!-- [common] -->
|
<!-- [common] -->
|
||||||
```bash
|
```bash
|
||||||
grep --color=always --exclude-dir={.repo,.venv,.git} --exclude="*.svg" -nri v1.8.0
|
grep --color=always --exclude-dir={.repo,.venv,.git} --exclude="*.svg" -nri v1.6.0
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ make docker_build_odoo_18
|
||||||
Rechercher l'ancienne version, comme :
|
Rechercher l'ancienne version, comme :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep --color=always --exclude-dir={.repo,.venv,.git} --exclude="*.svg" -nri v1.8.0
|
grep --color=always --exclude-dir={.repo,.venv,.git} --exclude="*.svg" -nri v1.6.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Remplacer si nécessaire par la nouvelle version.
|
Remplacer si nécessaire par la nouvelle version.
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ make docker_build_odoo_18
|
||||||
Search old version, like :
|
Search old version, like :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep --color=always --exclude-dir={.repo,.venv,.git} --exclude="*.svg" -nri v1.8.0
|
grep --color=always --exclude-dir={.repo,.venv,.git} --exclude="*.svg" -nri v1.6.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace if need it to new version.
|
Replace if need it to new version.
|
||||||
|
|
|
||||||
|
|
@ -37,38 +37,6 @@ Simplement
|
||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
Without any argument, ERPLibre picks the database for you: with a single
|
|
||||||
one it starts on it, with several it shows a numbered menu. It only asks
|
|
||||||
when a real terminal is there — a service started by systemd behaves
|
|
||||||
exactly as before.
|
|
||||||
|
|
||||||
Two options steer that, and neither reaches Odoo:
|
|
||||||
|
|
||||||
| option | effect |
|
|
||||||
|---|---|
|
|
||||||
| `--auto-erplibre` | pick the database even when other arguments are given |
|
|
||||||
| `--no-cli-erplibre` | never show the menu; a lone database is still taken |
|
|
||||||
|
|
||||||
Naming a database yourself — `-d`, `--database` — turns the whole thing
|
|
||||||
off, and so does a `db_name` set in the configuration file.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
Sans aucun argument, ERPLibre choisit la base pour vous : s'il n'y en a
|
|
||||||
qu'une il démarre dessus, s'il y en a plusieurs il affiche un menu
|
|
||||||
numéroté. Il ne pose la question que devant un vrai terminal — un service
|
|
||||||
lancé par systemd se comporte exactement comme avant.
|
|
||||||
|
|
||||||
Deux options le pilotent, et aucune n'arrive jusqu'à Odoo :
|
|
||||||
|
|
||||||
| option | effet |
|
|
||||||
|---|---|
|
|
||||||
| `--auto-erplibre` | choisir la base même quand d'autres arguments sont donnés |
|
|
||||||
| `--no-cli-erplibre` | ne jamais afficher le menu ; une base seule est quand même retenue |
|
|
||||||
|
|
||||||
Nommer soi-même une base — `-d`, `--database` — désactive tout, de même
|
|
||||||
qu'un `db_name` posé dans le fichier de configuration.
|
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
With arguments
|
With arguments
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,6 @@ Simplement
|
||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Sans aucun argument, ERPLibre choisit la base pour vous : s'il n'y en a
|
|
||||||
qu'une il démarre dessus, s'il y en a plusieurs il affiche un menu
|
|
||||||
numéroté. Il ne pose la question que devant un vrai terminal — un service
|
|
||||||
lancé par systemd se comporte exactement comme avant.
|
|
||||||
|
|
||||||
Deux options le pilotent, et aucune n'arrive jusqu'à Odoo :
|
|
||||||
|
|
||||||
| option | effet |
|
|
||||||
|---|---|
|
|
||||||
| `--auto-erplibre` | choisir la base même quand d'autres arguments sont donnés |
|
|
||||||
| `--no-cli-erplibre` | ne jamais afficher le menu ; une base seule est quand même retenue |
|
|
||||||
|
|
||||||
Nommer soi-même une base — `-d`, `--database` — désactive tout, de même
|
|
||||||
qu'un `db_name` posé dans le fichier de configuration.
|
|
||||||
|
|
||||||
Avec des arguments
|
Avec des arguments
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
15
doc/RUN.md
15
doc/RUN.md
|
|
@ -17,21 +17,6 @@ Simply
|
||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Without any argument, ERPLibre picks the database for you: with a single
|
|
||||||
one it starts on it, with several it shows a numbered menu. It only asks
|
|
||||||
when a real terminal is there — a service started by systemd behaves
|
|
||||||
exactly as before.
|
|
||||||
|
|
||||||
Two options steer that, and neither reaches Odoo:
|
|
||||||
|
|
||||||
| option | effect |
|
|
||||||
|---|---|
|
|
||||||
| `--auto-erplibre` | pick the database even when other arguments are given |
|
|
||||||
| `--no-cli-erplibre` | never show the menu; a lone database is still taken |
|
|
||||||
|
|
||||||
Naming a database yourself — `-d`, `--database` — turns the whole thing
|
|
||||||
off, and so does a `db_name` set in the configuration file.
|
|
||||||
|
|
||||||
With arguments
|
With arguments
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,3 @@ TODO: having the DB variable configurable
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
À FAIRE : rendre la variable DB configurable
|
À FAIRE : rendre la variable DB configurable
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
See also: [EMAIL.md](EMAIL.md) — the mail client built into the TODO CLI
|
|
||||||
(`Assistant > Mail`).
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
Voir aussi : [EMAIL.fr.md](EMAIL.fr.md) — le client courriel intégré au CLI
|
|
||||||
TODO (`Assistant > Courriel`).
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,3 @@ Une base de données est installée mais on n'arrive pas à l'exécuter
|
||||||
HEALTHCHECK CMD curl --fail http://localhost:8069/web || exit 1
|
HEALTHCHECK CMD curl --fail http://localhost:8069/web || exit 1
|
||||||
|
|
||||||
À FAIRE : rendre la variable DB configurable
|
À FAIRE : rendre la variable DB configurable
|
||||||
|
|
||||||
Voir aussi : [EMAIL.fr.md](EMAIL.fr.md) — le client courriel intégré au CLI
|
|
||||||
TODO (`Assistant > Courriel`).
|
|
||||||
|
|
@ -12,6 +12,3 @@ A database is installed but cannot be executed
|
||||||
HEALTHCHECK CMD curl --fail http://localhost:8069/web || exit 1
|
HEALTHCHECK CMD curl --fail http://localhost:8069/web || exit 1
|
||||||
|
|
||||||
TODO: having the DB variable configurable
|
TODO: having the DB variable configurable
|
||||||
|
|
||||||
See also: [EMAIL.md](EMAIL.md) — the mail client built into the TODO CLI
|
|
||||||
(`Assistant > Mail`).
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual.
|
A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual.
|
||||||
|
|
||||||
**"WSL2 Ubuntu 24.04" will be referred as "WSL2"**
|
**"WSL2 Ubuntu 22.04" will be referred as "WSL2"**
|
||||||
|
|
||||||
**"PyCharm Professional" will be referred as "PyCharm"**
|
**"PyCharm Professional" will be referred as "PyCharm"**
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ Run Powershell with administrator rights and run the following command:
|
||||||
|
|
||||||
Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle.
|
Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle.
|
||||||
|
|
||||||
**"WSL2 Ubuntu 24.04" sera désigné par "WSL2"**
|
**"WSL2 Ubuntu 22.04" sera désigné par "WSL2"**
|
||||||
|
|
||||||
**"PyCharm Professional" sera désigné par "PyCharm"**
|
**"PyCharm Professional" sera désigné par "PyCharm"**
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ Exécutez Powershell avec les droits administrateur et lancez la commande suivan
|
||||||
|
|
||||||
<!-- [common] -->
|
<!-- [common] -->
|
||||||
```bash
|
```bash
|
||||||
wsl --install -d Ubuntu-24.04
|
wsl --install -d Ubuntu-22.04
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
@ -406,12 +406,12 @@ https://www.jetbrains.com/pycharm/download/#section=windows
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
### Set up Pycharm
|
### Set up Pycharm
|
||||||
|
|
||||||
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-24.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-22.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
### Configurer Pycharm
|
### Configurer Pycharm
|
||||||
|
|
||||||
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-24.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-22.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
||||||
|
|
||||||
<!-- [common] -->
|
<!-- [common] -->
|
||||||

|

|
||||||
|
|
@ -439,10 +439,6 @@ If these last steps to set up your development environment were unsuccessful, fo
|
||||||
|
|
||||||
## Manual Installation
|
## Manual Installation
|
||||||
|
|
||||||
This system interpreter only needs to reach `make` (Python 3.10 or later).
|
|
||||||
`.venv.erplibre` is built on its own version, `conf/python-erplibre-version`,
|
|
||||||
through `EL_PYTHON_PROVIDER` (mise or pyenv).
|
|
||||||
|
|
||||||
### Install Python 3.10.14
|
### Install Python 3.10.14
|
||||||
You can delete the files that are left over in your home directory regarding the python installation when the steps have been completed succesfully.
|
You can delete the files that are left over in your home directory regarding the python installation when the steps have been completed succesfully.
|
||||||
|
|
||||||
|
|
@ -451,10 +447,6 @@ Si ces dernières étapes pour configurer votre environnement de développement
|
||||||
|
|
||||||
## Installation manuelle
|
## Installation manuelle
|
||||||
|
|
||||||
Cet interpréteur système ne sert qu'à atteindre `make` (Python 3.10 ou plus).
|
|
||||||
`.venv.erplibre` est bâti sur sa propre version, `conf/python-erplibre-version`,
|
|
||||||
par `EL_PYTHON_PROVIDER` (mise ou pyenv).
|
|
||||||
|
|
||||||
### Installer Python 3.10.14
|
### Installer Python 3.10.14
|
||||||
Vous pouvez supprimer les fichiers restants dans votre répertoire personnel concernant l'installation de Python une fois les étapes complétées avec succès.
|
Vous pouvez supprimer les fichiers restants dans votre répertoire personnel concernant l'installation de Python une fois les étapes complétées avec succès.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle.
|
Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle.
|
||||||
|
|
||||||
**"WSL2 Ubuntu 24.04" sera désigné par "WSL2"**
|
**"WSL2 Ubuntu 22.04" sera désigné par "WSL2"**
|
||||||
|
|
||||||
**"PyCharm Professional" sera désigné par "PyCharm"**
|
**"PyCharm Professional" sera désigné par "PyCharm"**
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows
|
||||||
Exécutez Powershell avec les droits administrateur et lancez la commande suivante :
|
Exécutez Powershell avec les droits administrateur et lancez la commande suivante :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wsl --install -d Ubuntu-24.04
|
wsl --install -d Ubuntu-22.04
|
||||||
```
|
```
|
||||||
|
|
||||||
Si vous avez des difficultés à ouvrir Powershell avec les droits administrateur, appuyez sur `Windows + R`, entrez la ligne suivante et appuyez sur `OK`. Les droits administrateur vous seront automatiquement demandés.
|
Si vous avez des difficultés à ouvrir Powershell avec les droits administrateur, appuyez sur `Windows + R`, entrez la ligne suivante et appuyez sur `OK`. Les droits administrateur vous seront automatiquement demandés.
|
||||||
|
|
@ -220,7 +220,7 @@ https://www.jetbrains.com/pycharm/download/#section=windows
|
||||||
|
|
||||||
### Configurer Pycharm
|
### Configurer Pycharm
|
||||||
|
|
||||||
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-24.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-22.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -236,10 +236,6 @@ Si ces dernières étapes pour configurer votre environnement de développement
|
||||||
|
|
||||||
## Installation manuelle
|
## Installation manuelle
|
||||||
|
|
||||||
Cet interpréteur système ne sert qu'à atteindre `make` (Python 3.10 ou plus).
|
|
||||||
`.venv.erplibre` est bâti sur sa propre version, `conf/python-erplibre-version`,
|
|
||||||
par `EL_PYTHON_PROVIDER` (mise ou pyenv).
|
|
||||||
|
|
||||||
### Installer Python 3.10.14
|
### Installer Python 3.10.14
|
||||||
Vous pouvez supprimer les fichiers restants dans votre répertoire personnel concernant l'installation de Python une fois les étapes complétées avec succès.
|
Vous pouvez supprimer les fichiers restants dans votre répertoire personnel concernant l'installation de Python une fois les étapes complétées avec succès.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual.
|
A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual.
|
||||||
|
|
||||||
**"WSL2 Ubuntu 24.04" will be referred as "WSL2"**
|
**"WSL2 Ubuntu 22.04" will be referred as "WSL2"**
|
||||||
|
|
||||||
**"PyCharm Professional" will be referred as "PyCharm"**
|
**"PyCharm Professional" will be referred as "PyCharm"**
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004
|
||||||
Run Powershell with administrator rights and run the following command:
|
Run Powershell with administrator rights and run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wsl --install -d Ubuntu-24.04
|
wsl --install -d Ubuntu-22.04
|
||||||
```
|
```
|
||||||
|
|
||||||
If you have trouble opening the Powershell with administrator rights, press `Windows + R`, enter the following line and press `OK`. You will be automatically prompted for administrator rights.
|
If you have trouble opening the Powershell with administrator rights, press `Windows + R`, enter the following line and press `OK`. You will be automatically prompted for administrator rights.
|
||||||
|
|
@ -220,7 +220,7 @@ https://www.jetbrains.com/pycharm/download/#section=windows
|
||||||
|
|
||||||
### Set up Pycharm
|
### Set up Pycharm
|
||||||
|
|
||||||
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-24.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-22.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -236,10 +236,6 @@ If these last steps to set up your development environment were unsuccessful, fo
|
||||||
|
|
||||||
## Manual Installation
|
## Manual Installation
|
||||||
|
|
||||||
This system interpreter only needs to reach `make` (Python 3.10 or later).
|
|
||||||
`.venv.erplibre` is built on its own version, `conf/python-erplibre-version`,
|
|
||||||
through `EL_PYTHON_PROVIDER` (mise or pyenv).
|
|
||||||
|
|
||||||
### Install Python 3.10.14
|
### Install Python 3.10.14
|
||||||
You can delete the files that are left over in your home directory regarding the python installation when the steps have been completed succesfully.
|
You can delete the files that are left over in your home directory regarding the python installation when the steps have been completed succesfully.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,7 @@ ENV ODOO_RC /etc/odoo/odoo.conf
|
||||||
|
|
||||||
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
||||||
|
|
||||||
# « +X » en majuscule n'ajoute le bit que si l'un des trois est DÉJÀ posé,
|
RUN chmod +X /usr/local/bin/wait-for-psql.py
|
||||||
# ou sur un répertoire. Sur le fichier que COPY vient de déposer en 644, il
|
|
||||||
# ne faisait rien : le script restait non exécutable dans l'image, malgré
|
|
||||||
# sa place dans /usr/local/bin.
|
|
||||||
RUN chmod +x /usr/local/bin/wait-for-psql.py
|
|
||||||
|
|
||||||
# Set default user when running the container
|
# Set default user when running the container
|
||||||
USER odoo
|
USER odoo
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,8 @@ RUN cd $ODOO_PREFIX && \
|
||||||
ls -lha /ERPLibre && \
|
ls -lha /ERPLibre && \
|
||||||
python -m venv .venv.$ERPLIBRE_VERSION
|
python -m venv .venv.$ERPLIBRE_VERSION
|
||||||
|
|
||||||
# Le venv des outils prend le python de l'image, celui d'Odoo : le RUN
|
|
||||||
# s'arrete ici si ce python ne sait pas lire la syntaxe de script/.
|
|
||||||
RUN cd $ODOO_PREFIX && \
|
RUN cd $ODOO_PREFIX && \
|
||||||
ls -lha /ERPLibre && \
|
ls -lha /ERPLibre && \
|
||||||
{ python -m compileall -q script/ || \
|
|
||||||
{ echo "Le python de l'image ($(python -V 2>&1)) ne lit pas script/ : prendre une version Odoo plus recente." ; exit 1 ; } ; } && \
|
|
||||||
python -m venv .venv.erplibre && \
|
python -m venv .venv.erplibre && \
|
||||||
source ./.venv.erplibre/bin/activate && \
|
source ./.venv.erplibre/bin/activate && \
|
||||||
pip3 install -r requirement/erplibre_require-ments.txt
|
pip3 install -r requirement/erplibre_require-ments.txt
|
||||||
|
|
|
||||||
|
|
@ -70,21 +70,11 @@ docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg .
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- [en] -->
|
<!-- [en] -->
|
||||||
|
|
||||||
`.venv.erplibre` is built on the image's own Python, Odoo's, without mise or
|
|
||||||
pyenv. The build stops when that Python cannot parse `script/`, which happens
|
|
||||||
with a deprecated Odoo version.
|
|
||||||
|
|
||||||
### Running ERPLibre using Docker-Compose
|
### Running ERPLibre using Docker-Compose
|
||||||
|
|
||||||
Go at the root of this git project.
|
Go at the root of this git project.
|
||||||
|
|
||||||
<!-- [fr] -->
|
<!-- [fr] -->
|
||||||
|
|
||||||
`.venv.erplibre` est bâti sur le Python de l'image, celui d'Odoo, sans mise ni
|
|
||||||
pyenv. La construction s'arrête quand ce Python ne sait pas lire `script/`, ce
|
|
||||||
qui arrive avec une version d'Odoo dépréciée.
|
|
||||||
|
|
||||||
### Exécuter ERPLibre avec Docker-Compose
|
### Exécuter ERPLibre avec Docker-Compose
|
||||||
|
|
||||||
Allez à la racine de ce projet git.
|
Allez à la racine de ce projet git.
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,6 @@ docker build -f Dockerfile.base -t technolibre/erplibre-base:12.0 .
|
||||||
docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg .
|
docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
`.venv.erplibre` est bâti sur le Python de l'image, celui d'Odoo, sans mise ni
|
|
||||||
pyenv. La construction s'arrête quand ce Python ne sait pas lire `script/`, ce
|
|
||||||
qui arrive avec une version d'Odoo dépréciée.
|
|
||||||
|
|
||||||
### Exécuter ERPLibre avec Docker-Compose
|
### Exécuter ERPLibre avec Docker-Compose
|
||||||
|
|
||||||
Allez à la racine de ce projet git.
|
Allez à la racine de ce projet git.
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,6 @@ docker build -f Dockerfile.base -t technolibre/erplibre-base:12.0 .
|
||||||
docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg .
|
docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
`.venv.erplibre` is built on the image's own Python, Odoo's, without mise or
|
|
||||||
pyenv. The build stops when that Python cannot parse `script/`, which happens
|
|
||||||
with a deprecated Odoo version.
|
|
||||||
|
|
||||||
### Running ERPLibre using Docker-Compose
|
### Running ERPLibre using Docker-Compose
|
||||||
|
|
||||||
Go at the root of this git project.
|
Go at the root of this git project.
|
||||||
|
|
|
||||||
44
env_var.sh
44
env_var.sh
|
|
@ -14,17 +14,6 @@ EL_POETRY_VERSION=$(cat ".poetry-version" | xargs)
|
||||||
EL_PYTHON_ODOO_VERSION=$(cat ".python-odoo-version" | xargs)
|
EL_PYTHON_ODOO_VERSION=$(cat ".python-odoo-version" | xargs)
|
||||||
EL_PYTHON_ERPLIBRE_VERSION=$(cat "./conf/python-erplibre-version" | xargs)
|
EL_PYTHON_ERPLIBRE_VERSION=$(cat "./conf/python-erplibre-version" | xargs)
|
||||||
EL_ERPLIBRE_VERSION=$(cat ".erplibre-version" | xargs)
|
EL_ERPLIBRE_VERSION=$(cat ".erplibre-version" | xargs)
|
||||||
# Fournisseur d'interpreteur Python : auto | mise | pyenv.
|
|
||||||
# « auto » prend ce qui est DEJA pose, sinon mise s'il est present (Python
|
|
||||||
# precompile, quelques secondes), sinon pyenv (compilation). Voir
|
|
||||||
# script/install/lib_python_provider.sh.
|
|
||||||
EL_PYTHON_PROVIDER="${EL_PYTHON_PROVIDER:-auto}"
|
|
||||||
# Installateur de paquets Python : auto | uv | pip.
|
|
||||||
# « auto » prend uv s'il est present et si le venv vise est en Python >= 3.8,
|
|
||||||
# sinon pip ; un echec d'uv retombe sur pip. Voir
|
|
||||||
# script/install/lib_pip_provider.sh. Note : « poetry install » n'est PAS
|
|
||||||
# concerne, uv ne lit pas poetry.lock.
|
|
||||||
EL_PIP_PROVIDER="${EL_PIP_PROVIDER:-auto}"
|
|
||||||
# The default port where this Odoo instance will run under (provided you use the command -c in the terminal)
|
# The default port where this Odoo instance will run under (provided you use the command -c in the terminal)
|
||||||
# Set to true if you want to install it, false if you don't need it or have it already installed.
|
# Set to true if you want to install it, false if you don't need it or have it already installed.
|
||||||
EL_INSTALL_WKHTMLTOPDF="True"
|
EL_INSTALL_WKHTMLTOPDF="True"
|
||||||
|
|
@ -48,36 +37,3 @@ EL_LANG="fr"
|
||||||
# 0 = silencieux (défaut), 1 = logs détaillés. On respecte une valeur déjà
|
# 0 = silencieux (défaut), 1 = logs détaillés. On respecte une valeur déjà
|
||||||
# passée en environnement : EL_VERBOSE=1 make install_odoo_18
|
# passée en environnement : EL_VERBOSE=1 make install_odoo_18
|
||||||
EL_VERBOSE="${EL_VERBOSE:-0}"
|
EL_VERBOSE="${EL_VERBOSE:-0}"
|
||||||
|
|
||||||
# NixOS : donner aux roues manylinux les bibliotheques que nix-ld declare.
|
|
||||||
#
|
|
||||||
# nix-ld fournit l'editeur de liens (/lib64/ld-linux-x86-64.so.2) aux binaires
|
|
||||||
# etrangers qu'il LANCE. Mais un « .so » de roue manylinux n'est pas lance :
|
|
||||||
# il est charge par dlopen depuis le CPython du systeme, qui est un binaire de
|
|
||||||
# Nix et ne lit pas NIX_LD_LIBRARY_PATH. Sans cette ligne, l'installation
|
|
||||||
# reussit et « import psycopg2 » echoue APRES, sur « libz.so.1: cannot open
|
|
||||||
# shared object file » -- une erreur qui ne parle ni de pip ni de Nix.
|
|
||||||
#
|
|
||||||
# Rien ailleurs : la variable n'est posee que la ou nix-ld existe, et ne
|
|
||||||
# masque donc aucune bibliotheque sur les autres systemes.
|
|
||||||
if [ -n "${NIX_LD_LIBRARY_PATH:-}" ]; then
|
|
||||||
export LD_LIBRARY_PATH="${NIX_LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# NixOS : les chemins de compilation, relus du SYSTEME DE FICHIERS.
|
|
||||||
#
|
|
||||||
# Le module declare CPATH, LIBRARY_PATH et PKG_CONFIG_PATH, mais une session
|
|
||||||
# les recoit de pam_env a son OUVERTURE. Or l'installation applique le module
|
|
||||||
# (« make install_os ») puis compile (« make install_odoo_18 ») dans la MEME
|
|
||||||
# session : la sienne est plus vieille que ce qu'elle vient de declarer. Les
|
|
||||||
# trois paquets du verrou qui n'ont pas de roue amont s'arretaient alors sur
|
|
||||||
# « lber.h », « cups/http.h » et « mysql.h » -- au PREMIER passage seulement,
|
|
||||||
# ce qui est la pire des pannes : le second reussit et donne raison a tort.
|
|
||||||
#
|
|
||||||
# « /run/current-system/sw » est le profil courant du systeme, un lien que le
|
|
||||||
# rebuild vient de reposer : le lire ne depend d'aucune variable heritee.
|
|
||||||
if [ -d /run/current-system/sw ]; then
|
|
||||||
export CPATH="/run/current-system/sw/include${CPATH:+:${CPATH}}"
|
|
||||||
export LIBRARY_PATH="/run/current-system/sw/lib${LIBRARY_PATH:+:${LIBRARY_PATH}}"
|
|
||||||
export PKG_CONFIG_PATH="/run/current-system/sw/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"
|
|
||||||
fi
|
|
||||||
|
|
|
||||||
93
install.sh
93
install.sh
|
|
@ -1,94 +1,3 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
||||||
#
|
|
||||||
# Point d'entrée de TODO : choisir un interpréteur capable de LIRE le code
|
|
||||||
# avant de le lui donner.
|
|
||||||
#
|
|
||||||
# todo.py porte le hashbang « python3 », donc le Python du système. Celui-ci
|
|
||||||
# est libre d'être plus vieux que conf/python-erplibre-version, qui est la
|
|
||||||
# seule version que le dépôt vise : une syntaxe qu'il ne connaît pas l'arrête
|
|
||||||
# en SyntaxError au chargement, AVANT le garde de todo.py qui aurait su nommer
|
|
||||||
# le geste. Un système hors d'âge n'a alors plus aucun moyen de lancer
|
|
||||||
# l'installation qui le tirerait de là.
|
|
||||||
#
|
|
||||||
# D'où l'ordre : le venv d'outillage, qui porte la bonne version ; sinon le
|
|
||||||
# Python du système s'il est assez récent pour lire le code, et le garde de
|
|
||||||
# todo.py prend le relais ; sinon l'installation, seule issue.
|
|
||||||
|
|
||||||
cd "$(dirname "$0")" || exit 1
|
./script/todo/todo.py
|
||||||
|
|
||||||
VENV="$(xargs < conf/python-erplibre-venv 2> /dev/null)"
|
|
||||||
VOULUE="$(xargs < conf/python-erplibre-version 2> /dev/null)"
|
|
||||||
ATTENDUE="${VOULUE%.*}"
|
|
||||||
|
|
||||||
# Majeure.mineure de l'exécutable : elle seule décide de la grammaire acceptée.
|
|
||||||
# La version est lue par du code exécuté, et non par « -V » : celui-ci répond
|
|
||||||
# avant le chargement de la bibliothèque standard, si bien qu'un interpréteur
|
|
||||||
# qui ne la trouve pas rend sa version puis meurt au premier vrai lancement.
|
|
||||||
# Rien n'est rendu quand la sortie n'a pas la forme X.Y : un interpréteur en
|
|
||||||
# panne écrit son diagnostic, et le premier mot venu passerait sinon pour un
|
|
||||||
# numéro de version que « sort -V » jugerait assez récent.
|
|
||||||
el_mineure() {
|
|
||||||
"$1" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2> /dev/null \
|
|
||||||
| awk '/^[0-9]+\.[0-9]+$/ { print; exit }'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Vrai si la version lue atteint au moins celle attendue, comparée en version
|
|
||||||
# et non en chaîne : « 3.9 » est une chaîne plus grande que « 3.14 ».
|
|
||||||
el_assez_recent() {
|
|
||||||
[ -n "$1" ] && [ -n "${ATTENDUE}" ] \
|
|
||||||
&& [ "$(printf '%s\n%s\n' "${ATTENDUE}" "$1" | sort -V | head -1)" \
|
|
||||||
= "${ATTENDUE}" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Vrai sur une réponse affirmative lue au terminal, en français ou en anglais :
|
|
||||||
# o, oui, y, yes, sans égard à la casse. Le défaut est NON : une frappe sur
|
|
||||||
# Entrée ne lance pas une installation qui peut détruire un venv. Sans
|
|
||||||
# terminal sur l'entrée, rien ne peut consentir, et la réponse est non.
|
|
||||||
el_confirmer() {
|
|
||||||
[ -t 0 ] || return 1
|
|
||||||
printf '%s [o/N] ' "$1"
|
|
||||||
read -r reponse || return 1
|
|
||||||
case "$(printf '%s' "${reponse}" | tr '[:upper:]' '[:lower:]')" in
|
|
||||||
o | oui | y | yes) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
PYTHON_VENV="./${VENV}/bin/python"
|
|
||||||
if [ -n "${VENV}" ] && [ -x "${PYTHON_VENV}" ] \
|
|
||||||
&& el_assez_recent "$(el_mineure "${PYTHON_VENV}")"; then
|
|
||||||
exec "${PYTHON_VENV}" ./script/todo/todo.py "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Un venv dont l'interpréteur ne rend pas sa version ne démarrera pas
|
|
||||||
# davantage TODO : c'est le cas d'un checkout monté depuis une autre machine,
|
|
||||||
# dont le venv cherche sa bibliothèque standard là où elle n'est pas. Le dire,
|
|
||||||
# et couper la relance de todo.py, qui sinon se remplacerait par ce python mort
|
|
||||||
# et rendrait son pavé d'initialisation au lieu d'un message.
|
|
||||||
if [ -x "${PYTHON_VENV}" ] && [ -z "$(el_mineure "${PYTHON_VENV}")" ]; then
|
|
||||||
echo "${PYTHON_VENV} ne demarre pas : ce venv a ete bati ailleurs."
|
|
||||||
echo " Rebatissez-le ici : ./script/install/install_erplibre.sh"
|
|
||||||
export EL_TODO_VENV_RELAUNCHED=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v python3 > /dev/null 2>&1 \
|
|
||||||
&& el_assez_recent "$(el_mineure python3)"; then
|
|
||||||
exec ./script/todo/todo.py "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SYSTEME="$(el_mineure python3)"
|
|
||||||
echo "Python ${VOULUE:-du depot} est requis pour lire le code de TODO ;"
|
|
||||||
echo " ce systeme livre ${SYSTEME:-aucun python3 utilisable}."
|
|
||||||
echo " L'installation du venv d'outillage le pose :"
|
|
||||||
echo " ./script/install/install_erplibre.sh"
|
|
||||||
if [ -d "./${VENV}" ]; then
|
|
||||||
echo " ${VENV} existant sera SUPPRIME puis rebati, avec ce qui y a ete"
|
|
||||||
echo " pose a la main."
|
|
||||||
fi
|
|
||||||
if ! el_confirmer "Lancer l'installation ?"; then
|
|
||||||
echo "Installation non lancee : TODO ne demarre pas."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exec ./script/todo/source_todo.sh "$@"
|
|
||||||
|
|
|
||||||
|
|
@ -1,549 +0,0 @@
|
||||||
<!---------------------------->
|
|
||||||
<!-- multilingual suffix: en, fr -->
|
|
||||||
<!-- no suffix: en -->
|
|
||||||
<!---------------------------->
|
|
||||||
|
|
||||||
<!-- [en] -->
|
|
||||||
# long_test — tests that create real machines
|
|
||||||
|
|
||||||
These are not unit tests. They create virtual machines, install systems on
|
|
||||||
them, and take hours. They live here and **not** in `test/`, which the unit
|
|
||||||
runner sweeps: `./script/test/run_unit_test.sh` must stay runnable in seconds
|
|
||||||
on any machine, including one without virtualisation.
|
|
||||||
|
|
||||||
Run them from the menu — `TODO › Execute › Test › Long tests` — or directly.
|
|
||||||
|
|
||||||
## deep_proxmox.py — how deep does Proxmox-in-Proxmox go?
|
|
||||||
|
|
||||||
The practicable nesting depth cannot be deduced, only measured — and one
|
|
||||||
measurement is not a measurement.
|
|
||||||
|
|
||||||
A manual look at one fourth-level VM found a guest **36 times slower than real
|
|
||||||
time** (583 seconds of wall clock for 16 seconds of guest time, each ACPI line
|
|
||||||
taking a second) and then a frozen kernel: identical RIP across three samples
|
|
||||||
two minutes apart, and **not one byte written** to disk.
|
|
||||||
|
|
||||||
Running this script **refuted the conclusion drawn from it**. Its own
|
|
||||||
fourth-level VM — 2 vCPU where the manual one had 12 — booted, installed, and
|
|
||||||
wrote gigabytes. What looked like a nesting ceiling was a *parallelism*
|
|
||||||
ceiling under nesting. That is exactly what the algorithm caps, and this is
|
|
||||||
how it stopped being a guess.
|
|
||||||
|
|
||||||
Which is the point of the script: a number obtained once, on one machine, in
|
|
||||||
one chain, is an anecdote.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py # three levels, ~30 minutes
|
|
||||||
./long_test/deep_proxmox.py --dry-run # the plan, nothing created
|
|
||||||
./long_test/deep_proxmox.py --depth 5 # ask for more, knowingly
|
|
||||||
./long_test/deep_proxmox.py --detruire # undo it
|
|
||||||
```
|
|
||||||
|
|
||||||
### How deep is worth asking for
|
|
||||||
|
|
||||||
The depth is the only setting, and **three** is the default because three
|
|
||||||
works. Measured on a 28-core machine, one full descent per row:
|
|
||||||
|
|
||||||
| level | boot (ssh) | install | total |
|
|
||||||
|------:|-----------:|--------:|------:|
|
|
||||||
| 1 | 0 s | 200 s | 280 s |
|
|
||||||
| 2 | 37 s | 344 s | 495 s |
|
|
||||||
| 3 | 93 s | 777 s | 1 064 s |
|
|
||||||
| 4 | **15 608 s** | **26 306 s** | did not finish |
|
|
||||||
|
|
||||||
Three levels cost half an hour. The **fourth** cost 4 h 20 of boot and 7 h 18
|
|
||||||
of install on the same machine — everything there is 15 to 30 times slower, not
|
|
||||||
just one step. And it lands exactly where the hardware vendors stop: level 4 is
|
|
||||||
the *third* nested hypervisor, and AMD documents two.
|
|
||||||
|
|
||||||
A wider guest makes it worse, sharply: at level 4, one extra vCPU multiplied
|
|
||||||
the boot by 9.4 (1 664 s at two vCPU, 15 608 s at three), and at eight vCPU the
|
|
||||||
guest read 32 MiB in 106 minutes with a static instruction pointer. At levels 2
|
|
||||||
and 3 that same vCPU costs nothing.
|
|
||||||
|
|
||||||
So: three by default, five if you want to know, ten only to watch the wall.
|
|
||||||
|
|
||||||
The descent is **uniform**. Every level, the first included, goes through the
|
|
||||||
same six steps: create, wait for ssh, install Proxmox, reboot and check the
|
|
||||||
kernel, bring pmxcfs back up, check the storage. Only creation differs —
|
|
||||||
libvirt locally, `qm` afterwards.
|
|
||||||
|
|
||||||
It sends **our** `install_proxmox.sh` over scp instead of letting the VM clone
|
|
||||||
the repository: it is our code we want to exercise, and the remote is often
|
|
||||||
behind the checkout — a fix absent from the remote made the same defect "come
|
|
||||||
back" on three VMs in a row.
|
|
||||||
|
|
||||||
### The resource algorithm — sized from the bottom up
|
|
||||||
|
|
||||||
The first version handed down whatever the parent could spare, and a real
|
|
||||||
descent showed what that costs. Level 4 ended up with 44 GB of memory and
|
|
||||||
2 vCPU **on a host that had 2** — a hundred percent overcommit, at every
|
|
||||||
level, with the hypervisor itself to serve on top. Its install ran past two
|
|
||||||
and a half hours against thirteen minutes for level 3, and extrapolating that
|
|
||||||
ratio gave five years for the tenth.
|
|
||||||
|
|
||||||
So the direction is reversed for **memory and disk**. The deepest level gets
|
|
||||||
what a test Proxmox actually asks for — 4 GB of memory, 25 GB of disk — and
|
|
||||||
every parent above it adds its own overhead and nothing else: 2 GiB and 10 GB.
|
|
||||||
A ten-level descent therefore asks its first level for 22 GB and 115 GB, where
|
|
||||||
handing resources down wanted 50 GB of memory for the same depth. The
|
|
||||||
processor follows a different rule; see below.
|
|
||||||
|
|
||||||
Three budgets can bound the depth, and `script/proxmox/nesting.py` names the
|
|
||||||
one that ran out:
|
|
||||||
|
|
||||||
* **memory** — every level must run its own daemons (`pve-cluster`,
|
|
||||||
`pvestatd`, `pvedaemon`, `pveproxy`) *and* hold its child;
|
|
||||||
* **disk** — the child's disk lives *inside* the parent's, which must also
|
|
||||||
hold its own system;
|
|
||||||
* **processor** — it does *not* grow with depth. Every nested level keeps a
|
|
||||||
fixed, narrow width; only the first level counts against the physical cores.
|
|
||||||
Either the machine can carry that first level or it can carry nothing.
|
|
||||||
|
|
||||||
That third rule is measured, and it cost two descents to get right. A nested
|
|
||||||
guest at the **fourth** level freezes in early boot as soon as it is wide:
|
|
||||||
twelve vCPU the first time, eight the second — same instruction pointer at
|
|
||||||
three readings five minutes apart, 32 MiB read and not one byte more for 106
|
|
||||||
minutes. Two vCPU boots.
|
|
||||||
|
|
||||||
The first freeze was blamed on **overcommit**: that VM had twelve vCPU on a
|
|
||||||
host with two. The second measurement refuted it — eight vCPU on a parent with
|
|
||||||
**nine**, load 1.47, no overcommit at all, and the same freeze. It is the
|
|
||||||
nested guest's vCPU count, not its ratio to its host's.
|
|
||||||
|
|
||||||
At the third level, 9 vCPU boots in 117 s. The threshold sits between the
|
|
||||||
third and fourth level, so no nested level is ever made wide. An earlier
|
|
||||||
version of this algorithm gave each parent one vCPU more than its child, which
|
|
||||||
made level 4 eight wide — exactly the frozen case. The rule made wide what
|
|
||||||
must stay narrow.
|
|
||||||
|
|
||||||
Hence three fixed widths: `VCPU_METAL` for level 1 (on bare metal, no freeze
|
|
||||||
risk — eleven vCPU booted there in 42 s), `VCPU_IMBRIQUE` for the deepest, and
|
|
||||||
`VCPU_INTERMEDIAIRE` in between, wide enough to host its child without being
|
|
||||||
as narrow as it. That middle number is a **hypothesis**: two is proven to boot
|
|
||||||
at the fourth level and eight is proven to freeze, with nothing measured in
|
|
||||||
between. The descent decides.
|
|
||||||
|
|
||||||
Memory is not the lever. On that same manual VM, dropping it from 9 GB to 2 GB
|
|
||||||
moved nothing — it stopped after reading the same 32 MiB, which is simply the
|
|
||||||
size of the boot files.
|
|
||||||
|
|
||||||
The plan is printed **before** anything is created, and the script never
|
|
||||||
promises a depth it knows will not fit — better to announce six levels and
|
|
||||||
reach six than to promise ten and die at the seventh without knowing why.
|
|
||||||
|
|
||||||
## deep_qemu.py — how deep does QEMU-in-QEMU go?
|
|
||||||
|
|
||||||
The same descent, a different stack — and the pair is the point. The fourth
|
|
||||||
level's slowdown comes from the **processor**: what a VM exit costs under
|
|
||||||
nested paging. The per-level *cost*, though, comes from what you install. A
|
|
||||||
Proxmox node lays down a kernel, corosync, ceph and a web UI; a libvirt host
|
|
||||||
lays down `libvirtd` and `qemu-kvm`. Measured together, the two separate what
|
|
||||||
is due to the hardware from what is due to the stack — two things the Proxmox
|
|
||||||
measurement alone confounds.
|
|
||||||
|
|
||||||
### What this test must prove before it measures anything
|
|
||||||
|
|
||||||
`deploy_qemu.py` never passes `--cpu host-passthrough`, and when `/dev/kvm` is
|
|
||||||
missing it does **not** fail: it sets `--virt-type qemu`, warns on one line,
|
|
||||||
and creates a fully **emulated** VM. Seven and a half minutes to boot, and no
|
|
||||||
exit code says so.
|
|
||||||
|
|
||||||
Unguarded, this script would measure stacked TCG while believing it measured
|
|
||||||
nesting — and return a more flattering number that means nothing. So every
|
|
||||||
level must prove, not assume:
|
|
||||||
|
|
||||||
* `/dev/kvm` is readable;
|
|
||||||
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` reads `Y`;
|
|
||||||
* the child's domain is `<domain type='kvm'>`, checked right after creation.
|
|
||||||
|
|
||||||
**What was not read counts as NO.** An absent `/sys/module` file means an
|
|
||||||
unloaded module, not a permissions problem. A level that fails these stops the
|
|
||||||
descent instead of prolonging it into the void.
|
|
||||||
|
|
||||||
## qemu_cache.py — does the download cache really serve the second VM?
|
|
||||||
|
|
||||||
Two sibling VMs, the same distribution, the same packages. The first fills the
|
|
||||||
cache, the second must be served by it.
|
|
||||||
|
|
||||||
**Zero upstream bytes is the headline, not the criterion.** Arch is a rolling
|
|
||||||
release: between the two deployments a mirror can publish a newer version,
|
|
||||||
which the second VM legitimately fetches — the cache never serves an index
|
|
||||||
while upstream answers, so the VM sees it. A criterion built on volume alone
|
|
||||||
would call the cache broken while it works.
|
|
||||||
|
|
||||||
The criterion is therefore: **no URL requested by BOTH VMs is fetched upstream
|
|
||||||
a second time.** What the second VM discovers on its own is counted, shown,
|
|
||||||
and does not fail.
|
|
||||||
|
|
||||||
`--hors-ligne` adds the counter-proof, which is what makes the test worth its
|
|
||||||
hours: it cuts the upstream of the cache SERVICE alone — by its system
|
|
||||||
account, not by a blanket rule that would take down the ssh session running
|
|
||||||
the test — and deploys a third VM, which must build from the stored index.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/qemu_cache.py # two VMs
|
|
||||||
./long_test/qemu_cache.py --dry-run # the plan, nothing created
|
|
||||||
./long_test/qemu_cache.py --hors-ligne # + the third VM, upstream cut
|
|
||||||
./long_test/qemu_cache.py --detruire # undo it
|
|
||||||
```
|
|
||||||
|
|
||||||
It needs the cache installed and running — `TODO › Deployment › QEMU cache` —
|
|
||||||
and it refuses to create anything before saying which prerequisite is missing.
|
|
||||||
Among those prerequisites: the rules must target the subnet libvirt actually
|
|
||||||
serves, which is not always 192.168.122.0/24.
|
|
||||||
|
|
||||||
What governs the duration is the FIRST VM's download, everything else being
|
|
||||||
boot and install: minutes on a machine with nested KVM and a nearby mirror,
|
|
||||||
much longer on a slow link. The second VM does not download at all — that is
|
|
||||||
what is being measured.
|
|
||||||
|
|
||||||
One limit the counter-proof exposes: with upstream cut, the repository
|
|
||||||
database SIGNATURES are missing from the cache, the mirror answering 404 for
|
|
||||||
them, so the cache returns its named 504. pacman treats them as optional and
|
|
||||||
carries on. A distribution that required them would stop there.
|
|
||||||
## install_nixos.py — ERPLibre s'installe-t-il sur NixOS ?
|
|
||||||
|
|
||||||
Not a depth: one machine, one binary question. The other two measure how far
|
|
||||||
nesting goes; this one asks whether the path the menu takes reaches the end on
|
|
||||||
a **declarative** system, where nothing is installed one command at a time.
|
|
||||||
|
|
||||||
It sends the menu's own remote command, `_qemu_erplibre_remote_cmd`, taken as
|
|
||||||
it is. A test that installed by its own means would prove *its* path, not the
|
|
||||||
product's — and that is exactly where the failures hid: a bootstrap with no
|
|
||||||
nix branch, a Makefile assuming `/bin/bash`, compile paths read from a session
|
|
||||||
older than the module it had just applied.
|
|
||||||
|
|
||||||
The block goes in **one** ssh session, as the deployment does. That is the
|
|
||||||
condition that exposes the first-pass failure: the session opens before
|
|
||||||
`make install_os` applies the module, so before `pam_env` sets `CPATH`, and
|
|
||||||
the packages with no upstream wheel stopped there. Replaying the install in a
|
|
||||||
fresh session succeeds and proves the wrong thing.
|
|
||||||
|
|
||||||
The verdict is the **state of the machine**, not a return code:
|
|
||||||
`nixos-rebuild switch` returns 4 on a system that is nonetheless activated,
|
|
||||||
and every tool block of the menu returns 0 by construction. So it checks what
|
|
||||||
`envfs` makes (`/bin/bash`, `/usr/bin/env`, `/usr/bin/python3.x`), the venv,
|
|
||||||
the four modules that have no wheel and must compile — psycopg2, python-ldap,
|
|
||||||
pycups, mysqlclient — the absence of the HTML manuals, and Odoo answering.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/install_nixos.py # create the VM, install, judge
|
|
||||||
./long_test/install_nixos.py --dry-run # the plan and the commands
|
|
||||||
./long_test/install_nixos.py --hote nixos-1 # on a machine you already have
|
|
||||||
./long_test/install_nixos.py --detruire # undo it
|
|
||||||
```
|
|
||||||
|
|
||||||
`--hote` expects a machine that **already runs NixOS**: the script installs
|
|
||||||
ERPLibre there, it does not install the system.
|
|
||||||
|
|
||||||
It clones from the **published** repository, on the branch asked for
|
|
||||||
(`develop` by default). That is deliberate — the test measures what a user
|
|
||||||
receives, not what a local checkout holds. Say it before running: a fix still
|
|
||||||
on an unmerged branch is *not* in the VM, and the test will fail on whatever
|
|
||||||
that fix repairs.
|
|
||||||
|
|
||||||
## Starting from a host you already have
|
|
||||||
|
|
||||||
The three scripts take `--hote`. Creating a head VM to host a hypervisor you
|
|
||||||
already own costs five minutes *and* one level of nesting — that is, slowness,
|
|
||||||
which is the very thing being measured.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py --hote root@10.0.0.5 # an existing Proxmox
|
|
||||||
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # an existing libvirt host
|
|
||||||
```
|
|
||||||
|
|
||||||
Three things follow, and they are not decorative:
|
|
||||||
|
|
||||||
* the plan is sized on the **root**, read over ssh — sizing it on the local
|
|
||||||
machine while the levels live elsewhere would announce levels that do not
|
|
||||||
fit;
|
|
||||||
* the delays count **absolute** depth: a level-1 child placed in a root that
|
|
||||||
is already at the third level is really at the fourth;
|
|
||||||
* the root is **never** a level reached, and **never** destroyed. A borrowed
|
|
||||||
host has no local libvirt UUID, so `--detruire` refuses to fall back on its
|
|
||||||
name — `virsh undefine --remove-all-storage` erases a disk for good.
|
|
||||||
|
|
||||||
The menu offers the host already chosen without searching for it, and undoes
|
|
||||||
each stack separately: they share the report directory, but each knows only
|
|
||||||
its own reports.
|
|
||||||
|
|
||||||
<!-- [fr] -->
|
|
||||||
# long_test — des tests qui créent de vraies machines
|
|
||||||
|
|
||||||
Ce ne sont pas des tests unitaires. Ils créent des machines virtuelles, y
|
|
||||||
installent des systèmes, et durent des heures. Ils vivent ici et **non** dans
|
|
||||||
`test/`, que le lanceur unitaire balaie : `./script/test/run_unit_test.sh`
|
|
||||||
doit rester lançable en quelques secondes, sur n'importe quelle machine, y
|
|
||||||
compris sans virtualisation.
|
|
||||||
|
|
||||||
Ils se lancent depuis le menu — `TODO › Execute › Test › Tests longs` — ou
|
|
||||||
directement.
|
|
||||||
|
|
||||||
## deep_proxmox.py — jusqu'à quel étage un Proxmox dans un Proxmox tient-il ?
|
|
||||||
|
|
||||||
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure — et
|
|
||||||
une mesure n'est pas une mesure.
|
|
||||||
|
|
||||||
Un examen à la main d'UNE VM du quatrième étage a trouvé un invité **36 fois
|
|
||||||
plus lent que le temps réel** (583 secondes d'horloge pour 16 secondes de
|
|
||||||
temps invité, chaque ligne d'ACPI prenant une seconde), puis un noyau gelé :
|
|
||||||
même RIP à trois relevés deux minutes d'écart, et **pas un octet écrit** sur
|
|
||||||
le disque.
|
|
||||||
|
|
||||||
Lancer ce script a **réfuté la conclusion qu'on en avait tirée**. Sa propre VM
|
|
||||||
du quatrième étage — 2 vCPU là où celle de la main en avait 12 — a démarré,
|
|
||||||
s'est installée, et a écrit des gigaoctets. Ce qui ressemblait à un plafond
|
|
||||||
d'imbrication était un plafond de *parallélisme* sous imbrication. C'est
|
|
||||||
précisément ce que l'algorithme borne, et c'est ainsi qu'il a cessé d'être une
|
|
||||||
supposition.
|
|
||||||
|
|
||||||
D'où le script : un chiffre obtenu une fois, sur une machine, dans une chaîne,
|
|
||||||
est une anecdote.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py # trois étages, ~30 minutes
|
|
||||||
./long_test/deep_proxmox.py --dry-run # le plan, rien de créé
|
|
||||||
./long_test/deep_proxmox.py --depth 5 # en demander plus, sciemment
|
|
||||||
./long_test/deep_proxmox.py --detruire # défaire
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quelle profondeur vaut la peine d'être demandée
|
|
||||||
|
|
||||||
La profondeur est le seul réglage, et **trois** est le défaut parce que trois
|
|
||||||
marche. Mesuré sur une machine à 28 cœurs, une descente complète par ligne :
|
|
||||||
|
|
||||||
| étage | amorçage (ssh) | installation | total |
|
|
||||||
|------:|---------------:|-------------:|------:|
|
|
||||||
| 1 | 0 s | 200 s | 280 s |
|
|
||||||
| 2 | 37 s | 344 s | 495 s |
|
|
||||||
| 3 | 93 s | 777 s | 1 064 s |
|
|
||||||
| 4 | **15 608 s** | **26 306 s** | n'a pas abouti |
|
|
||||||
|
|
||||||
Trois étages coûtent une demi-heure. Le **quatrième** a coûté 4 h 20
|
|
||||||
d'amorçage et 7 h 18 d'installation sur la même machine — tout y est 15 à 30
|
|
||||||
fois plus lent, pas une seule étape. Et cela tombe précisément là où les
|
|
||||||
fabricants s'arrêtent : le quatrième étage est le *troisième* hyperviseur
|
|
||||||
imbriqué, et AMD en documente deux.
|
|
||||||
|
|
||||||
Un invité plus large aggrave brutalement : au quatrième étage, un vCPU de plus
|
|
||||||
a multiplié l'amorçage par 9,4 (1 664 s à deux vCPU, 15 608 s à trois), et à
|
|
||||||
huit vCPU l'invité a lu 32 Mio en 106 minutes, pointeur d'instruction
|
|
||||||
immobile. Aux étages 2 et 3, ce même vCPU ne coûte rien.
|
|
||||||
|
|
||||||
Donc : trois par défaut, cinq pour savoir, dix seulement pour voir le mur.
|
|
||||||
|
|
||||||
La descente est **uniforme**. Chaque étage, le premier compris, passe par les
|
|
||||||
mêmes six étapes : créer, attendre le ssh, installer Proxmox, redémarrer et
|
|
||||||
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage. Seule la
|
|
||||||
création diffère — libvirt en local, `qm` ensuite.
|
|
||||||
|
|
||||||
Il envoie **notre** `install_proxmox.sh` par scp au lieu de laisser la VM
|
|
||||||
cloner le dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant
|
|
||||||
est souvent en retard sur le checkout — un correctif absent du distant a fait
|
|
||||||
« revenir » le même défaut sur trois VM de suite.
|
|
||||||
|
|
||||||
### L'algorithme de ressources — dimensionné depuis le bas
|
|
||||||
|
|
||||||
La première version cédait à l'enfant ce que le parent pouvait céder, et une
|
|
||||||
descente réelle a montré ce que cela coûte. L'étage 4 se retrouvait avec 44 Go
|
|
||||||
de mémoire et 2 vCPU **sur un hôte qui en avait 2** — cent pour cent de
|
|
||||||
surengagement, à chaque étage, avec l'hyperviseur lui-même à servir par-dessus.
|
|
||||||
Son installation dépassait deux heures et demie contre treize minutes pour
|
|
||||||
l'étage 3, et l'extrapolation de ce rapport donnait cinq ANS pour le dixième.
|
|
||||||
|
|
||||||
Le sens est donc inversé pour la **mémoire et le disque**. Le plus profond
|
|
||||||
reçoit ce qu'un Proxmox de test demande vraiment — 4 Go de mémoire, 25 Go de
|
|
||||||
disque — et chaque parent au-dessus ajoute son propre surcoût, rien d'autre :
|
|
||||||
2 Gio et 10 Go. Une descente à dix étages demande ainsi 22 Go et 115 Go à son
|
|
||||||
premier étage, là où la cession de haut en bas voulait 50 Go de mémoire pour la
|
|
||||||
même profondeur. Le processeur, lui, suit une autre règle — voir plus bas.
|
|
||||||
|
|
||||||
Trois budgets peuvent borner la profondeur, et `script/proxmox/nesting.py`
|
|
||||||
nomme celui qui a manqué :
|
|
||||||
|
|
||||||
* **la mémoire** — chaque étage doit faire tourner ses propres démons
|
|
||||||
(`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) *et* héberger son
|
|
||||||
enfant ;
|
|
||||||
* **le disque** — le disque de l'enfant vit *dans* celui du parent, qui doit
|
|
||||||
aussi contenir son propre système ;
|
|
||||||
* **le processeur** — il ne croît *pas* avec la profondeur. Tout étage
|
|
||||||
imbriqué garde une largeur fixe et étroite ; seul le premier compte sur les
|
|
||||||
cœurs physiques. Ou la machine peut porter ce premier étage, ou elle ne peut
|
|
||||||
rien.
|
|
||||||
|
|
||||||
Cette troisième règle est mesurée, et il a fallu deux descentes pour la poser
|
|
||||||
juste. Un invité imbriqué au **quatrième** étage gèle en tout début de
|
|
||||||
démarrage dès qu'il est large : douze vCPU la première fois, huit la seconde —
|
|
||||||
même pointeur d'instruction à trois relevés espacés de cinq minutes, 32 Mio lus
|
|
||||||
et plus un octet pendant 106 minutes. Deux vCPU démarrent.
|
|
||||||
|
|
||||||
Le premier gel avait été imputé au **surengagement** : cette VM à douze vCPU
|
|
||||||
tournait sur un hôte qui en avait deux. La seconde mesure l'a réfuté — huit
|
|
||||||
vCPU sur un parent qui en avait **neuf**, charge 1,47, aucun surengagement, et
|
|
||||||
le même gel. C'est le nombre de vCPU de l'invité imbriqué, et non son rapport à
|
|
||||||
celui de son hôte.
|
|
||||||
|
|
||||||
Au troisième étage, 9 vCPU démarrent en 117 s. Le seuil est entre le troisième
|
|
||||||
et le quatrième étage : aucun étage imbriqué n'est donc rendu large. Une version
|
|
||||||
précédente de cet algorithme donnait un vCPU de plus à chaque parent, ce qui
|
|
||||||
rendait l'étage 4 large de huit — exactement le cas gelé. La règle rendait large
|
|
||||||
ce qui doit rester étroit.
|
|
||||||
|
|
||||||
D'où trois largeurs fixes : `VCPU_METAL` pour l'étage 1 (sur le métal, aucun
|
|
||||||
risque de gel — onze vCPU y ont démarré en 42 s), `VCPU_IMBRIQUE` pour le plus
|
|
||||||
profond, et `VCPU_INTERMEDIAIRE` entre les deux, juste assez large pour héberger
|
|
||||||
son enfant sans être aussi étroit que lui. Ce nombre du milieu est une
|
|
||||||
**hypothèse** : deux démarre au quatrième étage, huit gèle, et rien n'est mesuré
|
|
||||||
entre les deux. C'est la descente qui tranche.
|
|
||||||
|
|
||||||
La mémoire n'est pas le levier. Sur cette même VM examinée à la main, la faire
|
|
||||||
passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu les
|
|
||||||
mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage.
|
|
||||||
|
|
||||||
Le plan est affiché **avant** que quoi que ce soit ne soit créé, et le script
|
|
||||||
ne promet jamais une profondeur qu'il sait irréalisable — mieux vaut annoncer
|
|
||||||
six étages et en réussir six que d'en promettre dix et mourir au septième sans
|
|
||||||
savoir pourquoi.
|
|
||||||
|
|
||||||
## deep_qemu.py — jusqu'à quel étage une QEMU dans une QEMU tient-elle ?
|
|
||||||
|
|
||||||
La même descente, une autre pile — et c'est le couple qui compte. Le
|
|
||||||
ralentissement du quatrième étage vient du **processeur** : de ce que coûte une
|
|
||||||
sortie de VM sous pagination imbriquée. Le *coût* par étage, lui, vient de ce
|
|
||||||
qu'on installe. Un nœud Proxmox pose un noyau, corosync, ceph et une interface
|
|
||||||
web ; un hôte libvirt pose `libvirtd` et `qemu-kvm`. Mesurées ensemble, les
|
|
||||||
deux séparent ce qui tient au matériel de ce qui tient à la pile — deux choses
|
|
||||||
que la seule mesure Proxmox confond.
|
|
||||||
|
|
||||||
### Ce que ce test doit prouver avant de mesurer quoi que ce soit
|
|
||||||
|
|
||||||
`deploy_qemu.py` ne passe jamais `--cpu host-passthrough`, et quand
|
|
||||||
`/dev/kvm` manque il n'échoue **pas** : il pose `--virt-type qemu`, avertit sur
|
|
||||||
une ligne, et crée une VM entièrement **émulée**. Sept minutes et demie de
|
|
||||||
démarrage, et aucun code de retour ne le dit.
|
|
||||||
|
|
||||||
Sans garde, ce script mesurerait de la TCG empilée en croyant mesurer de
|
|
||||||
l'imbrication — et rendrait un chiffre plus flatteur qui ne veut rien dire.
|
|
||||||
Chaque étage doit donc prouver, et non supposer :
|
|
||||||
|
|
||||||
* `/dev/kvm` est lisible ;
|
|
||||||
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` vaut `Y` ;
|
|
||||||
* le domaine de l'enfant est `<domain type='kvm'>`, vérifié juste après sa
|
|
||||||
création.
|
|
||||||
|
|
||||||
**Ce qui n'a pas été lu vaut NON.** Un fichier `/sys/module` absent, c'est un
|
|
||||||
module non chargé, pas un problème de permission. Un étage qui échoue à cela
|
|
||||||
arrête la descente au lieu de la prolonger dans le vide.
|
|
||||||
|
|
||||||
## qemu_cache.py — le cache de téléchargement sert-il vraiment la seconde VM ?
|
|
||||||
|
|
||||||
Deux machines sœurs, la même distribution, les mêmes paquets. La première
|
|
||||||
remplit le cache, la seconde doit être servie par lui.
|
|
||||||
|
|
||||||
**« Zéro octet d'amont » est la manchette, pas le critère.** Arch est une
|
|
||||||
publication continue : entre les deux déploiements, un miroir peut publier une
|
|
||||||
version neuve, que la seconde VM tire légitimement — le cache ne sert jamais
|
|
||||||
un index tant que l'amont répond, donc elle la voit. Un critère fondé sur le
|
|
||||||
seul volume déclarerait le cache en panne alors qu'il fonctionne.
|
|
||||||
|
|
||||||
Le critère est donc : **aucune URL demandée par les DEUX VM n'est retirée de
|
|
||||||
l'amont une seconde fois.** Ce que la seconde découvre seule est compté,
|
|
||||||
montré, et n'échoue pas.
|
|
||||||
|
|
||||||
« --hors-ligne » ajoute la contre-épreuve, qui fait la valeur de ces heures :
|
|
||||||
elle coupe l'amont du SEUL service du cache — par son compte système, non par
|
|
||||||
une règle générale qui emporterait la session ssh depuis laquelle le test se
|
|
||||||
lance — et déploie une troisième VM, qui doit se bâtir sur l'index stocké.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/qemu_cache.py # deux VM
|
|
||||||
./long_test/qemu_cache.py --dry-run # le plan, rien de créé
|
|
||||||
./long_test/qemu_cache.py --hors-ligne # + la troisième VM, amont coupé
|
|
||||||
./long_test/qemu_cache.py --detruire # défaire
|
|
||||||
```
|
|
||||||
|
|
||||||
Il exige le cache installé et actif — « TODO › Déploiement › Cache QEMU » — et
|
|
||||||
refuse de rien créer avant d'avoir dit lequel des préalables manque. Parmi
|
|
||||||
eux : les règles doivent viser le sous-réseau que libvirt sert vraiment, qui
|
|
||||||
n'est pas toujours 192.168.122.0/24.
|
|
||||||
|
|
||||||
Ce qui gouverne la durée est le téléchargement de la PREMIÈRE VM, le reste
|
|
||||||
n'étant que démarrage et installation : quelques minutes sur une machine à
|
|
||||||
KVM imbriqué et miroir proche, bien davantage sur une liaison lente. La
|
|
||||||
seconde VM ne télécharge rien — c'est précisément ce qu'on mesure.
|
|
||||||
|
|
||||||
Une limite que la contre-épreuve met au jour : amont coupé, les SIGNATURES
|
|
||||||
des bases de dépôt manquent au cache, le miroir y répondant 404, et le cache
|
|
||||||
rend donc son 504 nommé. pacman les traite comme optionnelles et poursuit.
|
|
||||||
Une distribution qui les exigerait s'arrêterait là.
|
|
||||||
## install_nixos.py — ERPLibre s'installe-t-il sur NixOS ?
|
|
||||||
|
|
||||||
Pas une profondeur : une machine, une question binaire. Les deux autres
|
|
||||||
mesurent jusqu'où l'imbrication tient ; celui-ci demande si le chemin que le
|
|
||||||
menu emprunte aboutit sur un système **déclaratif**, où rien ne s'installe
|
|
||||||
commande par commande.
|
|
||||||
|
|
||||||
Il envoie la commande distante du menu, `_qemu_erplibre_remote_cmd`, prise
|
|
||||||
telle quelle. Un test qui installerait par ses propres soins prouverait *son*
|
|
||||||
chemin, pas celui du produit — et c'est justement là que se cachaient les
|
|
||||||
pannes : un amorçage sans branche nix, un Makefile qui présumait `/bin/bash`,
|
|
||||||
des chemins de compilation lus d'une session plus vieille que le module
|
|
||||||
qu'elle venait d'appliquer.
|
|
||||||
|
|
||||||
Le bloc part en **une** session ssh, comme le déploiement le fait. C'est la
|
|
||||||
condition qui expose la panne du premier passage : la session est ouverte
|
|
||||||
avant que `make install_os` n'applique le module, donc avant que `pam_env` ne
|
|
||||||
pose `CPATH`, et les paquets sans roue amont s'y arrêtaient. Rejouer
|
|
||||||
l'installation dans une session neuve réussit et donne raison à tort.
|
|
||||||
|
|
||||||
Le verdict est l'**état de la machine**, pas un code de retour :
|
|
||||||
`nixos-rebuild switch` rend 4 sur un système pourtant activé, et chaque bloc
|
|
||||||
d'outil du menu rend 0 par construction. On contrôle donc ce que fabrique
|
|
||||||
`envfs` (`/bin/bash`, `/usr/bin/env`, `/usr/bin/python3.x`), le venv, les
|
|
||||||
quatre modules sans roue qui doivent se compiler — psycopg2, python-ldap,
|
|
||||||
pycups, mysqlclient —, l'absence des manuels HTML, et Odoo qui répond.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/install_nixos.py # crée la VM, installe, juge
|
|
||||||
./long_test/install_nixos.py --dry-run # le plan et les commandes
|
|
||||||
./long_test/install_nixos.py --hote nixos-1 # sur une machine qu'on a déjà
|
|
||||||
./long_test/install_nixos.py --detruire # défaire ce qui a été posé
|
|
||||||
```
|
|
||||||
|
|
||||||
`--hote` attend une machine qui porte **déjà** NixOS : le script y installe
|
|
||||||
ERPLibre, il n'y installe pas le système.
|
|
||||||
|
|
||||||
Le clone vient du dépôt **publié**, sur la branche demandée (`develop` par
|
|
||||||
défaut). C'est voulu : le test mesure ce qu'un utilisateur reçoit, pas ce
|
|
||||||
qu'un checkout local contient. À dire avant de lancer : un correctif encore
|
|
||||||
sur une branche non fusionnée n'est *pas* dans la VM, et le test échouera sur
|
|
||||||
ce que ce correctif répare.
|
|
||||||
|
|
||||||
## Partir d'un hôte qu'on possède déjà
|
|
||||||
|
|
||||||
Les trois scripts acceptent `--hote`. Créer une VM de tête pour héberger un
|
|
||||||
hyperviseur qu'on a sous la main coûte cinq minutes *et* un étage
|
|
||||||
d'imbrication — donc de la lenteur, puisque c'est justement elle qu'on mesure.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py --hote root@10.0.0.5 # un Proxmox existant
|
|
||||||
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # un hôte libvirt existant
|
|
||||||
```
|
|
||||||
|
|
||||||
Trois choses en découlent, et elles ne sont pas décoratives :
|
|
||||||
|
|
||||||
* le plan se dimensionne sur la **racine**, lue par ssh — le dimensionner sur
|
|
||||||
la machine locale quand les étages vivent ailleurs annoncerait des étages qui
|
|
||||||
ne tiennent pas ;
|
|
||||||
* les délais comptent la profondeur **absolue** : un enfant de niveau 1 posé
|
|
||||||
dans une racine déjà au troisième étage est en réalité au quatrième ;
|
|
||||||
* la racine n'est **jamais** un étage atteint, et **jamais** détruite. Un hôte
|
|
||||||
emprunté n'a pas d'UUID libvirt local, donc `--detruire` refuse de se rabattre
|
|
||||||
sur son nom — `virsh undefine --remove-all-storage` efface un disque pour de
|
|
||||||
bon.
|
|
||||||
|
|
||||||
Le menu propose l'hôte déjà retenu sans le rechercher, et défait chaque pile
|
|
||||||
séparément : elles partagent le dossier des rapports, mais chacune ne connaît
|
|
||||||
que les siens.
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
|
|
||||||
# long_test — des tests qui créent de vraies machines
|
|
||||||
|
|
||||||
Ce ne sont pas des tests unitaires. Ils créent des machines virtuelles, y
|
|
||||||
installent des systèmes, et durent des heures. Ils vivent ici et **non** dans
|
|
||||||
`test/`, que le lanceur unitaire balaie : `./script/test/run_unit_test.sh`
|
|
||||||
doit rester lançable en quelques secondes, sur n'importe quelle machine, y
|
|
||||||
compris sans virtualisation.
|
|
||||||
|
|
||||||
Ils se lancent depuis le menu — `TODO › Execute › Test › Tests longs` — ou
|
|
||||||
directement.
|
|
||||||
|
|
||||||
## deep_proxmox.py — jusqu'à quel étage un Proxmox dans un Proxmox tient-il ?
|
|
||||||
|
|
||||||
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure — et
|
|
||||||
une mesure n'est pas une mesure.
|
|
||||||
|
|
||||||
Un examen à la main d'UNE VM du quatrième étage a trouvé un invité **36 fois
|
|
||||||
plus lent que le temps réel** (583 secondes d'horloge pour 16 secondes de
|
|
||||||
temps invité, chaque ligne d'ACPI prenant une seconde), puis un noyau gelé :
|
|
||||||
même RIP à trois relevés deux minutes d'écart, et **pas un octet écrit** sur
|
|
||||||
le disque.
|
|
||||||
|
|
||||||
Lancer ce script a **réfuté la conclusion qu'on en avait tirée**. Sa propre VM
|
|
||||||
du quatrième étage — 2 vCPU là où celle de la main en avait 12 — a démarré,
|
|
||||||
s'est installée, et a écrit des gigaoctets. Ce qui ressemblait à un plafond
|
|
||||||
d'imbrication était un plafond de *parallélisme* sous imbrication. C'est
|
|
||||||
précisément ce que l'algorithme borne, et c'est ainsi qu'il a cessé d'être une
|
|
||||||
supposition.
|
|
||||||
|
|
||||||
D'où le script : un chiffre obtenu une fois, sur une machine, dans une chaîne,
|
|
||||||
est une anecdote.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py # trois étages, ~30 minutes
|
|
||||||
./long_test/deep_proxmox.py --dry-run # le plan, rien de créé
|
|
||||||
./long_test/deep_proxmox.py --depth 5 # en demander plus, sciemment
|
|
||||||
./long_test/deep_proxmox.py --detruire # défaire
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quelle profondeur vaut la peine d'être demandée
|
|
||||||
|
|
||||||
La profondeur est le seul réglage, et **trois** est le défaut parce que trois
|
|
||||||
marche. Mesuré sur une machine à 28 cœurs, une descente complète par ligne :
|
|
||||||
|
|
||||||
| étage | amorçage (ssh) | installation | total |
|
|
||||||
|------:|---------------:|-------------:|------:|
|
|
||||||
| 1 | 0 s | 200 s | 280 s |
|
|
||||||
| 2 | 37 s | 344 s | 495 s |
|
|
||||||
| 3 | 93 s | 777 s | 1 064 s |
|
|
||||||
| 4 | **15 608 s** | **26 306 s** | n'a pas abouti |
|
|
||||||
|
|
||||||
Trois étages coûtent une demi-heure. Le **quatrième** a coûté 4 h 20
|
|
||||||
d'amorçage et 7 h 18 d'installation sur la même machine — tout y est 15 à 30
|
|
||||||
fois plus lent, pas une seule étape. Et cela tombe précisément là où les
|
|
||||||
fabricants s'arrêtent : le quatrième étage est le *troisième* hyperviseur
|
|
||||||
imbriqué, et AMD en documente deux.
|
|
||||||
|
|
||||||
Un invité plus large aggrave brutalement : au quatrième étage, un vCPU de plus
|
|
||||||
a multiplié l'amorçage par 9,4 (1 664 s à deux vCPU, 15 608 s à trois), et à
|
|
||||||
huit vCPU l'invité a lu 32 Mio en 106 minutes, pointeur d'instruction
|
|
||||||
immobile. Aux étages 2 et 3, ce même vCPU ne coûte rien.
|
|
||||||
|
|
||||||
Donc : trois par défaut, cinq pour savoir, dix seulement pour voir le mur.
|
|
||||||
|
|
||||||
La descente est **uniforme**. Chaque étage, le premier compris, passe par les
|
|
||||||
mêmes six étapes : créer, attendre le ssh, installer Proxmox, redémarrer et
|
|
||||||
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage. Seule la
|
|
||||||
création diffère — libvirt en local, `qm` ensuite.
|
|
||||||
|
|
||||||
Il envoie **notre** `install_proxmox.sh` par scp au lieu de laisser la VM
|
|
||||||
cloner le dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant
|
|
||||||
est souvent en retard sur le checkout — un correctif absent du distant a fait
|
|
||||||
« revenir » le même défaut sur trois VM de suite.
|
|
||||||
|
|
||||||
### L'algorithme de ressources — dimensionné depuis le bas
|
|
||||||
|
|
||||||
La première version cédait à l'enfant ce que le parent pouvait céder, et une
|
|
||||||
descente réelle a montré ce que cela coûte. L'étage 4 se retrouvait avec 44 Go
|
|
||||||
de mémoire et 2 vCPU **sur un hôte qui en avait 2** — cent pour cent de
|
|
||||||
surengagement, à chaque étage, avec l'hyperviseur lui-même à servir par-dessus.
|
|
||||||
Son installation dépassait deux heures et demie contre treize minutes pour
|
|
||||||
l'étage 3, et l'extrapolation de ce rapport donnait cinq ANS pour le dixième.
|
|
||||||
|
|
||||||
Le sens est donc inversé pour la **mémoire et le disque**. Le plus profond
|
|
||||||
reçoit ce qu'un Proxmox de test demande vraiment — 4 Go de mémoire, 25 Go de
|
|
||||||
disque — et chaque parent au-dessus ajoute son propre surcoût, rien d'autre :
|
|
||||||
2 Gio et 10 Go. Une descente à dix étages demande ainsi 22 Go et 115 Go à son
|
|
||||||
premier étage, là où la cession de haut en bas voulait 50 Go de mémoire pour la
|
|
||||||
même profondeur. Le processeur, lui, suit une autre règle — voir plus bas.
|
|
||||||
|
|
||||||
Trois budgets peuvent borner la profondeur, et `script/proxmox/nesting.py`
|
|
||||||
nomme celui qui a manqué :
|
|
||||||
|
|
||||||
* **la mémoire** — chaque étage doit faire tourner ses propres démons
|
|
||||||
(`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) *et* héberger son
|
|
||||||
enfant ;
|
|
||||||
* **le disque** — le disque de l'enfant vit *dans* celui du parent, qui doit
|
|
||||||
aussi contenir son propre système ;
|
|
||||||
* **le processeur** — il ne croît *pas* avec la profondeur. Tout étage
|
|
||||||
imbriqué garde une largeur fixe et étroite ; seul le premier compte sur les
|
|
||||||
cœurs physiques. Ou la machine peut porter ce premier étage, ou elle ne peut
|
|
||||||
rien.
|
|
||||||
|
|
||||||
Cette troisième règle est mesurée, et il a fallu deux descentes pour la poser
|
|
||||||
juste. Un invité imbriqué au **quatrième** étage gèle en tout début de
|
|
||||||
démarrage dès qu'il est large : douze vCPU la première fois, huit la seconde —
|
|
||||||
même pointeur d'instruction à trois relevés espacés de cinq minutes, 32 Mio lus
|
|
||||||
et plus un octet pendant 106 minutes. Deux vCPU démarrent.
|
|
||||||
|
|
||||||
Le premier gel avait été imputé au **surengagement** : cette VM à douze vCPU
|
|
||||||
tournait sur un hôte qui en avait deux. La seconde mesure l'a réfuté — huit
|
|
||||||
vCPU sur un parent qui en avait **neuf**, charge 1,47, aucun surengagement, et
|
|
||||||
le même gel. C'est le nombre de vCPU de l'invité imbriqué, et non son rapport à
|
|
||||||
celui de son hôte.
|
|
||||||
|
|
||||||
Au troisième étage, 9 vCPU démarrent en 117 s. Le seuil est entre le troisième
|
|
||||||
et le quatrième étage : aucun étage imbriqué n'est donc rendu large. Une version
|
|
||||||
précédente de cet algorithme donnait un vCPU de plus à chaque parent, ce qui
|
|
||||||
rendait l'étage 4 large de huit — exactement le cas gelé. La règle rendait large
|
|
||||||
ce qui doit rester étroit.
|
|
||||||
|
|
||||||
D'où trois largeurs fixes : `VCPU_METAL` pour l'étage 1 (sur le métal, aucun
|
|
||||||
risque de gel — onze vCPU y ont démarré en 42 s), `VCPU_IMBRIQUE` pour le plus
|
|
||||||
profond, et `VCPU_INTERMEDIAIRE` entre les deux, juste assez large pour héberger
|
|
||||||
son enfant sans être aussi étroit que lui. Ce nombre du milieu est une
|
|
||||||
**hypothèse** : deux démarre au quatrième étage, huit gèle, et rien n'est mesuré
|
|
||||||
entre les deux. C'est la descente qui tranche.
|
|
||||||
|
|
||||||
La mémoire n'est pas le levier. Sur cette même VM examinée à la main, la faire
|
|
||||||
passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu les
|
|
||||||
mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage.
|
|
||||||
|
|
||||||
Le plan est affiché **avant** que quoi que ce soit ne soit créé, et le script
|
|
||||||
ne promet jamais une profondeur qu'il sait irréalisable — mieux vaut annoncer
|
|
||||||
six étages et en réussir six que d'en promettre dix et mourir au septième sans
|
|
||||||
savoir pourquoi.
|
|
||||||
|
|
||||||
## deep_qemu.py — jusqu'à quel étage une QEMU dans une QEMU tient-elle ?
|
|
||||||
|
|
||||||
La même descente, une autre pile — et c'est le couple qui compte. Le
|
|
||||||
ralentissement du quatrième étage vient du **processeur** : de ce que coûte une
|
|
||||||
sortie de VM sous pagination imbriquée. Le *coût* par étage, lui, vient de ce
|
|
||||||
qu'on installe. Un nœud Proxmox pose un noyau, corosync, ceph et une interface
|
|
||||||
web ; un hôte libvirt pose `libvirtd` et `qemu-kvm`. Mesurées ensemble, les
|
|
||||||
deux séparent ce qui tient au matériel de ce qui tient à la pile — deux choses
|
|
||||||
que la seule mesure Proxmox confond.
|
|
||||||
|
|
||||||
### Ce que ce test doit prouver avant de mesurer quoi que ce soit
|
|
||||||
|
|
||||||
`deploy_qemu.py` ne passe jamais `--cpu host-passthrough`, et quand
|
|
||||||
`/dev/kvm` manque il n'échoue **pas** : il pose `--virt-type qemu`, avertit sur
|
|
||||||
une ligne, et crée une VM entièrement **émulée**. Sept minutes et demie de
|
|
||||||
démarrage, et aucun code de retour ne le dit.
|
|
||||||
|
|
||||||
Sans garde, ce script mesurerait de la TCG empilée en croyant mesurer de
|
|
||||||
l'imbrication — et rendrait un chiffre plus flatteur qui ne veut rien dire.
|
|
||||||
Chaque étage doit donc prouver, et non supposer :
|
|
||||||
|
|
||||||
* `/dev/kvm` est lisible ;
|
|
||||||
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` vaut `Y` ;
|
|
||||||
* le domaine de l'enfant est `<domain type='kvm'>`, vérifié juste après sa
|
|
||||||
création.
|
|
||||||
|
|
||||||
**Ce qui n'a pas été lu vaut NON.** Un fichier `/sys/module` absent, c'est un
|
|
||||||
module non chargé, pas un problème de permission. Un étage qui échoue à cela
|
|
||||||
arrête la descente au lieu de la prolonger dans le vide.
|
|
||||||
|
|
||||||
## qemu_cache.py — le cache de téléchargement sert-il vraiment la seconde VM ?
|
|
||||||
|
|
||||||
Deux machines sœurs, la même distribution, les mêmes paquets. La première
|
|
||||||
remplit le cache, la seconde doit être servie par lui.
|
|
||||||
|
|
||||||
**« Zéro octet d'amont » est la manchette, pas le critère.** Arch est une
|
|
||||||
publication continue : entre les deux déploiements, un miroir peut publier une
|
|
||||||
version neuve, que la seconde VM tire légitimement — le cache ne sert jamais
|
|
||||||
un index tant que l'amont répond, donc elle la voit. Un critère fondé sur le
|
|
||||||
seul volume déclarerait le cache en panne alors qu'il fonctionne.
|
|
||||||
|
|
||||||
Le critère est donc : **aucune URL demandée par les DEUX VM n'est retirée de
|
|
||||||
l'amont une seconde fois.** Ce que la seconde découvre seule est compté,
|
|
||||||
montré, et n'échoue pas.
|
|
||||||
|
|
||||||
« --hors-ligne » ajoute la contre-épreuve, qui fait la valeur de ces heures :
|
|
||||||
elle coupe l'amont du SEUL service du cache — par son compte système, non par
|
|
||||||
une règle générale qui emporterait la session ssh depuis laquelle le test se
|
|
||||||
lance — et déploie une troisième VM, qui doit se bâtir sur l'index stocké.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/qemu_cache.py # deux VM
|
|
||||||
./long_test/qemu_cache.py --dry-run # le plan, rien de créé
|
|
||||||
./long_test/qemu_cache.py --hors-ligne # + la troisième VM, amont coupé
|
|
||||||
./long_test/qemu_cache.py --detruire # défaire
|
|
||||||
```
|
|
||||||
|
|
||||||
Il exige le cache installé et actif — « TODO › Déploiement › Cache QEMU » — et
|
|
||||||
refuse de rien créer avant d'avoir dit lequel des préalables manque. Parmi
|
|
||||||
eux : les règles doivent viser le sous-réseau que libvirt sert vraiment, qui
|
|
||||||
n'est pas toujours 192.168.122.0/24.
|
|
||||||
|
|
||||||
Ce qui gouverne la durée est le téléchargement de la PREMIÈRE VM, le reste
|
|
||||||
n'étant que démarrage et installation : quelques minutes sur une machine à
|
|
||||||
KVM imbriqué et miroir proche, bien davantage sur une liaison lente. La
|
|
||||||
seconde VM ne télécharge rien — c'est précisément ce qu'on mesure.
|
|
||||||
|
|
||||||
Une limite que la contre-épreuve met au jour : amont coupé, les SIGNATURES
|
|
||||||
des bases de dépôt manquent au cache, le miroir y répondant 404, et le cache
|
|
||||||
rend donc son 504 nommé. pacman les traite comme optionnelles et poursuit.
|
|
||||||
Une distribution qui les exigerait s'arrêterait là.
|
|
||||||
## install_nixos.py — ERPLibre s'installe-t-il sur NixOS ?
|
|
||||||
|
|
||||||
Pas une profondeur : une machine, une question binaire. Les deux autres
|
|
||||||
mesurent jusqu'où l'imbrication tient ; celui-ci demande si le chemin que le
|
|
||||||
menu emprunte aboutit sur un système **déclaratif**, où rien ne s'installe
|
|
||||||
commande par commande.
|
|
||||||
|
|
||||||
Il envoie la commande distante du menu, `_qemu_erplibre_remote_cmd`, prise
|
|
||||||
telle quelle. Un test qui installerait par ses propres soins prouverait *son*
|
|
||||||
chemin, pas celui du produit — et c'est justement là que se cachaient les
|
|
||||||
pannes : un amorçage sans branche nix, un Makefile qui présumait `/bin/bash`,
|
|
||||||
des chemins de compilation lus d'une session plus vieille que le module
|
|
||||||
qu'elle venait d'appliquer.
|
|
||||||
|
|
||||||
Le bloc part en **une** session ssh, comme le déploiement le fait. C'est la
|
|
||||||
condition qui expose la panne du premier passage : la session est ouverte
|
|
||||||
avant que `make install_os` n'applique le module, donc avant que `pam_env` ne
|
|
||||||
pose `CPATH`, et les paquets sans roue amont s'y arrêtaient. Rejouer
|
|
||||||
l'installation dans une session neuve réussit et donne raison à tort.
|
|
||||||
|
|
||||||
Le verdict est l'**état de la machine**, pas un code de retour :
|
|
||||||
`nixos-rebuild switch` rend 4 sur un système pourtant activé, et chaque bloc
|
|
||||||
d'outil du menu rend 0 par construction. On contrôle donc ce que fabrique
|
|
||||||
`envfs` (`/bin/bash`, `/usr/bin/env`, `/usr/bin/python3.x`), le venv, les
|
|
||||||
quatre modules sans roue qui doivent se compiler — psycopg2, python-ldap,
|
|
||||||
pycups, mysqlclient —, l'absence des manuels HTML, et Odoo qui répond.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/install_nixos.py # crée la VM, installe, juge
|
|
||||||
./long_test/install_nixos.py --dry-run # le plan et les commandes
|
|
||||||
./long_test/install_nixos.py --hote nixos-1 # sur une machine qu'on a déjà
|
|
||||||
./long_test/install_nixos.py --detruire # défaire ce qui a été posé
|
|
||||||
```
|
|
||||||
|
|
||||||
`--hote` attend une machine qui porte **déjà** NixOS : le script y installe
|
|
||||||
ERPLibre, il n'y installe pas le système.
|
|
||||||
|
|
||||||
Le clone vient du dépôt **publié**, sur la branche demandée (`develop` par
|
|
||||||
défaut). C'est voulu : le test mesure ce qu'un utilisateur reçoit, pas ce
|
|
||||||
qu'un checkout local contient. À dire avant de lancer : un correctif encore
|
|
||||||
sur une branche non fusionnée n'est *pas* dans la VM, et le test échouera sur
|
|
||||||
ce que ce correctif répare.
|
|
||||||
|
|
||||||
## Partir d'un hôte qu'on possède déjà
|
|
||||||
|
|
||||||
Les trois scripts acceptent `--hote`. Créer une VM de tête pour héberger un
|
|
||||||
hyperviseur qu'on a sous la main coûte cinq minutes *et* un étage
|
|
||||||
d'imbrication — donc de la lenteur, puisque c'est justement elle qu'on mesure.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py --hote root@10.0.0.5 # un Proxmox existant
|
|
||||||
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # un hôte libvirt existant
|
|
||||||
```
|
|
||||||
|
|
||||||
Trois choses en découlent, et elles ne sont pas décoratives :
|
|
||||||
|
|
||||||
* le plan se dimensionne sur la **racine**, lue par ssh — le dimensionner sur
|
|
||||||
la machine locale quand les étages vivent ailleurs annoncerait des étages qui
|
|
||||||
ne tiennent pas ;
|
|
||||||
* les délais comptent la profondeur **absolue** : un enfant de niveau 1 posé
|
|
||||||
dans une racine déjà au troisième étage est en réalité au quatrième ;
|
|
||||||
* la racine n'est **jamais** un étage atteint, et **jamais** détruite. Un hôte
|
|
||||||
emprunté n'a pas d'UUID libvirt local, donc `--detruire` refuse de se rabattre
|
|
||||||
sur son nom — `virsh undefine --remove-all-storage` efface un disque pour de
|
|
||||||
bon.
|
|
||||||
|
|
||||||
Le menu propose l'hôte déjà retenu sans le rechercher, et défait chaque pile
|
|
||||||
séparément : elles partagent le dossier des rapports, mais chacune ne connaît
|
|
||||||
que les siens.
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
|
|
||||||
# long_test — tests that create real machines
|
|
||||||
|
|
||||||
These are not unit tests. They create virtual machines, install systems on
|
|
||||||
them, and take hours. They live here and **not** in `test/`, which the unit
|
|
||||||
runner sweeps: `./script/test/run_unit_test.sh` must stay runnable in seconds
|
|
||||||
on any machine, including one without virtualisation.
|
|
||||||
|
|
||||||
Run them from the menu — `TODO › Execute › Test › Long tests` — or directly.
|
|
||||||
|
|
||||||
## deep_proxmox.py — how deep does Proxmox-in-Proxmox go?
|
|
||||||
|
|
||||||
The practicable nesting depth cannot be deduced, only measured — and one
|
|
||||||
measurement is not a measurement.
|
|
||||||
|
|
||||||
A manual look at one fourth-level VM found a guest **36 times slower than real
|
|
||||||
time** (583 seconds of wall clock for 16 seconds of guest time, each ACPI line
|
|
||||||
taking a second) and then a frozen kernel: identical RIP across three samples
|
|
||||||
two minutes apart, and **not one byte written** to disk.
|
|
||||||
|
|
||||||
Running this script **refuted the conclusion drawn from it**. Its own
|
|
||||||
fourth-level VM — 2 vCPU where the manual one had 12 — booted, installed, and
|
|
||||||
wrote gigabytes. What looked like a nesting ceiling was a *parallelism*
|
|
||||||
ceiling under nesting. That is exactly what the algorithm caps, and this is
|
|
||||||
how it stopped being a guess.
|
|
||||||
|
|
||||||
Which is the point of the script: a number obtained once, on one machine, in
|
|
||||||
one chain, is an anecdote.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py # three levels, ~30 minutes
|
|
||||||
./long_test/deep_proxmox.py --dry-run # the plan, nothing created
|
|
||||||
./long_test/deep_proxmox.py --depth 5 # ask for more, knowingly
|
|
||||||
./long_test/deep_proxmox.py --detruire # undo it
|
|
||||||
```
|
|
||||||
|
|
||||||
### How deep is worth asking for
|
|
||||||
|
|
||||||
The depth is the only setting, and **three** is the default because three
|
|
||||||
works. Measured on a 28-core machine, one full descent per row:
|
|
||||||
|
|
||||||
| level | boot (ssh) | install | total |
|
|
||||||
|------:|-----------:|--------:|------:|
|
|
||||||
| 1 | 0 s | 200 s | 280 s |
|
|
||||||
| 2 | 37 s | 344 s | 495 s |
|
|
||||||
| 3 | 93 s | 777 s | 1 064 s |
|
|
||||||
| 4 | **15 608 s** | **26 306 s** | did not finish |
|
|
||||||
|
|
||||||
Three levels cost half an hour. The **fourth** cost 4 h 20 of boot and 7 h 18
|
|
||||||
of install on the same machine — everything there is 15 to 30 times slower, not
|
|
||||||
just one step. And it lands exactly where the hardware vendors stop: level 4 is
|
|
||||||
the *third* nested hypervisor, and AMD documents two.
|
|
||||||
|
|
||||||
A wider guest makes it worse, sharply: at level 4, one extra vCPU multiplied
|
|
||||||
the boot by 9.4 (1 664 s at two vCPU, 15 608 s at three), and at eight vCPU the
|
|
||||||
guest read 32 MiB in 106 minutes with a static instruction pointer. At levels 2
|
|
||||||
and 3 that same vCPU costs nothing.
|
|
||||||
|
|
||||||
So: three by default, five if you want to know, ten only to watch the wall.
|
|
||||||
|
|
||||||
The descent is **uniform**. Every level, the first included, goes through the
|
|
||||||
same six steps: create, wait for ssh, install Proxmox, reboot and check the
|
|
||||||
kernel, bring pmxcfs back up, check the storage. Only creation differs —
|
|
||||||
libvirt locally, `qm` afterwards.
|
|
||||||
|
|
||||||
It sends **our** `install_proxmox.sh` over scp instead of letting the VM clone
|
|
||||||
the repository: it is our code we want to exercise, and the remote is often
|
|
||||||
behind the checkout — a fix absent from the remote made the same defect "come
|
|
||||||
back" on three VMs in a row.
|
|
||||||
|
|
||||||
### The resource algorithm — sized from the bottom up
|
|
||||||
|
|
||||||
The first version handed down whatever the parent could spare, and a real
|
|
||||||
descent showed what that costs. Level 4 ended up with 44 GB of memory and
|
|
||||||
2 vCPU **on a host that had 2** — a hundred percent overcommit, at every
|
|
||||||
level, with the hypervisor itself to serve on top. Its install ran past two
|
|
||||||
and a half hours against thirteen minutes for level 3, and extrapolating that
|
|
||||||
ratio gave five years for the tenth.
|
|
||||||
|
|
||||||
So the direction is reversed for **memory and disk**. The deepest level gets
|
|
||||||
what a test Proxmox actually asks for — 4 GB of memory, 25 GB of disk — and
|
|
||||||
every parent above it adds its own overhead and nothing else: 2 GiB and 10 GB.
|
|
||||||
A ten-level descent therefore asks its first level for 22 GB and 115 GB, where
|
|
||||||
handing resources down wanted 50 GB of memory for the same depth. The
|
|
||||||
processor follows a different rule; see below.
|
|
||||||
|
|
||||||
Three budgets can bound the depth, and `script/proxmox/nesting.py` names the
|
|
||||||
one that ran out:
|
|
||||||
|
|
||||||
* **memory** — every level must run its own daemons (`pve-cluster`,
|
|
||||||
`pvestatd`, `pvedaemon`, `pveproxy`) *and* hold its child;
|
|
||||||
* **disk** — the child's disk lives *inside* the parent's, which must also
|
|
||||||
hold its own system;
|
|
||||||
* **processor** — it does *not* grow with depth. Every nested level keeps a
|
|
||||||
fixed, narrow width; only the first level counts against the physical cores.
|
|
||||||
Either the machine can carry that first level or it can carry nothing.
|
|
||||||
|
|
||||||
That third rule is measured, and it cost two descents to get right. A nested
|
|
||||||
guest at the **fourth** level freezes in early boot as soon as it is wide:
|
|
||||||
twelve vCPU the first time, eight the second — same instruction pointer at
|
|
||||||
three readings five minutes apart, 32 MiB read and not one byte more for 106
|
|
||||||
minutes. Two vCPU boots.
|
|
||||||
|
|
||||||
The first freeze was blamed on **overcommit**: that VM had twelve vCPU on a
|
|
||||||
host with two. The second measurement refuted it — eight vCPU on a parent with
|
|
||||||
**nine**, load 1.47, no overcommit at all, and the same freeze. It is the
|
|
||||||
nested guest's vCPU count, not its ratio to its host's.
|
|
||||||
|
|
||||||
At the third level, 9 vCPU boots in 117 s. The threshold sits between the
|
|
||||||
third and fourth level, so no nested level is ever made wide. An earlier
|
|
||||||
version of this algorithm gave each parent one vCPU more than its child, which
|
|
||||||
made level 4 eight wide — exactly the frozen case. The rule made wide what
|
|
||||||
must stay narrow.
|
|
||||||
|
|
||||||
Hence three fixed widths: `VCPU_METAL` for level 1 (on bare metal, no freeze
|
|
||||||
risk — eleven vCPU booted there in 42 s), `VCPU_IMBRIQUE` for the deepest, and
|
|
||||||
`VCPU_INTERMEDIAIRE` in between, wide enough to host its child without being
|
|
||||||
as narrow as it. That middle number is a **hypothesis**: two is proven to boot
|
|
||||||
at the fourth level and eight is proven to freeze, with nothing measured in
|
|
||||||
between. The descent decides.
|
|
||||||
|
|
||||||
Memory is not the lever. On that same manual VM, dropping it from 9 GB to 2 GB
|
|
||||||
moved nothing — it stopped after reading the same 32 MiB, which is simply the
|
|
||||||
size of the boot files.
|
|
||||||
|
|
||||||
The plan is printed **before** anything is created, and the script never
|
|
||||||
promises a depth it knows will not fit — better to announce six levels and
|
|
||||||
reach six than to promise ten and die at the seventh without knowing why.
|
|
||||||
|
|
||||||
## deep_qemu.py — how deep does QEMU-in-QEMU go?
|
|
||||||
|
|
||||||
The same descent, a different stack — and the pair is the point. The fourth
|
|
||||||
level's slowdown comes from the **processor**: what a VM exit costs under
|
|
||||||
nested paging. The per-level *cost*, though, comes from what you install. A
|
|
||||||
Proxmox node lays down a kernel, corosync, ceph and a web UI; a libvirt host
|
|
||||||
lays down `libvirtd` and `qemu-kvm`. Measured together, the two separate what
|
|
||||||
is due to the hardware from what is due to the stack — two things the Proxmox
|
|
||||||
measurement alone confounds.
|
|
||||||
|
|
||||||
### What this test must prove before it measures anything
|
|
||||||
|
|
||||||
`deploy_qemu.py` never passes `--cpu host-passthrough`, and when `/dev/kvm` is
|
|
||||||
missing it does **not** fail: it sets `--virt-type qemu`, warns on one line,
|
|
||||||
and creates a fully **emulated** VM. Seven and a half minutes to boot, and no
|
|
||||||
exit code says so.
|
|
||||||
|
|
||||||
Unguarded, this script would measure stacked TCG while believing it measured
|
|
||||||
nesting — and return a more flattering number that means nothing. So every
|
|
||||||
level must prove, not assume:
|
|
||||||
|
|
||||||
* `/dev/kvm` is readable;
|
|
||||||
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` reads `Y`;
|
|
||||||
* the child's domain is `<domain type='kvm'>`, checked right after creation.
|
|
||||||
|
|
||||||
**What was not read counts as NO.** An absent `/sys/module` file means an
|
|
||||||
unloaded module, not a permissions problem. A level that fails these stops the
|
|
||||||
descent instead of prolonging it into the void.
|
|
||||||
|
|
||||||
## qemu_cache.py — does the download cache really serve the second VM?
|
|
||||||
|
|
||||||
Two sibling VMs, the same distribution, the same packages. The first fills the
|
|
||||||
cache, the second must be served by it.
|
|
||||||
|
|
||||||
**Zero upstream bytes is the headline, not the criterion.** Arch is a rolling
|
|
||||||
release: between the two deployments a mirror can publish a newer version,
|
|
||||||
which the second VM legitimately fetches — the cache never serves an index
|
|
||||||
while upstream answers, so the VM sees it. A criterion built on volume alone
|
|
||||||
would call the cache broken while it works.
|
|
||||||
|
|
||||||
The criterion is therefore: **no URL requested by BOTH VMs is fetched upstream
|
|
||||||
a second time.** What the second VM discovers on its own is counted, shown,
|
|
||||||
and does not fail.
|
|
||||||
|
|
||||||
`--hors-ligne` adds the counter-proof, which is what makes the test worth its
|
|
||||||
hours: it cuts the upstream of the cache SERVICE alone — by its system
|
|
||||||
account, not by a blanket rule that would take down the ssh session running
|
|
||||||
the test — and deploys a third VM, which must build from the stored index.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/qemu_cache.py # two VMs
|
|
||||||
./long_test/qemu_cache.py --dry-run # the plan, nothing created
|
|
||||||
./long_test/qemu_cache.py --hors-ligne # + the third VM, upstream cut
|
|
||||||
./long_test/qemu_cache.py --detruire # undo it
|
|
||||||
```
|
|
||||||
|
|
||||||
It needs the cache installed and running — `TODO › Deployment › QEMU cache` —
|
|
||||||
and it refuses to create anything before saying which prerequisite is missing.
|
|
||||||
Among those prerequisites: the rules must target the subnet libvirt actually
|
|
||||||
serves, which is not always 192.168.122.0/24.
|
|
||||||
|
|
||||||
What governs the duration is the FIRST VM's download, everything else being
|
|
||||||
boot and install: minutes on a machine with nested KVM and a nearby mirror,
|
|
||||||
much longer on a slow link. The second VM does not download at all — that is
|
|
||||||
what is being measured.
|
|
||||||
|
|
||||||
One limit the counter-proof exposes: with upstream cut, the repository
|
|
||||||
database SIGNATURES are missing from the cache, the mirror answering 404 for
|
|
||||||
them, so the cache returns its named 504. pacman treats them as optional and
|
|
||||||
carries on. A distribution that required them would stop there.
|
|
||||||
## install_nixos.py — ERPLibre s'installe-t-il sur NixOS ?
|
|
||||||
|
|
||||||
Not a depth: one machine, one binary question. The other two measure how far
|
|
||||||
nesting goes; this one asks whether the path the menu takes reaches the end on
|
|
||||||
a **declarative** system, where nothing is installed one command at a time.
|
|
||||||
|
|
||||||
It sends the menu's own remote command, `_qemu_erplibre_remote_cmd`, taken as
|
|
||||||
it is. A test that installed by its own means would prove *its* path, not the
|
|
||||||
product's — and that is exactly where the failures hid: a bootstrap with no
|
|
||||||
nix branch, a Makefile assuming `/bin/bash`, compile paths read from a session
|
|
||||||
older than the module it had just applied.
|
|
||||||
|
|
||||||
The block goes in **one** ssh session, as the deployment does. That is the
|
|
||||||
condition that exposes the first-pass failure: the session opens before
|
|
||||||
`make install_os` applies the module, so before `pam_env` sets `CPATH`, and
|
|
||||||
the packages with no upstream wheel stopped there. Replaying the install in a
|
|
||||||
fresh session succeeds and proves the wrong thing.
|
|
||||||
|
|
||||||
The verdict is the **state of the machine**, not a return code:
|
|
||||||
`nixos-rebuild switch` returns 4 on a system that is nonetheless activated,
|
|
||||||
and every tool block of the menu returns 0 by construction. So it checks what
|
|
||||||
`envfs` makes (`/bin/bash`, `/usr/bin/env`, `/usr/bin/python3.x`), the venv,
|
|
||||||
the four modules that have no wheel and must compile — psycopg2, python-ldap,
|
|
||||||
pycups, mysqlclient — the absence of the HTML manuals, and Odoo answering.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/install_nixos.py # create the VM, install, judge
|
|
||||||
./long_test/install_nixos.py --dry-run # the plan and the commands
|
|
||||||
./long_test/install_nixos.py --hote nixos-1 # on a machine you already have
|
|
||||||
./long_test/install_nixos.py --detruire # undo it
|
|
||||||
```
|
|
||||||
|
|
||||||
`--hote` expects a machine that **already runs NixOS**: the script installs
|
|
||||||
ERPLibre there, it does not install the system.
|
|
||||||
|
|
||||||
It clones from the **published** repository, on the branch asked for
|
|
||||||
(`develop` by default). That is deliberate — the test measures what a user
|
|
||||||
receives, not what a local checkout holds. Say it before running: a fix still
|
|
||||||
on an unmerged branch is *not* in the VM, and the test will fail on whatever
|
|
||||||
that fix repairs.
|
|
||||||
|
|
||||||
## Starting from a host you already have
|
|
||||||
|
|
||||||
The three scripts take `--hote`. Creating a head VM to host a hypervisor you
|
|
||||||
already own costs five minutes *and* one level of nesting — that is, slowness,
|
|
||||||
which is the very thing being measured.
|
|
||||||
|
|
||||||
```
|
|
||||||
./long_test/deep_proxmox.py --hote root@10.0.0.5 # an existing Proxmox
|
|
||||||
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # an existing libvirt host
|
|
||||||
```
|
|
||||||
|
|
||||||
Three things follow, and they are not decorative:
|
|
||||||
|
|
||||||
* the plan is sized on the **root**, read over ssh — sizing it on the local
|
|
||||||
machine while the levels live elsewhere would announce levels that do not
|
|
||||||
fit;
|
|
||||||
* the delays count **absolute** depth: a level-1 child placed in a root that
|
|
||||||
is already at the third level is really at the fourth;
|
|
||||||
* the root is **never** a level reached, and **never** destroyed. A borrowed
|
|
||||||
host has no local libvirt UUID, so `--detruire` refuses to fall back on its
|
|
||||||
name — `virsh undefine --remove-all-storage` erases a disk for good.
|
|
||||||
|
|
||||||
The menu offers the host already chosen without searching for it, and undoes
|
|
||||||
each stack separately: they share the report directory, but each knows only
|
|
||||||
its own reports.
|
|
||||||
|
|
@ -1,420 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
||||||
"""Jusqu'à quel étage un Proxmox dans un Proxmox tient-il ?
|
|
||||||
|
|
||||||
Ce n'est pas un test unitaire : il crée de vraies machines et prend des
|
|
||||||
HEURES. Il vit donc hors de `test/`, que le lanceur unitaire balaie.
|
|
||||||
|
|
||||||
Ce qu'il établit, et pourquoi cela valait un script : la profondeur
|
|
||||||
d'imbrication praticable ne se déduit pas, elle se mesure. Mesuré ici, sur une
|
|
||||||
machine à 28 cœurs : trois étages coûtent 34 minutes, et le quatrième 4 h 20
|
|
||||||
d'amorçage plus 7 h 18 d'installation. Tout y est 15 à 30 fois plus lent — et
|
|
||||||
c'est là que les fabricants cessent de documenter l'imbrication.
|
|
||||||
|
|
||||||
La descente et ce qu'elle sait sont dans `descente.py`, partagés avec
|
|
||||||
`deep_qemu.py`. Ce fichier-ci n'a que les VERBES de Proxmox : « qm create »
|
|
||||||
chez le parent, install_proxmox.sh, le noyau -pve, pmxcfs debout, un stockage
|
|
||||||
capable d'accueillir l'étage suivant.
|
|
||||||
|
|
||||||
Il envoie NOTRE install_proxmox.sh par scp au lieu de laisser la VM cloner le
|
|
||||||
dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant est souvent
|
|
||||||
en retard sur le checkout — un correctif absent du distant a fait « revenir »
|
|
||||||
le même défaut sur trois VM de suite.
|
|
||||||
|
|
||||||
./long_test/deep_proxmox.py # trois étages, ~34 minutes
|
|
||||||
./long_test/deep_proxmox.py --depth 5 # en demander plus, sciemment
|
|
||||||
./long_test/deep_proxmox.py --dry-run # le plan, rien de créé
|
|
||||||
./long_test/deep_proxmox.py --detruire # défaire ce qui a été posé
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
sys.path.insert(0, RACINE)
|
|
||||||
sys.path.insert(0, os.path.join(RACINE, "long_test"))
|
|
||||||
|
|
||||||
import descente # noqa: E402
|
|
||||||
from descente import ( # noqa: E402,F401
|
|
||||||
DELAIS,
|
|
||||||
Famille,
|
|
||||||
_lance_une_descente,
|
|
||||||
a_defaire,
|
|
||||||
autre_descente,
|
|
||||||
capacite_hote,
|
|
||||||
cle_publique,
|
|
||||||
dernier_rapport,
|
|
||||||
descente_vivante,
|
|
||||||
detruire,
|
|
||||||
detruire_etage1,
|
|
||||||
dire,
|
|
||||||
identite_de,
|
|
||||||
mener,
|
|
||||||
module_qemu,
|
|
||||||
retirer_alias,
|
|
||||||
)
|
|
||||||
from descente import (
|
|
||||||
alias_etage as _alias_etage,
|
|
||||||
)
|
|
||||||
from descente import (
|
|
||||||
nom_etage as _nom_etage,
|
|
||||||
)
|
|
||||||
|
|
||||||
from script.proxmox import proxmox_deploy as pve # noqa: E402
|
|
||||||
|
|
||||||
# L'image des étages imbriqués. Debian parce que install_proxmox.sh s'installe
|
|
||||||
# SUR une Debian — Proxmox ne publie pas d'image cloud.
|
|
||||||
DISTRO = "proxmox"
|
|
||||||
NOM_BASE = "deep-pve"
|
|
||||||
OUTIL = "deep_proxmox"
|
|
||||||
|
|
||||||
|
|
||||||
def nom_etage(niveau):
|
|
||||||
return _nom_etage(niveau, NOM_BASE)
|
|
||||||
|
|
||||||
|
|
||||||
def alias_etage(niveau, parent_alias):
|
|
||||||
return _alias_etage(niveau, parent_alias, NOM_BASE)
|
|
||||||
|
|
||||||
|
|
||||||
class Descente(descente.Descente):
|
|
||||||
"""Les verbes de Proxmox. Le reste est dans `descente.Descente`."""
|
|
||||||
|
|
||||||
OUTIL = OUTIL
|
|
||||||
NOM_BASE = NOM_BASE
|
|
||||||
DISTRO = DISTRO
|
|
||||||
|
|
||||||
def noyau_convient(self, noyau):
|
|
||||||
"""Le noyau Proxmox, et pas celui de Debian.
|
|
||||||
|
|
||||||
Sans lui la machine reste sur le noyau cloud, dépouillé de tout
|
|
||||||
netfilter : ni pont NAT, ni invité.
|
|
||||||
"""
|
|
||||||
return "-pve" in noyau
|
|
||||||
|
|
||||||
def installer(self, hote):
|
|
||||||
"""Envoie NOTRE script et l'exécute. Rend True si Proxmox est posé."""
|
|
||||||
local = os.path.join(RACINE, "script/proxmox/install_proxmox.sh")
|
|
||||||
distant = "/tmp/install_proxmox.sh"
|
|
||||||
if self.dry_run:
|
|
||||||
print(f" scp {local} <hôte>:{distant}")
|
|
||||||
print(f" bash {distant}")
|
|
||||||
return True
|
|
||||||
argv = pve.ssh_argv(hote, "")[:-1] # les options, sans la commande
|
|
||||||
cible = argv[-1]
|
|
||||||
options = argv[1:-1]
|
|
||||||
res = subprocess.run(
|
|
||||||
["scp", "-q"] + options + [local, f"{cible}:{distant}"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=300,
|
|
||||||
)
|
|
||||||
if res.returncode:
|
|
||||||
self.dire(f" ✗ scp : {res.stderr.strip()[:200]}")
|
|
||||||
return False
|
|
||||||
# « bash » et non « sh » : le script porte « set -euo pipefail » et un
|
|
||||||
# shebang bash. Sur Debian /bin/sh est dash, qui répond « set: Illegal
|
|
||||||
# option -o pipefail » et sort à la PREMIÈRE ligne — vérifié. Chaque
|
|
||||||
# étage aurait échoué sur l'installation, à tous les coups.
|
|
||||||
code, _o = self.executer(
|
|
||||||
dict(hote, sudo=""),
|
|
||||||
f"bash {distant}",
|
|
||||||
self.delai("install"),
|
|
||||||
"install_proxmox.sh",
|
|
||||||
montrer=True,
|
|
||||||
)
|
|
||||||
return code == 0
|
|
||||||
|
|
||||||
def preparer_parent(self, parent):
|
|
||||||
"""Stockage, pont et réseau interne du parent, ou None.
|
|
||||||
|
|
||||||
Les codes de retour des LECTURES sont regardés, et c'est tout le
|
|
||||||
sujet ici. Ailleurs dans ce dépôt un code de retour ne prouve rien —
|
|
||||||
celui d'une commande distante composée est celui du dernier maillon.
|
|
||||||
Mais pour une lecture, il est la SEULE chose qui distingue « j'ai lu,
|
|
||||||
il n'y a rien » de « je n'ai pas pu lire ».
|
|
||||||
|
|
||||||
La différence n'est pas académique : de l'absence de pont on
|
|
||||||
RECONFIGURE le réseau du parent. Un « ip link show » qui échoue — un
|
|
||||||
hoquet ssh, un sudo pas encore prêt — se lisait « pas de pont », et on
|
|
||||||
posait un pont et un NAT sur une machine qui en avait déjà un.
|
|
||||||
"""
|
|
||||||
code, out = self.executer(
|
|
||||||
parent,
|
|
||||||
"pvesm status --content images",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"pvesm",
|
|
||||||
)
|
|
||||||
if code and not self.dry_run:
|
|
||||||
self.dire(" ✗ « pvesm status » a échoué : rien conclu")
|
|
||||||
return None
|
|
||||||
stockage = pve.pick_storage(pve.parse_storages(out))
|
|
||||||
if not stockage and not self.dry_run:
|
|
||||||
self.dire(" ✗ aucun stockage sur le parent")
|
|
||||||
return None
|
|
||||||
code, out = self.executer(
|
|
||||||
parent,
|
|
||||||
"ip -o link show type bridge",
|
|
||||||
self.delai("controle"),
|
|
||||||
"ponts",
|
|
||||||
)
|
|
||||||
if code and not self.dry_run:
|
|
||||||
self.dire(
|
|
||||||
" ✗ liste des ponts illisible : on ne touche PAS au"
|
|
||||||
" réseau du parent"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
ponts = pve.parse_bridges(out)
|
|
||||||
if not ponts:
|
|
||||||
_c, nets = self.executer(
|
|
||||||
parent, pve.USED_NETS_CMD, self.delai("controle"), "réseaux"
|
|
||||||
)
|
|
||||||
cidr = pve.pick_internal_cidr(nets) or pve.INTERNAL_CIDR
|
|
||||||
_c, rt = self.executer(
|
|
||||||
parent,
|
|
||||||
"ip -o -4 route show default",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"uplink",
|
|
||||||
)
|
|
||||||
trouve = re.search(r"dev\s+(\S+)", rt or "")
|
|
||||||
uplink = trouve.group(1) if trouve else ""
|
|
||||||
self.dire(f" pont {cidr}, NAT par {uplink or '—'}")
|
|
||||||
for cmd in pve.bridge_setup_cmds(cidr=cidr, uplink=uplink):
|
|
||||||
code, _o = self.executer(
|
|
||||||
parent, cmd, DELAIS["reparation"], "pont"
|
|
||||||
)
|
|
||||||
if code and not self.dry_run:
|
|
||||||
return None
|
|
||||||
ponts = [pve.INTERNAL_BRIDGE]
|
|
||||||
_c, cfg = self.executer(
|
|
||||||
parent,
|
|
||||||
"cat /etc/network/interfaces",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"interfaces",
|
|
||||||
)
|
|
||||||
# Le DNS de l'hôte. « --ipconfig0 » ne le porte PAS : une VM en
|
|
||||||
# adresse fixe route mais ne résout rien, et install_proxmox.sh meurt
|
|
||||||
# sur « apt update » sans que rien ne l'explique. Le rapport imputerait
|
|
||||||
# à l'installation ce qui est un défaut de résolveur.
|
|
||||||
_c, resolv = self.executer(
|
|
||||||
parent, pve.RESOLV_CMD, self.delai("controle"), "resolv"
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
stockage or "local",
|
|
||||||
ponts[0],
|
|
||||||
pve.parse_bridge_config(cfg).get(ponts[0], {}),
|
|
||||||
pve.parse_nameservers(resolv),
|
|
||||||
)
|
|
||||||
|
|
||||||
def creer_enfant(self, parent, niveau, res, prepare, noter=None):
|
|
||||||
"""« qm create » sur le parent. Rend (vmid, adresse) ou (None, None).
|
|
||||||
|
|
||||||
`noter` reçoit le VMID AVANT la première commande qui peut créer la
|
|
||||||
VM. Sans lui, le VMID ne remontait qu'au RETOUR : une création qui
|
|
||||||
échouait à la quatrième de ses six commandes — « qm resize » sur un
|
|
||||||
stockage plein, par exemple — laissait une VM allumée et un disque
|
|
||||||
alloué que le rapport ne nommait nulle part, donc que « --detruire »
|
|
||||||
ne pouvait pas défaire.
|
|
||||||
"""
|
|
||||||
stockage, pont, info_pont, dns = prepare
|
|
||||||
mod = module_qemu()
|
|
||||||
version = mod.DISTROS[DISTRO][1]
|
|
||||||
code_img = mod.DISTROS[DISTRO][0][version][0]
|
|
||||||
url = mod.image_url(DISTRO, code_img, "amd64", version)
|
|
||||||
image = mod.default_image_name(DISTRO, code_img, "amd64", version)
|
|
||||||
_c, out = self.executer(
|
|
||||||
parent, "qm list", self.delai("controle"), "qm list"
|
|
||||||
)
|
|
||||||
vmid = pve.next_vmid(pve.parse_qm_list(out))
|
|
||||||
ipconfig = pve.ipconfig_for(info_pont, vmid)
|
|
||||||
adresse = pve.ip_from_ipconfig(ipconfig)
|
|
||||||
# AVANT de télécharger l'image et de démarrer quoi que ce soit : sur un
|
|
||||||
# pont relié au LAN, ipconfig_for rend « ip=dhcp » et l'adresse est
|
|
||||||
# vide. Le contrôle venait après la création : on laissait une VM
|
|
||||||
# allumée, un disque alloué, et une machine que --detruire ne
|
|
||||||
# connaissait pas.
|
|
||||||
if not adresse and self.dry_run:
|
|
||||||
# En essai à blanc on n'a rien lu du parent : conclure « pas de
|
|
||||||
# pont interne » serait une affirmation tirée d'une mesure qui
|
|
||||||
# n'a pas eu lieu. On prend une adresse plausible pour dérouler
|
|
||||||
# le plan jusqu'au bout.
|
|
||||||
adresse = "10.10.10.150"
|
|
||||||
ipconfig = f"ip={adresse}/24,gw=10.10.10.1"
|
|
||||||
if not adresse:
|
|
||||||
self.dire(" ✗ pas d'adresse fixe : le parent n'a pas de")
|
|
||||||
self.dire(" pont interne, et l'enfant serait injoignable")
|
|
||||||
return None, None
|
|
||||||
spec = {
|
|
||||||
"name": nom_etage(niveau),
|
|
||||||
"storage": stockage,
|
|
||||||
"image": image,
|
|
||||||
"memory": res["ram"],
|
|
||||||
"vcpus": res["vcpu"],
|
|
||||||
"bridge": pont,
|
|
||||||
"disk": f"{res['disque']}G",
|
|
||||||
"user": "erplibre",
|
|
||||||
"ipconfig": ipconfig,
|
|
||||||
"sshkey_path": "/root/.ssh/longtest.pub",
|
|
||||||
"nameservers": dns,
|
|
||||||
"start": True,
|
|
||||||
# Du CATALOGUE, jamais en dur : create_cmds lit cette clé, et un
|
|
||||||
# spec qui l'omet vaut SeaBIOS en silence — sur une image sans
|
|
||||||
# secteur d'amorçage BIOS, une VM « running » à la console muette.
|
|
||||||
"uefi": mod.requiert_uefi(DISTRO),
|
|
||||||
}
|
|
||||||
# La clé publique doit être un FICHIER sur le parent : « --sshkeys »
|
|
||||||
# n'accepte pas la clé en ligne.
|
|
||||||
pub = cle_publique()
|
|
||||||
if pub and not self.dry_run:
|
|
||||||
with open(pub, encoding="utf-8") as fh:
|
|
||||||
contenu = fh.read().strip()
|
|
||||||
self.executer(
|
|
||||||
parent,
|
|
||||||
f"mkdir -p /root/.ssh && printf '%s\\n'"
|
|
||||||
f" {shlex.quote(contenu)} > /root/.ssh/longtest.pub",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"clé",
|
|
||||||
)
|
|
||||||
# Le VMID est annoncé MAINTENANT. Un numéro noté pour une VM qui
|
|
||||||
# n'existera jamais ne coûte rien — « --detruire » lit « qm list » et
|
|
||||||
# la dit absente — alors qu'une VM créée et non notée reste sur le
|
|
||||||
# parent, invisible.
|
|
||||||
if noter:
|
|
||||||
noter(vmid)
|
|
||||||
for cmd in [pve.image_fetch_cmd(url, image)] + pve.create_cmds(
|
|
||||||
vmid, spec
|
|
||||||
):
|
|
||||||
code, _o = self.executer(
|
|
||||||
parent, cmd, self.delai("creation"), "qm create"
|
|
||||||
)
|
|
||||||
if code and not self.dry_run:
|
|
||||||
return None, None
|
|
||||||
return vmid, adresse
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- #
|
|
||||||
# La descente
|
|
||||||
def remettre_debout(self, hote):
|
|
||||||
"""Les unités PVE, puis le CONSTAT que /etc/pve est monté."""
|
|
||||||
if self.dry_run:
|
|
||||||
print(" unités PVE + montage de /etc/pve")
|
|
||||||
return True
|
|
||||||
# « pve_unit_cmd » joint le journal de l'unité à un échec — « la seule
|
|
||||||
# façon de dire la cause à quelqu'un dont le seul accès à l'hôte est
|
|
||||||
# cet outil », dit son propre commentaire. On le JETAIT : quand le
|
|
||||||
# montage échouait ensuite, il ne restait qu'un « /etc/pve : ABSENT »
|
|
||||||
# sans cause, et il fallait retourner sur la machine pour la chercher.
|
|
||||||
echecs = []
|
|
||||||
for unite in pve.PVE_UNITS:
|
|
||||||
code, sortie = self.executer(
|
|
||||||
hote, pve.pve_unit_cmd(unite, remonte=True), 300, unite
|
|
||||||
)
|
|
||||||
propre = pve.strip_ssh_noise(sortie)
|
|
||||||
if code or "-KO" in propre:
|
|
||||||
echecs.append((unite, propre))
|
|
||||||
_c, out = self.executer(
|
|
||||||
hote, pve.mount_wait_cmd(), self.delai("reparation"), "montage"
|
|
||||||
)
|
|
||||||
vu = pve.parse_mount_wait(out)
|
|
||||||
self.dire(f" /etc/pve : {vu['verdict']}")
|
|
||||||
if vu["verdict"] != "MONTE":
|
|
||||||
for unite, propre in echecs:
|
|
||||||
self.dire(f" ↳ {unite} : {propre.strip()[-400:]}")
|
|
||||||
if not echecs:
|
|
||||||
# Toutes debout et le montage absent : le dire, plutôt que de
|
|
||||||
# laisser croire qu'on n'a pas regardé.
|
|
||||||
self.dire(" ↳ toutes les unités PVE sont debout")
|
|
||||||
return vu["verdict"] == "MONTE"
|
|
||||||
|
|
||||||
def controler(self, hote):
|
|
||||||
"""Ce parent peut-il héberger l'étage suivant ?
|
|
||||||
|
|
||||||
Sixième étape, et elle manquait : le contrôle du stockage était celui
|
|
||||||
du DÉBUT de l'étage suivant, si bien qu'un étage marqué « terminé »
|
|
||||||
pouvait n'avoir aucun stockage capable d'accueillir une image — et le
|
|
||||||
compteur d'étages atteints mentait d'autant.
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
print(" pvesm status : un stockage pour les images")
|
|
||||||
return True
|
|
||||||
code, out = self.executer(
|
|
||||||
hote,
|
|
||||||
"pvesm status --content images",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"pvesm",
|
|
||||||
)
|
|
||||||
if code:
|
|
||||||
self.dire(" ✗ « pvesm status » a échoué : rien conclu")
|
|
||||||
return False
|
|
||||||
stockage = pve.pick_storage(pve.parse_storages(out))
|
|
||||||
if not stockage:
|
|
||||||
self.dire(" ✗ aucun stockage pour les images")
|
|
||||||
return False
|
|
||||||
self.dire(f" stockage : {stockage}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def detruire_une(parent_alias, vmid, nom, journal):
|
|
||||||
"""Arrête puis détruit UNE VM, par son VMID. Rend True si elle a disparu.
|
|
||||||
|
|
||||||
Par le VMID et par égalité stricte du nom : un filtre par sous-chaîne
|
|
||||||
aurait pris une « deep-pve-lab » de production, et « --purge » emporte les
|
|
||||||
disques ET les entrées de sauvegarde.
|
|
||||||
|
|
||||||
L'arrêt est CONSTATÉ avant la destruction : « qm stop » rend la main dès
|
|
||||||
que la tâche est lancée, et sur un hyperviseur imbriqué mesuré 36 fois
|
|
||||||
plus lent, « qm destroy » arrivait alors que la VM tournait encore et
|
|
||||||
refusait avec « VM is running ».
|
|
||||||
"""
|
|
||||||
parent = {"target": parent_alias, "sudo": "sudo ", "jump": ""}
|
|
||||||
code, out = pve.run(parent, "qm list", 180)
|
|
||||||
if code:
|
|
||||||
dire(f" ✗ {parent_alias} injoignable : rien touché", journal)
|
|
||||||
return False
|
|
||||||
presentes = {
|
|
||||||
int(v["vmid"]): (v.get("name") or "") for v in pve.parse_qm_list(out)
|
|
||||||
}
|
|
||||||
if vmid not in presentes:
|
|
||||||
dire(f" — {vmid} déjà absente de {parent_alias}", journal)
|
|
||||||
return True
|
|
||||||
if presentes[vmid] != nom:
|
|
||||||
dire(
|
|
||||||
f" ✗ {vmid} sur {parent_alias} s'appelle"
|
|
||||||
f" « {presentes[vmid]} », pas « {nom} » : rien touché",
|
|
||||||
journal,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
pve.run(parent, f"qm stop {vmid} --skiplock 1 || true", 300)
|
|
||||||
for _ in range(20):
|
|
||||||
_c, etat = pve.run(parent, f"qm status {vmid}", 120)
|
|
||||||
if "stopped" in pve.strip_ssh_noise(etat):
|
|
||||||
break
|
|
||||||
time.sleep(6)
|
|
||||||
code, out = pve.run(parent, f"qm destroy {vmid} --purge 1", 600)
|
|
||||||
if code:
|
|
||||||
dire(f" ✗ qm destroy {vmid} : code {code}", journal)
|
|
||||||
for ligne in pve.strip_ssh_noise(out).strip().splitlines()[-3:]:
|
|
||||||
dire(f" {ligne}", journal)
|
|
||||||
return False
|
|
||||||
dire(f" ✓ {nom} ({vmid}) sur {parent_alias}", journal)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
FAMILLE = Famille(OUTIL, NOM_BASE, detruire_une)
|
|
||||||
|
|
||||||
|
|
||||||
def principal(argv=None):
|
|
||||||
return mener(
|
|
||||||
argv,
|
|
||||||
"Jusqu'à quel étage un Proxmox dans un Proxmox tient-il ?",
|
|
||||||
FAMILLE,
|
|
||||||
Descente,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(principal())
|
|
||||||
|
|
@ -1,567 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
||||||
"""Jusqu'à quel étage une QEMU dans une QEMU tient-elle ?
|
|
||||||
|
|
||||||
Le pendant de `deep_proxmox.py`, et sa raison d'être : le ralentissement du
|
|
||||||
quatrième étage vient du PROCESSEUR — de ce que coûte une sortie de VM sous
|
|
||||||
pagination imbriquée — mais le coût par étage, lui, vient de ce qu'on installe.
|
|
||||||
Un nœud Proxmox pose un noyau, corosync, ceph et une interface web ; un hôte
|
|
||||||
libvirt nu pose libvirtd et qemu-kvm. Les deux mesures ensemble séparent ce qui
|
|
||||||
tient au matériel de ce qui tient à la pile, deux choses que la seule mesure
|
|
||||||
Proxmox confond.
|
|
||||||
|
|
||||||
CE QUE CE TEST DOIT PROUVER AVANT DE MESURER
|
|
||||||
|
|
||||||
`deploy_qemu.py` ne passe jamais « --cpu host-passthrough » : il s'en remet au
|
|
||||||
défaut de virt-install. Et quand /dev/kvm manque, il n'échoue pas — il pose
|
|
||||||
« --virt-type qemu », avertit sur une ligne, et crée une VM ENTIÈREMENT ÉMULÉE.
|
|
||||||
Un étage émulé démarre en sept minutes et demie au lieu de quelques secondes,
|
|
||||||
et rien dans le code de retour ne le dit.
|
|
||||||
|
|
||||||
Sans garde, ce script mesurerait donc de la TCG empilée en croyant mesurer de
|
|
||||||
la virtualisation imbriquée — et rendrait un chiffre plus flatteur, et faux.
|
|
||||||
D'où le contrôle de chaque étage : /dev/kvm lisible, « nested » à Y, et le
|
|
||||||
domaine de l'enfant en type='kvm' avec un CPU host-passthrough. Un étage qui
|
|
||||||
échoue à cela arrête la descente au lieu de la prolonger dans le vide.
|
|
||||||
|
|
||||||
./long_test/deep_qemu.py # trois étages
|
|
||||||
./long_test/deep_qemu.py --depth 5 # en demander plus, sciemment
|
|
||||||
./long_test/deep_qemu.py --dry-run # le plan, rien de créé
|
|
||||||
./long_test/deep_qemu.py --detruire # défaire ce qui a été posé
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
sys.path.insert(0, RACINE)
|
|
||||||
sys.path.insert(0, os.path.join(RACINE, "long_test"))
|
|
||||||
|
|
||||||
import descente # noqa: E402
|
|
||||||
from descente import ( # noqa: E402,F401
|
|
||||||
DELAIS,
|
|
||||||
Famille,
|
|
||||||
a_defaire,
|
|
||||||
autre_descente,
|
|
||||||
capacite_hote,
|
|
||||||
cle_publique,
|
|
||||||
dernier_rapport,
|
|
||||||
descente_vivante,
|
|
||||||
detruire,
|
|
||||||
detruire_etage1,
|
|
||||||
dire,
|
|
||||||
identite_de,
|
|
||||||
mener,
|
|
||||||
module_qemu,
|
|
||||||
retirer_alias,
|
|
||||||
)
|
|
||||||
from descente import (
|
|
||||||
alias_etage as _alias_etage,
|
|
||||||
)
|
|
||||||
from descente import (
|
|
||||||
nom_etage as _nom_etage,
|
|
||||||
)
|
|
||||||
|
|
||||||
from script.proxmox import nesting # noqa: E402
|
|
||||||
from script.proxmox import proxmox_deploy as pve # noqa: E402
|
|
||||||
|
|
||||||
# Une Debian nue : c'est elle qui recevra libvirt et qemu-kvm.
|
|
||||||
DISTRO = "debian"
|
|
||||||
NOM_BASE = "deep-qemu"
|
|
||||||
OUTIL = "deep_qemu"
|
|
||||||
|
|
||||||
# Le script est envoyé par scp, comme install_proxmox.sh l'est pour Proxmox :
|
|
||||||
# c'est NOTRE code qu'on veut éprouver, et il n'importe que la bibliothèque
|
|
||||||
# standard, donc il tourne dans un invité nu sans rien d'autre.
|
|
||||||
LOCAL_CLI = "script/qemu/deploy_qemu.py"
|
|
||||||
DISTANT_CLI = "/tmp/deploy_qemu.py"
|
|
||||||
CLE_DISTANTE = "/root/.ssh/longtest.pub"
|
|
||||||
|
|
||||||
# Trois faits, trois lignes, et l'ABSENCE d'une ligne vaut « non ». Écrit pour
|
|
||||||
# dash : /bin/sh sur Debian n'est pas bash.
|
|
||||||
CONTROLE_CMD = (
|
|
||||||
"if [ -r /dev/kvm ]; then echo KVM=oui; else echo KVM=non; fi; "
|
|
||||||
"cat /sys/module/kvm_amd/parameters/nested 2>/dev/null"
|
|
||||||
" | sed s/^/NESTED=/; "
|
|
||||||
"cat /sys/module/kvm_intel/parameters/nested 2>/dev/null"
|
|
||||||
" | sed s/^/NESTED=/; "
|
|
||||||
"df --output=avail -BG /var/lib/libvirt/images 2>/dev/null"
|
|
||||||
" | tail -1 | sed s/^/DISQUE=/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Les listes apt AVANT toute installation. Constaté au premier lancement
|
|
||||||
# réel : « --setup-host » a échoué en ZÉRO seconde sur « Unable to locate
|
|
||||||
# package qemu-system-x86 », alors que le paquet existe. La VM venait de
|
|
||||||
# démarrer, ses listes ne portaient que « bookworm-security », et un
|
|
||||||
# apt-get update les a complétées d'un coup.
|
|
||||||
#
|
|
||||||
# Deux causes, une seule parade : cloud-init n'a pas fini de composer
|
|
||||||
# /etc/apt, et apt-daily tient le verrou des listes au premier démarrage.
|
|
||||||
# install_proxmox.sh a la même parade, et pour la même raison — arrêter les
|
|
||||||
# minuteries, puis réessayer.
|
|
||||||
PREPARE_APT_CMD = (
|
|
||||||
"systemctl stop apt-daily.service apt-daily-upgrade.service"
|
|
||||||
" apt-daily.timer apt-daily-upgrade.timer >/dev/null 2>&1;"
|
|
||||||
" i=1; while [ $i -le 12 ]; do"
|
|
||||||
" DEBIAN_FRONTEND=noninteractive apt-get update && exit 0;"
|
|
||||||
" echo APT-RETRY=$i; sleep 15; i=$((i+1)); done; exit 1"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Le réseau « default » de libvirt sert 192.168.122.0/24, à TOUS les étages.
|
|
||||||
# Un étage dont l'adresse vient du « default » de son parent voit donc son
|
|
||||||
# propre « net-start default » refusé net :
|
|
||||||
#
|
|
||||||
# error: internal error: Network is already in use by interface enp1s0
|
|
||||||
#
|
|
||||||
# Un invité qui vit DANS un réseau ne peut pas servir le même. Chaque étage
|
|
||||||
# reçoit donc son propre sous-réseau, déduit de sa PROFONDEUR : deux étages ne
|
|
||||||
# peuvent pas tomber sur le même, et rien n'est à deviner.
|
|
||||||
#
|
|
||||||
# 131 et au-delà : 122 est le sous-réseau de libvirt, et 123 celui qu'une
|
|
||||||
# machine de développement porte souvent. Les éviter coûte un octet.
|
|
||||||
RESEAU_BASE = 131
|
|
||||||
|
|
||||||
|
|
||||||
def cidr_pour(profondeur):
|
|
||||||
"""Le troisième octet du sous-réseau d'un étage. Déterminé, jamais tiré."""
|
|
||||||
return f"192.168.{RESEAU_BASE + max(0, int(profondeur) - 1)}"
|
|
||||||
|
|
||||||
|
|
||||||
def reseau_xml(prefixe, nom="default"):
|
|
||||||
"""Le réseau NAT d'un étage, en une ligne — pas de heredoc.
|
|
||||||
|
|
||||||
Une seule ligne parce qu'elle traverse deux couches de quoting pour
|
|
||||||
atterrir dans dash : un heredoc n'y survivrait pas.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
f"<network><name>{nom}</name><forward mode='nat'/>"
|
|
||||||
f"<bridge name='virbr0' stp='on' delay='0'/>"
|
|
||||||
f"<ip address='{prefixe}.1' netmask='255.255.255.0'>"
|
|
||||||
f"<dhcp><range start='{prefixe}.10' end='{prefixe}.200'/></dhcp>"
|
|
||||||
f"</ip></network>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
RESEAU_CMD = (
|
|
||||||
"virsh -c qemu:///system net-info default 2>&1 | sed s/^/NET:/; "
|
|
||||||
"systemctl is-active libvirtd 2>/dev/null | sed s/^/UNITE:/"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def nom_etage(niveau):
|
|
||||||
return _nom_etage(niveau, NOM_BASE)
|
|
||||||
|
|
||||||
|
|
||||||
def alias_etage(niveau, parent_alias):
|
|
||||||
return _alias_etage(niveau, parent_alias, NOM_BASE)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_controle(texte):
|
|
||||||
"""Ce que le contrôle a VU. Ce qui n'a pas été lu vaut « non ».
|
|
||||||
|
|
||||||
L'absence d'une ligne n'est jamais un oui : si
|
|
||||||
/sys/module/kvm_amd/parameters/nested n'existe pas, c'est que le module
|
|
||||||
n'est pas chargé, et l'étage suivant serait émulé.
|
|
||||||
"""
|
|
||||||
propre = pve.strip_ssh_noise(texte or "")
|
|
||||||
nested = re.search(r"^NESTED=(\S+)", propre, re.M)
|
|
||||||
disque = re.search(r"^DISQUE=\s*(\d+)", propre, re.M)
|
|
||||||
return {
|
|
||||||
"kvm": bool(re.search(r"^KVM=oui\s*$", propre, re.M)),
|
|
||||||
# « Y » ou « 1 » selon les versions du module.
|
|
||||||
"nested": bool(nested and nested.group(1).strip() in ("Y", "1")),
|
|
||||||
"disque_go": int(disque.group(1)) if disque else 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_reseau(texte):
|
|
||||||
"""Le réseau libvirt « default » est-il actif, et libvirtd debout ?"""
|
|
||||||
propre = pve.strip_ssh_noise(texte or "")
|
|
||||||
actif = re.search(r"^NET:\s*Active:\s*(\S+)", propre, re.M | re.I)
|
|
||||||
unite = re.search(r"^UNITE:(\S+)", propre, re.M)
|
|
||||||
return {
|
|
||||||
"reseau": bool(actif and actif.group(1).lower() == "yes"),
|
|
||||||
"libvirtd": bool(unite and unite.group(1).strip() == "active"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_domifaddr(texte):
|
|
||||||
"""La première adresse IPv4 d'un domaine, sans son masque, ou "".
|
|
||||||
|
|
||||||
virsh écrit un tableau ; on ne prend que les lignes qui annoncent « ipv4 »,
|
|
||||||
et jamais la ligne d'en-tête ni les tirets.
|
|
||||||
"""
|
|
||||||
for ligne in pve.strip_ssh_noise(texte or "").splitlines():
|
|
||||||
champs = ligne.split()
|
|
||||||
if len(champs) >= 4 and champs[2].lower() == "ipv4":
|
|
||||||
return champs[3].split("/")[0]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def parse_domaine(xml):
|
|
||||||
"""Le domaine tourne-t-il sous KVM, et son CPU passe-t-il l'hôte ?
|
|
||||||
|
|
||||||
Les deux comptent, et pour la même raison : un domaine type='qemu' est
|
|
||||||
ÉMULÉ, et un CPU qui ne passe pas les drapeaux de l'hôte ne porte pas la
|
|
||||||
virtualisation — l'étage suivant serait émulé à son tour, sept minutes et
|
|
||||||
demie de démarrage, sans qu'aucun code de retour ne le dise.
|
|
||||||
"""
|
|
||||||
propre = pve.strip_ssh_noise(xml or "")
|
|
||||||
domaine = re.search(r"<domain[^>]*\btype=['\"](\w+)['\"]", propre)
|
|
||||||
cpu = re.search(r"<cpu[^>]*\bmode=['\"]([\w-]+)['\"]", propre)
|
|
||||||
return {
|
|
||||||
"type": domaine.group(1) if domaine else "",
|
|
||||||
"cpu": cpu.group(1) if cpu else "",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Descente(descente.Descente):
|
|
||||||
"""Les verbes de QEMU/KVM. Le reste est dans `descente.Descente`."""
|
|
||||||
|
|
||||||
OUTIL = OUTIL
|
|
||||||
NOM_BASE = NOM_BASE
|
|
||||||
DISTRO = DISTRO
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def _envoyer_cli(self, hote):
|
|
||||||
"""Pose NOTRE deploy_qemu.py sur l'hôte. Rend True s'il y est.
|
|
||||||
|
|
||||||
Refait à chaque besoin plutôt qu'une fois : /tmp est vidé au
|
|
||||||
démarrage sur bien des systèmes, et l'installation redémarre.
|
|
||||||
"""
|
|
||||||
local = os.path.join(RACINE, LOCAL_CLI)
|
|
||||||
if self.dry_run:
|
|
||||||
print(f" scp {local} <hôte>:{DISTANT_CLI}")
|
|
||||||
return True
|
|
||||||
argv = pve.ssh_argv(hote, "")[:-1] # les options, sans la commande
|
|
||||||
cible = argv[-1]
|
|
||||||
options = argv[1:-1]
|
|
||||||
res = subprocess.run(
|
|
||||||
["scp", "-q"] + options + [local, f"{cible}:{DISTANT_CLI}"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=300,
|
|
||||||
)
|
|
||||||
if res.returncode:
|
|
||||||
self.dire(f" ✗ scp : {res.stderr.strip()[:200]}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def installer(self, hote):
|
|
||||||
"""libvirt et qemu-kvm, par « --setup-host ».
|
|
||||||
|
|
||||||
Le code de retour ne prouve RIEN ici : « --setup-host » rend 0 même
|
|
||||||
quand il s'est contenté de PROGRAMMER un redémarrage. C'est l'étape
|
|
||||||
suivante — redémarrer et constater que la machine est revenue — qui
|
|
||||||
prouve quelque chose, et le contrôle de fin d'étage qui prouve que KVM
|
|
||||||
est là.
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
print(f" {DISTANT_CLI} --setup-host")
|
|
||||||
return True
|
|
||||||
if not self._envoyer_cli(hote):
|
|
||||||
return False
|
|
||||||
# Les listes d'abord. Sans elles, « --setup-host » échoue en zéro
|
|
||||||
# seconde sur des paquets qui existent — et le message parle de
|
|
||||||
# paquets introuvables, pas de listes vides.
|
|
||||||
code, _o = self.executer(
|
|
||||||
hote,
|
|
||||||
PREPARE_APT_CMD,
|
|
||||||
self.delai("install"),
|
|
||||||
"apt-get update",
|
|
||||||
)
|
|
||||||
if code:
|
|
||||||
self.dire(" ✗ listes apt : le verrou reste tenu")
|
|
||||||
return False
|
|
||||||
code, _o = self.executer(
|
|
||||||
hote,
|
|
||||||
f"python3 {DISTANT_CLI} --setup-host --assume-yes",
|
|
||||||
self.delai("install"),
|
|
||||||
"deploy_qemu --setup-host",
|
|
||||||
montrer=True,
|
|
||||||
)
|
|
||||||
return code == 0
|
|
||||||
|
|
||||||
def noyau_convient(self, noyau):
|
|
||||||
"""Tout noyau convient : c'est le REDÉMARRAGE qui compte, pas ce
|
|
||||||
qu'on redémarre.
|
|
||||||
|
|
||||||
Il charge les modules KVM et applique l'appartenance au groupe
|
|
||||||
libvirt, qui ne prend effet qu'à la SESSION SUIVANTE — sans lui,
|
|
||||||
virt-install retombe sur qemu:///session, où le réseau « default »
|
|
||||||
n'existe pas. Le moteur prouve déjà que la machine a vraiment
|
|
||||||
redémarré en comparant l'instant de démarrage.
|
|
||||||
"""
|
|
||||||
return bool(noyau)
|
|
||||||
|
|
||||||
def remettre_debout(self, hote):
|
|
||||||
"""libvirtd actif et le réseau « default » démarré."""
|
|
||||||
if self.dry_run:
|
|
||||||
print(" libvirtd + réseau default")
|
|
||||||
return True
|
|
||||||
# Son sous-réseau à LUI, sinon « net-start default » se heurte à
|
|
||||||
# l'adresse que son parent lui a servie.
|
|
||||||
profondeur = self.profondeur_racine + max(1, self.niveau_courant)
|
|
||||||
xml = reseau_xml(cidr_pour(profondeur))
|
|
||||||
self.executer(
|
|
||||||
hote,
|
|
||||||
"systemctl enable --now libvirtd 2>/dev/null;"
|
|
||||||
" virsh -c qemu:///system net-destroy default 2>/dev/null;"
|
|
||||||
" virsh -c qemu:///system net-undefine default 2>/dev/null;"
|
|
||||||
f" printf '%s' {shlex.quote(xml)} > /tmp/reseau.xml;"
|
|
||||||
" virsh -c qemu:///system net-define /tmp/reseau.xml;"
|
|
||||||
" virsh -c qemu:///system net-start default;"
|
|
||||||
" virsh -c qemu:///system net-autostart default; true",
|
|
||||||
self.delai("reparation"),
|
|
||||||
"libvirtd",
|
|
||||||
)
|
|
||||||
self.dire(f" réseau {cidr_pour(profondeur)}.0/24")
|
|
||||||
_c, out = self.executer(
|
|
||||||
hote, RESEAU_CMD, self.delai("controle"), "réseau"
|
|
||||||
)
|
|
||||||
vu = parse_reseau(out)
|
|
||||||
self.dire(
|
|
||||||
f" libvirtd {'actif' if vu['libvirtd'] else 'ABSENT'},"
|
|
||||||
f" réseau default {'actif' if vu['reseau'] else 'ABSENT'}"
|
|
||||||
)
|
|
||||||
return vu["libvirtd"] and vu["reseau"]
|
|
||||||
|
|
||||||
def rallumer_a_froid(self, parent, nom):
|
|
||||||
"""« virsh destroy » puis « start » : un processus QEMU neuf.
|
|
||||||
|
|
||||||
Mesuré sur la machine bloquée : à chaud elle restait 46 minutes au
|
|
||||||
même pointeur d'instruction, dans son micrologiciel ; à froid elle a
|
|
||||||
chargé son noyau en 60 à 90 secondes, trois fois de suite, à 2, 3 et
|
|
||||||
4 Go. Ce n'est donc pas la taille de la mémoire — c'est la façon de
|
|
||||||
redémarrer.
|
|
||||||
"""
|
|
||||||
if self.dry_run or not parent:
|
|
||||||
return False
|
|
||||||
self.executer(
|
|
||||||
parent,
|
|
||||||
f"virsh -c qemu:///system destroy {nom} 2>/dev/null; true",
|
|
||||||
self.delai("controle"),
|
|
||||||
"extinction",
|
|
||||||
)
|
|
||||||
code, _o = self.executer(
|
|
||||||
parent,
|
|
||||||
f"virsh -c qemu:///system start {nom}",
|
|
||||||
self.delai("controle"),
|
|
||||||
"rallumage",
|
|
||||||
)
|
|
||||||
return code == 0
|
|
||||||
|
|
||||||
def controler(self, hote):
|
|
||||||
"""CET étage peut-il héberger le suivant SANS l'émuler ?
|
|
||||||
|
|
||||||
Le contrôle qui donne son sens à la mesure. Sans lui, un étage sans
|
|
||||||
KVM ne casse pas : il bascule en émulation et continue. La descente
|
|
||||||
irait plus « profond » en mesurant tout autre chose — de la TCG
|
|
||||||
empilée, pas de la virtualisation imbriquée.
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
print(" /dev/kvm + nested=Y + place disque")
|
|
||||||
return True
|
|
||||||
code, out = self.executer(
|
|
||||||
hote, CONTROLE_CMD, self.delai("controle"), "kvm"
|
|
||||||
)
|
|
||||||
if code:
|
|
||||||
self.dire(" ✗ contrôle KVM illisible : rien conclu")
|
|
||||||
return False
|
|
||||||
vu = parse_controle(out)
|
|
||||||
self.dire(
|
|
||||||
f" /dev/kvm {'oui' if vu['kvm'] else 'NON'},"
|
|
||||||
f" nested {'oui' if vu['nested'] else 'NON'},"
|
|
||||||
f" {vu['disque_go']} Go libres"
|
|
||||||
)
|
|
||||||
if not vu["kvm"]:
|
|
||||||
self.dire(" ✗ pas de /dev/kvm : l'étage suivant serait ÉMULÉ")
|
|
||||||
return False
|
|
||||||
if not vu["nested"]:
|
|
||||||
self.dire(
|
|
||||||
" ✗ virtualisation imbriquée absente :"
|
|
||||||
" l'étage suivant serait ÉMULÉ"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def preparer_parent(self, parent):
|
|
||||||
"""Ce qu'il faut du parent : son réseau. C'est tout.
|
|
||||||
|
|
||||||
Rien à construire, contrairement à Proxmox, où il faut poser un pont
|
|
||||||
et un NAT dans /etc/network/interfaces : libvirt fournit déjà
|
|
||||||
« default », avec NAT, bail DHCP ET résolveur dnsmasq. Le défaut qui a
|
|
||||||
coûté cher là-bas — une VM en adresse fixe qui route mais ne résout
|
|
||||||
rien, et une installation qui meurt sur « apt update » sans que rien
|
|
||||||
ne l'explique — ne peut pas se produire ici.
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
print(" réseau default du parent")
|
|
||||||
return ("default",)
|
|
||||||
code, out = self.executer(
|
|
||||||
parent, RESEAU_CMD, self.delai("controle"), "réseau"
|
|
||||||
)
|
|
||||||
if code:
|
|
||||||
self.dire(" ✗ état du réseau illisible : rien conclu")
|
|
||||||
return None
|
|
||||||
vu = parse_reseau(out)
|
|
||||||
if not vu["reseau"]:
|
|
||||||
self.dire(
|
|
||||||
" ✗ le réseau « default » du parent n'est pas actif"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
return ("default",)
|
|
||||||
|
|
||||||
def attendre_adresse(self, parent, nom):
|
|
||||||
"""Le bail DHCP de l'enfant, attendu. Rend l'adresse, ou "".
|
|
||||||
|
|
||||||
ATTENDU, et non lu une fois. Constaté au troisième étage : le domaine
|
|
||||||
était créé, en type='kvm', et « domifaddr » ne rendait rien — l'invité
|
|
||||||
n'avait pas encore demandé son bail. Plus l'étage est profond, plus il
|
|
||||||
démarre lentement, et c'est justement ce qu'on mesure.
|
|
||||||
|
|
||||||
`deploy_qemu` attend lui-même l'adresse — 90 secondes par défaut — puis
|
|
||||||
rend 0 quand il ne l'a pas trouvée. Son code de sortie ne prouve donc
|
|
||||||
rien ici non plus.
|
|
||||||
"""
|
|
||||||
debut = time.time()
|
|
||||||
delai = self.delai("ssh")
|
|
||||||
while time.time() - debut < delai:
|
|
||||||
_c, sortie = self.executer(
|
|
||||||
parent,
|
|
||||||
f"virsh -c qemu:///system domifaddr {nom} --source lease",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"domifaddr",
|
|
||||||
)
|
|
||||||
adresse = parse_domifaddr(sortie)
|
|
||||||
if adresse:
|
|
||||||
if time.time() - debut > 20:
|
|
||||||
self.dire(f" bail après {int(time.time() - debut)} s")
|
|
||||||
return adresse
|
|
||||||
time.sleep(15)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def creer_enfant(self, parent, niveau, res, prepare, noter=None):
|
|
||||||
"""Une VM dans le parent, par NOTRE deploy_qemu.py.
|
|
||||||
|
|
||||||
L'ordre compte, et il n'est pas celui de Proxmox. Là-bas l'adresse est
|
|
||||||
exigée AVANT la création (« --ipconfig0 ») ; ici libvirt ne la donne
|
|
||||||
qu'APRÈS le démarrage. On note donc l'identité — le nom du domaine,
|
|
||||||
qui est déterminé — avant la première commande qui peut créer quoi que
|
|
||||||
ce soit, faute de quoi une création échouée à mi-chemin laisserait une
|
|
||||||
machine que le rapport ne nomme nulle part.
|
|
||||||
"""
|
|
||||||
(reseau,) = prepare
|
|
||||||
nom = self.nom_etage(niveau)
|
|
||||||
if noter:
|
|
||||||
noter(nom)
|
|
||||||
if not self._envoyer_cli(parent):
|
|
||||||
return None, None
|
|
||||||
pub = cle_publique()
|
|
||||||
if pub and not self.dry_run:
|
|
||||||
with open(pub, encoding="utf-8") as fh:
|
|
||||||
contenu = fh.read().strip()
|
|
||||||
self.executer(
|
|
||||||
parent,
|
|
||||||
f"mkdir -p /root/.ssh && printf '%s\\n'"
|
|
||||||
f" {shlex.quote(contenu)} > {CLE_DISTANTE}",
|
|
||||||
DELAIS["controle"],
|
|
||||||
"clé",
|
|
||||||
)
|
|
||||||
creation = (
|
|
||||||
f"python3 {DISTANT_CLI} --distro {DISTRO} --name {nom}"
|
|
||||||
f" --vcpus {res['vcpu']} --memory {res['ram']}"
|
|
||||||
f" --disk-size {res['disque']}G --network network={reseau}"
|
|
||||||
f" --ssh-key {CLE_DISTANTE} --assume-yes"
|
|
||||||
)
|
|
||||||
code, _o = self.executer(
|
|
||||||
parent,
|
|
||||||
creation,
|
|
||||||
self.delai("creation"),
|
|
||||||
"deploy_qemu",
|
|
||||||
montrer=True,
|
|
||||||
)
|
|
||||||
if code and not self.dry_run:
|
|
||||||
return None, None
|
|
||||||
if self.dry_run:
|
|
||||||
return nom, "10.10.10.150"
|
|
||||||
# Le domaine est-il vraiment accéléré ? « deploy_qemu » n'échoue PAS
|
|
||||||
# quand KVM manque : il pose --virt-type qemu et continue. Un étage
|
|
||||||
# émulé fausserait toute la mesure sans rien dire.
|
|
||||||
_c, xml = self.executer(
|
|
||||||
parent,
|
|
||||||
f"virsh -c qemu:///system dumpxml {nom}",
|
|
||||||
self.delai("controle"),
|
|
||||||
"dumpxml",
|
|
||||||
)
|
|
||||||
vu = parse_domaine(xml)
|
|
||||||
if vu["type"] != "kvm":
|
|
||||||
self.dire(
|
|
||||||
f" ✗ domaine type='{vu['type'] or '?'}' : cette VM est"
|
|
||||||
" ÉMULÉE, la mesure ne voudrait rien dire"
|
|
||||||
)
|
|
||||||
return None, None
|
|
||||||
self.dire(f" domaine kvm, cpu {vu['cpu'] or '?'}")
|
|
||||||
adresse = self.attendre_adresse(parent, nom)
|
|
||||||
if not adresse:
|
|
||||||
self.dire(" ✗ créée, mais sans adresse : rien à joindre")
|
|
||||||
return None, None
|
|
||||||
self.dire(f" {nom} : {adresse}")
|
|
||||||
return nom, adresse
|
|
||||||
|
|
||||||
|
|
||||||
def detruire_une(parent_alias, identite, nom, journal):
|
|
||||||
"""Arrête puis détruit UNE VM chez son parent, par son NOM et son UUID.
|
|
||||||
|
|
||||||
Par égalité STRICTE du nom : un filtre par sous-chaîne aurait pris une
|
|
||||||
« deep-qemu-lab » de production, et « --remove-all-storage » efface un
|
|
||||||
disque pour de bon.
|
|
||||||
"""
|
|
||||||
parent = {"target": parent_alias, "sudo": "sudo ", "jump": ""}
|
|
||||||
code, out = pve.run(
|
|
||||||
parent, "virsh -c qemu:///system list --all --name", 180
|
|
||||||
)
|
|
||||||
if code:
|
|
||||||
dire(f" ✗ {parent_alias} injoignable : rien touché", journal)
|
|
||||||
return False
|
|
||||||
presents = [
|
|
||||||
ligne.strip()
|
|
||||||
for ligne in pve.strip_ssh_noise(out).splitlines()
|
|
||||||
if ligne.strip()
|
|
||||||
]
|
|
||||||
if nom not in presents:
|
|
||||||
dire(f" — {nom} : absent de {parent_alias}", journal)
|
|
||||||
return True
|
|
||||||
pve.run(parent, f"virsh -c qemu:///system destroy {nom}", 300)
|
|
||||||
code, _o = pve.run(
|
|
||||||
parent,
|
|
||||||
f"virsh -c qemu:///system undefine {nom} --nvram --remove-all-storage",
|
|
||||||
600,
|
|
||||||
)
|
|
||||||
if code:
|
|
||||||
dire(f" ✗ {nom} sur {parent_alias} : undefine a échoué", journal)
|
|
||||||
return False
|
|
||||||
dire(f" ✓ {nom} ({identite}) sur {parent_alias}", journal)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
FAMILLE = Famille(OUTIL, NOM_BASE, detruire_une)
|
|
||||||
|
|
||||||
|
|
||||||
def principal(argv=None):
|
|
||||||
return mener(
|
|
||||||
argv,
|
|
||||||
"Jusqu'à quel étage une QEMU dans une QEMU tient-elle ?",
|
|
||||||
FAMILLE,
|
|
||||||
Descente,
|
|
||||||
nesting.COUTS_QEMU,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(principal())
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,543 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
||||||
"""ERPLibre s'installe-t-il sur NixOS, de bout en bout ?
|
|
||||||
|
|
||||||
Ni `deep_qemu.py` ni `deep_proxmox.py` : ceux-là mesurent une profondeur
|
|
||||||
d'imbrication, celui-ci n'a qu'UNE machine et une question binaire — le
|
|
||||||
chemin que le menu emprunte aboutit-il sur un système déclaratif.
|
|
||||||
|
|
||||||
Il est ici et non dans `test/` pour la raison qui vaut pour ses deux voisins :
|
|
||||||
il crée une VM, y installe un système entier et prend des heures.
|
|
||||||
|
|
||||||
CE QU'IL ENVOIE, ET POURQUOI CE N'EST PAS UN SCRIPT À LUI
|
|
||||||
|
|
||||||
L'installation est la commande distante que le menu compose,
|
|
||||||
`_qemu_erplibre_remote_cmd`, prise telle quelle. Un test qui installerait par
|
|
||||||
ses propres soins prouverait SON chemin, pas celui du produit — et c'est
|
|
||||||
justement là que se cachaient les pannes : l'amorçage sans branche nix, le
|
|
||||||
Makefile qui présumait /bin/bash, les chemins de compilation lus d'une
|
|
||||||
session plus vieille que le module qu'elle venait d'appliquer.
|
|
||||||
|
|
||||||
UNE SEULE SESSION SSH, ET C'EST LA CONDITION QUI COMPTE
|
|
||||||
|
|
||||||
Le bloc part en UN appel, comme le déploiement le fait. C'est ce qui expose
|
|
||||||
la panne du PREMIER passage : la session est ouverte avant que
|
|
||||||
« make install_os » n'applique le module, donc avant que pam_env ne pose
|
|
||||||
CPATH ; les paquets qui n'ont pas de roue amont s'y arrêtaient. Rejouer
|
|
||||||
l'installation dans une session neuve réussit et donne raison à tort.
|
|
||||||
|
|
||||||
LE VERDICT EST LA VÉRIFICATION, PAS UN CODE DE RETOUR
|
|
||||||
|
|
||||||
« nixos-rebuild switch » rend 4 quand une unité n'a pas redémarré alors que
|
|
||||||
le système EST activé, et chaque bloc d'outil du menu rend 0 par
|
|
||||||
construction. On juge donc sur l'état de la machine : les chemins que envfs
|
|
||||||
fabrique, le venv, les modules qui n'ont pas de roue et doivent se compiler,
|
|
||||||
l'absence des manuels HTML — leur retour ramènerait une construction Sphinx
|
|
||||||
de trois mille pages à chaque installation —, puis Odoo qui répond.
|
|
||||||
|
|
||||||
CE QU'IL INSTALLE : LE DÉPÔT PUBLIÉ, ENVOYÉ PAR LE CODE LOCAL
|
|
||||||
|
|
||||||
Deux moitiés, et il faut les distinguer. La COMMANDE vient du checkout où
|
|
||||||
l'on est — c'est celle du menu, importée. Le CLONE qu'elle fait dans la VM
|
|
||||||
vient du dépôt PUBLIC, sur la branche demandée, « develop » par défaut.
|
|
||||||
|
|
||||||
C'est voulu : le test mesure ce qu'un utilisateur reçoit. Mais la
|
|
||||||
conséquence se dit avant de lancer plutôt que de se découvrir sur un rouge —
|
|
||||||
un correctif encore sur une branche non fusionnée voyage dans la commande et
|
|
||||||
PAS dans le clone. Éprouvé : l'amorçage passe, le clone se fait, et le make
|
|
||||||
s'arrête sur le « /bin/bash » que le dépôt publié exige encore.
|
|
||||||
|
|
||||||
./long_test/install_nixos.py # crée la VM, installe, juge
|
|
||||||
./long_test/install_nixos.py --dry-run # le plan et les commandes
|
|
||||||
./long_test/install_nixos.py --hote nixos-1 # sur une machine qu'on a déjà
|
|
||||||
./long_test/install_nixos.py --detruire # défaire ce qui a été posé
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
sys.path.insert(0, RACINE)
|
|
||||||
sys.path.insert(0, os.path.join(RACINE, "long_test"))
|
|
||||||
|
|
||||||
from descente import ( # noqa: E402
|
|
||||||
Famille,
|
|
||||||
capacite_hote,
|
|
||||||
cle_publique,
|
|
||||||
detruire,
|
|
||||||
dire,
|
|
||||||
)
|
|
||||||
|
|
||||||
DISTRO = "nixos"
|
|
||||||
NOM_BASE = "long-nixos"
|
|
||||||
OUTIL = "install_nixos"
|
|
||||||
|
|
||||||
# La RAM : le catalogue annonce 2048 Mo pour NixOS et le « nixos-rebuild » du
|
|
||||||
# module y tient — mesuré, une fois les manuels HTML écartés. La compilation
|
|
||||||
# de ce qui n'a pas de roue amont, elle, n'a été éprouvée qu'au-dessus : le
|
|
||||||
# défaut est donc plus large que le minimum, et « --memory » le règle pour qui
|
|
||||||
# veut mesurer la limite basse.
|
|
||||||
MEMOIRE_MO = 4096
|
|
||||||
VCPUS = 4
|
|
||||||
# Le disque VIRTUEL de la VM : 40 Go, le minimum que le catalogue annonce. Il
|
|
||||||
# garde la marge des générations du store Nix, que rien ne purge tout seul.
|
|
||||||
DISQUE_GO = 40
|
|
||||||
|
|
||||||
# Ce que l'installation ÉCRIT vraiment, mesuré à la fin d'une installation
|
|
||||||
# complète : 12 Go, dont 5,5 pour le store. C'est ce chiffre-là qu'il faut
|
|
||||||
# comparer à l'espace libre de l'hôte, et non les 40 Go ci-dessus : un qcow2
|
|
||||||
# n'est pas préalloué, il ne prend que ce qu'on y écrit. Les confondre faisait
|
|
||||||
# refuser le test sur une machine qui pouvait parfaitement le mener.
|
|
||||||
DISQUE_ECRIT_GO = 12
|
|
||||||
|
|
||||||
# Ce qu'il faut à la machine AVANT de commencer, en plus de la VM elle-même.
|
|
||||||
# Annoncer un plan qui ne tient pas coûte une heure pour rien.
|
|
||||||
MARGE_RAM_MO = 1024
|
|
||||||
MARGE_DISQUE_GO = 8
|
|
||||||
|
|
||||||
# Les modules qui n'ont PAS de roue amont et doivent donc se compiler dans la
|
|
||||||
# VM. Ce sont eux qui tombent quand les en-têtes manquent, et eux seuls : les
|
|
||||||
# roues manylinux s'installent sans compilateur.
|
|
||||||
MODULES_COMPILES = ("psycopg2", "ldap", "cups", "MySQLdb")
|
|
||||||
|
|
||||||
# Odoo sans base répond 303 vers son sélecteur. 200, 404 et 500 prouvent aussi
|
|
||||||
# qu'un serveur écoute et a chargé son registre ; 000 est l'absence de réponse.
|
|
||||||
CODES_VIVANTS = ("200", "303", "404", "500")
|
|
||||||
|
|
||||||
DELAI_SSH = 600
|
|
||||||
DELAI_INSTALL = 10800
|
|
||||||
DELAI_HTTP = 300
|
|
||||||
|
|
||||||
|
|
||||||
def journal_du_jour():
|
|
||||||
chemin = os.path.expanduser(
|
|
||||||
f"~/.erplibre/longtest/{NOM_BASE}-{time.strftime('%Y%m%d-%H%M%S')}.log"
|
|
||||||
)
|
|
||||||
os.makedirs(os.path.dirname(chemin), exist_ok=True)
|
|
||||||
return chemin
|
|
||||||
|
|
||||||
|
|
||||||
def commande_installation(branche):
|
|
||||||
"""Le bloc que le MENU envoie, pris tel quel.
|
|
||||||
|
|
||||||
Importé plutôt que recopié : deux copies d'une chaîne d'installation
|
|
||||||
divergent, et c'est la copie du test qui reste verte pendant que le
|
|
||||||
produit casse.
|
|
||||||
"""
|
|
||||||
sys.argv = ["todo.py"]
|
|
||||||
from script.todo.todo import TODO
|
|
||||||
|
|
||||||
return TODO.__new__(TODO)._qemu_erplibre_remote_cmd(branche)
|
|
||||||
|
|
||||||
|
|
||||||
def ssh_base(cible, jump=""):
|
|
||||||
opts = [
|
|
||||||
"-o",
|
|
||||||
"StrictHostKeyChecking=no",
|
|
||||||
"-o",
|
|
||||||
"UserKnownHostsFile=/dev/null",
|
|
||||||
"-o",
|
|
||||||
"BatchMode=yes",
|
|
||||||
"-o",
|
|
||||||
"ConnectTimeout=10",
|
|
||||||
]
|
|
||||||
if jump:
|
|
||||||
opts += ["-J", jump]
|
|
||||||
return ["ssh"] + opts + [cible]
|
|
||||||
|
|
||||||
|
|
||||||
def attendre_ssh(cible, journal, jump="", delai=DELAI_SSH):
|
|
||||||
"""Vrai dès que la machine répond. Le premier boot d'une image NixOS pose
|
|
||||||
le compte par cloud-init : tant qu'il n'a pas fini, le port répond mais
|
|
||||||
l'authentification échoue."""
|
|
||||||
fin = time.time() + delai
|
|
||||||
while time.time() < fin:
|
|
||||||
try:
|
|
||||||
fini = subprocess.run(
|
|
||||||
ssh_base(cible, jump) + ["true"],
|
|
||||||
capture_output=True,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
except (OSError, subprocess.SubprocessError):
|
|
||||||
# Un ssh pendu est un tour d'attente de plus, pas la fin de la
|
|
||||||
# course : sans ce rattrapage, une trace d'exception remplaçait
|
|
||||||
# le verdict d'un test lancé pour des heures sans surveillance.
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
if fini.returncode == 0:
|
|
||||||
return True
|
|
||||||
time.sleep(5)
|
|
||||||
dire(f" ✗ {cible} jamais joignable en {delai} s", journal)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def creer_vm(nom, journal, dry_run, memoire=MEMOIRE_MO):
|
|
||||||
"""La VM, par la CLI du dépôt — celle que le menu appelle aussi.
|
|
||||||
|
|
||||||
Rend (nom, uuid) ou (None, ""). L'UUID est ce qui identifie la machine
|
|
||||||
pour la destruction : un nom se réutilise, un UUID non.
|
|
||||||
"""
|
|
||||||
# SOUS SUDO, comme le menu le fait (« parts = [] if dry_run else
|
|
||||||
# ["sudo"] ») : le dossier des images appartient à root en 755 sur une
|
|
||||||
# installation ordinaire de libvirt, et la CLI s'arrête à l'étape 1 sur
|
|
||||||
# « Permission refusée » avant d'avoir rien créé. Sans cela le test ne
|
|
||||||
# peut pas tourner du tout — pas même échouer utilement.
|
|
||||||
#
|
|
||||||
# Jamais à blanc : un essai qui n'écrit rien n'a aucune raison de
|
|
||||||
# demander un mot de passe.
|
|
||||||
argv = ([] if dry_run else ["sudo"]) + [
|
|
||||||
os.path.join(RACINE, ".venv.erplibre/bin/python"),
|
|
||||||
os.path.join(RACINE, "script/qemu/deploy_qemu.py"),
|
|
||||||
"--distro",
|
|
||||||
DISTRO,
|
|
||||||
"--name",
|
|
||||||
nom,
|
|
||||||
"--vcpus",
|
|
||||||
str(VCPUS),
|
|
||||||
"--memory",
|
|
||||||
str(memoire),
|
|
||||||
"--disk-size",
|
|
||||||
f"{DISQUE_GO}G",
|
|
||||||
]
|
|
||||||
pub = cle_publique()
|
|
||||||
if pub:
|
|
||||||
argv += ["--ssh-key", pub]
|
|
||||||
if dry_run:
|
|
||||||
dire(" " + " ".join(shlex.quote(a) for a in argv), journal)
|
|
||||||
return nom, ""
|
|
||||||
try:
|
|
||||||
fini = subprocess.run(argv, timeout=DELAI_SSH * 3)
|
|
||||||
except (OSError, subprocess.SubprocessError) as souci:
|
|
||||||
dire(f" ✗ la création de la VM s'est arrêtée : {souci}", journal)
|
|
||||||
return None, ""
|
|
||||||
if fini.returncode:
|
|
||||||
dire(" ✗ la création de la VM a échoué", journal)
|
|
||||||
return None, ""
|
|
||||||
# L'UUID manquant n'annule PAS la création : la VM existe, et le défaire
|
|
||||||
# saura la reprendre par son nom, en le disant.
|
|
||||||
try:
|
|
||||||
uuid = subprocess.run(
|
|
||||||
["sudo", "-n", "virsh", "domuuid", nom],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
except (OSError, subprocess.SubprocessError):
|
|
||||||
dire(f" ⚠ {nom} : UUID illisible, le nom devra suffire", journal)
|
|
||||||
return nom, ""
|
|
||||||
return nom, ("" if uuid.returncode else uuid.stdout.strip())
|
|
||||||
|
|
||||||
|
|
||||||
def adresse_de(nom, journal):
|
|
||||||
"""L'adresse de la VM, et l'entrée ~/.ssh/config qui va avec.
|
|
||||||
|
|
||||||
La CLI ne l'écrit pas : sans elle, « ssh long-nixos » rend « Name or
|
|
||||||
service not known » et l'attente irait jusqu'à son plein délai sur une
|
|
||||||
machine qui répond parfaitement à son adresse.
|
|
||||||
"""
|
|
||||||
sys.argv = ["todo.py"]
|
|
||||||
from script.todo.todo import TODO
|
|
||||||
|
|
||||||
todo = TODO.__new__(TODO)
|
|
||||||
ip = todo._qemu_vm_ip_now(nom)
|
|
||||||
if not ip:
|
|
||||||
dire(f" ✗ {nom} créée mais sans adresse", journal)
|
|
||||||
return ""
|
|
||||||
prive = cle_publique()[:-4] if cle_publique() else None
|
|
||||||
todo._write_ssh_config_entry([nom], "erplibre", ip, identity_file=prive)
|
|
||||||
dire(f" {nom} : {ip}", journal)
|
|
||||||
return ip
|
|
||||||
|
|
||||||
|
|
||||||
def installer(cible, branche, journal, jump="", dry_run=False):
|
|
||||||
"""Le bloc du menu, en UNE session — la condition qui expose la panne du
|
|
||||||
premier passage. Rend (ok, secondes)."""
|
|
||||||
bloc = commande_installation(branche)
|
|
||||||
if dry_run:
|
|
||||||
dire(f" {len(bloc)} caractères de commande distante", journal)
|
|
||||||
dire(" " + bloc[:200] + " …", journal)
|
|
||||||
return True, 0
|
|
||||||
debut = time.time()
|
|
||||||
try:
|
|
||||||
with open(journal, "a", encoding="utf-8") as fh:
|
|
||||||
fini = subprocess.run(
|
|
||||||
ssh_base(cible, jump) + ["bash -s"],
|
|
||||||
input=bloc,
|
|
||||||
text=True,
|
|
||||||
stdout=fh,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
timeout=DELAI_INSTALL,
|
|
||||||
)
|
|
||||||
except (OSError, subprocess.SubprocessError) as souci:
|
|
||||||
# Une installation qui expire est un FAIT à rapporter, pas une trace
|
|
||||||
# d'exception : la vérification qui suit dira ce que la machine a
|
|
||||||
# tout de même reçu, et c'est elle qui tranche.
|
|
||||||
secondes = int(time.time() - debut)
|
|
||||||
dire(
|
|
||||||
f" ✗ installation interrompue après {secondes} s : {souci}",
|
|
||||||
journal,
|
|
||||||
)
|
|
||||||
return False, secondes
|
|
||||||
secondes = int(time.time() - debut)
|
|
||||||
# Le code de retour est NOTÉ, pas cru : la vérification tranche.
|
|
||||||
dire(f" installation : code={fini.returncode}, {secondes} s", journal)
|
|
||||||
return fini.returncode == 0, secondes
|
|
||||||
|
|
||||||
|
|
||||||
def sonder(cible, commande, jump=""):
|
|
||||||
"""La sortie d'une commande distante, ou "" si elle échoue."""
|
|
||||||
try:
|
|
||||||
fini = subprocess.run(
|
|
||||||
ssh_base(cible, jump) + [commande],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=180,
|
|
||||||
)
|
|
||||||
except (OSError, subprocess.SubprocessError):
|
|
||||||
return ""
|
|
||||||
return fini.stdout.strip() if fini.returncode == 0 else ""
|
|
||||||
|
|
||||||
|
|
||||||
def verifier(cible, journal, jump=""):
|
|
||||||
"""L'état de la machine, contrôle par contrôle. Rend {nom: bool}.
|
|
||||||
|
|
||||||
Ce sont des FAITS, pas des codes de retour : ce que envfs fabrique, le
|
|
||||||
venv, les modules qui doivent se compiler, et un serveur qui répond.
|
|
||||||
"""
|
|
||||||
resultats = {}
|
|
||||||
|
|
||||||
# Par NOM EXACT, jamais par motif. envfs est un système de fichiers qui
|
|
||||||
# RÉSOUT à la demande et n'ÉNUMÈRE pas : « ls /usr/bin » y rend un
|
|
||||||
# répertoire vide alors que « /usr/bin/python3.12 » s'ouvre. Un glob passe
|
|
||||||
# par readdir, donc ne rend rien, et le contrôle déclarait absent ce qui
|
|
||||||
# est là — il éprouvait envfs de la seule façon dont envfs ne peut pas
|
|
||||||
# répondre.
|
|
||||||
#
|
|
||||||
# La version vient du dépôt plutôt que d'être écrite ici : c'est elle que
|
|
||||||
# l'installation a suivie, et une constante vieillirait à côté.
|
|
||||||
chemins = sonder(
|
|
||||||
cible,
|
|
||||||
"for f in /bin/bash /usr/bin/env; do [ -e $f ] || exit 1; done;"
|
|
||||||
" v=$(cut -d. -f1,2 git/erplibre/.python-odoo-version 2>/dev/null);"
|
|
||||||
' [ -n "$v" ] || v=3;'
|
|
||||||
' [ -e "/usr/bin/python$v" ] || exit 1; echo ok',
|
|
||||||
jump,
|
|
||||||
)
|
|
||||||
resultats["envfs"] = chemins == "ok"
|
|
||||||
|
|
||||||
venv = sonder(
|
|
||||||
cible,
|
|
||||||
"cd git/erplibre 2>/dev/null && ls -d .venv.odoo*/bin/python"
|
|
||||||
" 2>/dev/null | head -1",
|
|
||||||
jump,
|
|
||||||
)
|
|
||||||
resultats["venv"] = bool(venv)
|
|
||||||
|
|
||||||
if venv:
|
|
||||||
# Les quatre en un seul import : ce qui manque est nommé par Python
|
|
||||||
# lui-même dans le journal, et un seul aller-retour suffit.
|
|
||||||
code = "import " + ", ".join(MODULES_COMPILES) + "; print('ok')"
|
|
||||||
rendu = sonder(
|
|
||||||
cible,
|
|
||||||
f"cd git/erplibre && ./{venv} -c {shlex.quote(code)}"
|
|
||||||
" 2>&1 | tail -1",
|
|
||||||
jump,
|
|
||||||
)
|
|
||||||
resultats["modules_compiles"] = rendu == "ok"
|
|
||||||
else:
|
|
||||||
resultats["modules_compiles"] = False
|
|
||||||
|
|
||||||
# Les manuels HTML : le module les écarte, et leur retour ferait revenir
|
|
||||||
# une construction Sphinx de trois mille pages à chaque installation.
|
|
||||||
doc = sonder(
|
|
||||||
cible, "ls /run/current-system/sw/share/doc 2>/dev/null | wc -l", jump
|
|
||||||
)
|
|
||||||
resultats["sans_manuels_html"] = doc == "0"
|
|
||||||
|
|
||||||
# Sans venv, il n'y a rien à démarrer : sonder cinq minutes une machine
|
|
||||||
# dont on sait déjà qu'elle n'a pas d'installation ne dit rien de plus.
|
|
||||||
if resultats["venv"]:
|
|
||||||
resultats["odoo_repond"] = odoo_repond(cible, journal, jump)
|
|
||||||
else:
|
|
||||||
dire(" — Odoo non sondé : pas de venv", journal)
|
|
||||||
resultats["odoo_repond"] = False
|
|
||||||
|
|
||||||
for nom, ok in resultats.items():
|
|
||||||
dire(f" {'✓' if ok else '✗'} {nom}", journal)
|
|
||||||
return resultats
|
|
||||||
|
|
||||||
|
|
||||||
def odoo_repond(cible, journal, jump=""):
|
|
||||||
"""Odoo démarré par le dépôt, et un code HTTP qui prouve un registre
|
|
||||||
chargé. Le serveur est lancé ICI plutôt que supposé en service : une VM de
|
|
||||||
développement n'en installe pas forcément un."""
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
ssh_base(cible, jump)
|
|
||||||
+ [
|
|
||||||
"cd git/erplibre &&"
|
|
||||||
" nohup ./run.sh >/tmp/odoo-longtest.log 2>&1 &"
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
except (OSError, subprocess.SubprocessError):
|
|
||||||
dire(" ✗ le lancement d'Odoo n'a pas abouti", journal)
|
|
||||||
return False
|
|
||||||
fin = time.time() + DELAI_HTTP
|
|
||||||
while time.time() < fin:
|
|
||||||
code = sonder(
|
|
||||||
cible,
|
|
||||||
"curl -s -o /dev/null -w '%{http_code}' -m 10"
|
|
||||||
" http://127.0.0.1:8069/web/login",
|
|
||||||
jump,
|
|
||||||
)
|
|
||||||
if code in CODES_VIVANTS:
|
|
||||||
dire(f" Odoo rend {code}", journal)
|
|
||||||
return True
|
|
||||||
time.sleep(10)
|
|
||||||
dire(" ✗ Odoo n'a rien rendu", journal)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def plan_tient(journal, memoire=MEMOIRE_MO):
|
|
||||||
"""La machine peut-elle héberger la VM ? Le dire AVANT de la créer."""
|
|
||||||
coeurs, ram, disque = capacite_hote()
|
|
||||||
dire(
|
|
||||||
f" machine locale : {coeurs} cœurs, {ram} Mo disponibles,"
|
|
||||||
f" {disque} Go de disque",
|
|
||||||
journal,
|
|
||||||
)
|
|
||||||
manque = []
|
|
||||||
if ram < memoire + MARGE_RAM_MO:
|
|
||||||
manque.append(f"RAM (il en faut {memoire + MARGE_RAM_MO} Mo)")
|
|
||||||
if disque < DISQUE_ECRIT_GO + MARGE_DISQUE_GO:
|
|
||||||
manque.append(
|
|
||||||
f"disque (il en faut {DISQUE_ECRIT_GO + MARGE_DISQUE_GO} Go"
|
|
||||||
f" libres ; le disque virtuel fait {DISQUE_GO} Go, creux)"
|
|
||||||
)
|
|
||||||
if manque:
|
|
||||||
dire(f" ✗ pas assez de {' ni de '.join(manque)}", journal)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def mener(argv):
|
|
||||||
parseur = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
||||||
parseur.add_argument("--dry-run", action="store_true")
|
|
||||||
parseur.add_argument("--detruire", action="store_true")
|
|
||||||
parseur.add_argument(
|
|
||||||
"--hote",
|
|
||||||
default="",
|
|
||||||
help="installer sur une machine NixOS existante (alias ssh ou"
|
|
||||||
" user@adresse) au lieu de créer une VM",
|
|
||||||
)
|
|
||||||
parseur.add_argument("--jump", default="", help="rebond ssh pour --hote")
|
|
||||||
parseur.add_argument("--branche", default="develop")
|
|
||||||
parseur.add_argument("--memory", type=int, default=MEMOIRE_MO)
|
|
||||||
args = parseur.parse_args(argv)
|
|
||||||
|
|
||||||
journal = journal_du_jour()
|
|
||||||
|
|
||||||
# `detruire_une` défait une VM IMBRIQUÉE chez son parent. Il n'y en a
|
|
||||||
# aucune ici — une seule machine, au premier étage — donc le défaire
|
|
||||||
# partagé ne l'appelle jamais. On le dit quand même plutôt que de poser
|
|
||||||
# None : le jour où il serait appelé, le journal nommerait la raison au
|
|
||||||
# lieu de s'arrêter sur « NoneType is not callable ».
|
|
||||||
def pas_d_imbrique(parent_alias, identite, nom, log=None):
|
|
||||||
dire(f" ✗ {nom} : ce test ne crée pas de VM imbriquée", log)
|
|
||||||
return False
|
|
||||||
|
|
||||||
famille = Famille(OUTIL, NOM_BASE, pas_d_imbrique)
|
|
||||||
if args.detruire:
|
|
||||||
return detruire(famille, journal, dry_run=args.dry_run)
|
|
||||||
|
|
||||||
dire(f" journal : {journal}", journal)
|
|
||||||
if args.dry_run:
|
|
||||||
dire(" --dry-run : rien ne sera créé.", journal)
|
|
||||||
|
|
||||||
# La forme du rapport n'est pas libre : c'est le CONTRAT du défaire
|
|
||||||
# partagé (descente.py). « etages » vide fait écarter le rapport par
|
|
||||||
# dernier_rapport — « rien créé » — et la VM survivrait au --detruire.
|
|
||||||
# « pid » est ce qui empêche de détruire la machine d'une installation
|
|
||||||
# EN COURS : sans lui, le rapport de la course en cours est le plus
|
|
||||||
# récent, donc celui qu'on choisit.
|
|
||||||
rapport = {
|
|
||||||
"outil": OUTIL,
|
|
||||||
"distro": DISTRO,
|
|
||||||
"branche": args.branche,
|
|
||||||
"dry_run": args.dry_run,
|
|
||||||
"pid": os.getpid(),
|
|
||||||
"etapes": {},
|
|
||||||
"etages": [],
|
|
||||||
}
|
|
||||||
chemin = journal[:-4] + ("-dryrun.json" if args.dry_run else ".json")
|
|
||||||
|
|
||||||
if args.hote:
|
|
||||||
cible, uuid = args.hote, ""
|
|
||||||
dire(f" hôte fourni : {args.hote}", journal)
|
|
||||||
else:
|
|
||||||
if not args.dry_run and not plan_tient(journal, args.memory):
|
|
||||||
return 1
|
|
||||||
nom = f"{NOM_BASE}-{time.strftime('%H%M%S')}"
|
|
||||||
dire(
|
|
||||||
f" création de {nom} ({args.memory} Mo, {DISQUE_GO} Go)", journal
|
|
||||||
)
|
|
||||||
nom, uuid = creer_vm(nom, journal, args.dry_run, args.memory)
|
|
||||||
if not nom:
|
|
||||||
return 1
|
|
||||||
# Écrit AVANT la suite : une course qui meurt en cours
|
|
||||||
# d'installation laisse quand même de quoi la défaire.
|
|
||||||
rapport["etages"] = [
|
|
||||||
{
|
|
||||||
"niveau": 1,
|
|
||||||
"nom": nom,
|
|
||||||
"uuid": uuid,
|
|
||||||
"alias": nom,
|
|
||||||
"cree": True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
cible = nom
|
|
||||||
if not args.dry_run:
|
|
||||||
_ecrire(chemin, rapport, journal)
|
|
||||||
if not adresse_de(nom, journal):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
installer(cible, args.branche, journal, args.jump, dry_run=True)
|
|
||||||
_ecrire(chemin, rapport, journal)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if not attendre_ssh(cible, journal, args.jump):
|
|
||||||
_ecrire(chemin, rapport, journal)
|
|
||||||
return 1
|
|
||||||
rapport["etapes"]["joignable"] = True
|
|
||||||
|
|
||||||
ok, secondes = installer(cible, args.branche, journal, args.jump)
|
|
||||||
rapport["etapes"]["installation_code_zero"] = ok
|
|
||||||
rapport["secondes_installation"] = secondes
|
|
||||||
|
|
||||||
controles = verifier(cible, journal, args.jump)
|
|
||||||
rapport["controles"] = controles
|
|
||||||
_ecrire(chemin, rapport, journal)
|
|
||||||
|
|
||||||
# Le verdict porte sur l'ÉTAT, pas sur le code de retour de l'installation.
|
|
||||||
tout = all(controles.values())
|
|
||||||
dire(f" {'✅' if tout else '❌'} ERPLibre sur NixOS", journal)
|
|
||||||
return 0 if tout else 1
|
|
||||||
|
|
||||||
|
|
||||||
def _ecrire(chemin, rapport, journal):
|
|
||||||
with open(chemin, "w", encoding="utf-8") as fh:
|
|
||||||
json.dump(rapport, fh, indent=2)
|
|
||||||
dire(f" rapport : {chemin}", journal)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(mener(sys.argv[1:]))
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -12,20 +12,12 @@
|
||||||
groups="mobile"
|
groups="mobile"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- sentencepiece : tokeniseur SentencePiece pour MarianMT (NDK JNI)
|
<!-- sentencepiece : tokeniseur SentencePiece pour MarianMT (NDK JNI) -->
|
||||||
Version PUBLIÉE, et non « master ». Deux raisons, la seconde
|
|
||||||
bloquante : « master » fait de ce dépôt une cible mouvante, et deux
|
|
||||||
VM construites le même jour peuvent ne pas contenir le même code ;
|
|
||||||
surtout, le master d'août 2026 récupère protobuf par FetchContent et
|
|
||||||
bâtit son « protoc » POUR LA CIBLE Android avant de l'exécuter sur
|
|
||||||
l'hôte — « Exec format error », la compilation s'arrête là. Mesuré sur
|
|
||||||
une VM Ubuntu 24.04. La v0.2.1 livre au contraire ses .pb.cc
|
|
||||||
pré-générés (src/builtin_pb) et n'a besoin d'aucun protoc. -->
|
|
||||||
<project
|
<project
|
||||||
name="sentencepiece"
|
name="sentencepiece"
|
||||||
path="mobile/erplibre_home_mobile/android/app/src/main/cpp/sentencepiece"
|
path="mobile/erplibre_home_mobile/android/app/src/main/cpp/sentencepiece"
|
||||||
remote="google"
|
remote="google"
|
||||||
revision="refs/tags/v0.2.1"
|
revision="master"
|
||||||
clone-depth="1"
|
clone-depth="1"
|
||||||
groups="mobile"
|
groups="mobile"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
<manifest>
|
<manifest>
|
||||||
<remote name="CybroOdoo" fetch="https://github.com/CybroOdoo/" />
|
<remote name="CybroOdoo" fetch="https://github.com/CybroOdoo/" />
|
||||||
<remote name="ERPLibre" fetch="https://github.com/ERPLibre/" />
|
<remote name="ERPLibre" fetch="https://github.com/ERPLibre/" />
|
||||||
<remote name="ERPLibre_origin_MathBenTech" fetch="https://github.com/ERPLibre/" />
|
|
||||||
<remote name="MathBenTech" fetch="https://github.com/MathBenTech/" />
|
<remote name="MathBenTech" fetch="https://github.com/MathBenTech/" />
|
||||||
<remote name="ERPLibre_origin_TechnoLibre" fetch="https://github.com/ERPLibre/" />
|
<remote name="ERPLibre_origin_TechnoLibre" fetch="https://github.com/ERPLibre/" />
|
||||||
<remote name="Numigi" fetch="https://github.com/Numigi/" />
|
<remote name="Numigi" fetch="https://github.com/Numigi/" />
|
||||||
|
|
@ -241,13 +240,6 @@
|
||||||
revision="13.0"
|
revision="13.0"
|
||||||
groups="addons,odoo13.0"
|
groups="addons,odoo13.0"
|
||||||
/>
|
/>
|
||||||
<project
|
|
||||||
name="development.git"
|
|
||||||
path="odoo13.0/addons/MathBenTech_development"
|
|
||||||
remote="ERPLibre_origin_MathBenTech"
|
|
||||||
revision="13.0"
|
|
||||||
groups="addons,odoo13.0,base"
|
|
||||||
/>
|
|
||||||
<project
|
<project
|
||||||
name="donation.git"
|
name="donation.git"
|
||||||
path="odoo13.0/addons/OCA_donation"
|
path="odoo13.0/addons/OCA_donation"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
<manifest>
|
<manifest>
|
||||||
<remote name="CybroOdoo" fetch="https://github.com/CybroOdoo/" />
|
<remote name="CybroOdoo" fetch="https://github.com/CybroOdoo/" />
|
||||||
<remote name="ERPLibre" fetch="https://github.com/ERPLibre/" />
|
<remote name="ERPLibre" fetch="https://github.com/ERPLibre/" />
|
||||||
<remote name="ERPLibre_origin_MathBenTech" fetch="https://github.com/ERPLibre/" />
|
|
||||||
<remote name="MathBenTech" fetch="https://github.com/MathBenTech/" />
|
<remote name="MathBenTech" fetch="https://github.com/MathBenTech/" />
|
||||||
<remote name="ERPLibre_origin_TechnoLibre" fetch="https://github.com/ERPLibre/" />
|
<remote name="ERPLibre_origin_TechnoLibre" fetch="https://github.com/ERPLibre/" />
|
||||||
<remote name="Numigi" fetch="https://github.com/Numigi/" />
|
<remote name="Numigi" fetch="https://github.com/Numigi/" />
|
||||||
|
|
@ -245,13 +244,6 @@
|
||||||
revision="15.0"
|
revision="15.0"
|
||||||
groups="addons,odoo15.0"
|
groups="addons,odoo15.0"
|
||||||
/>
|
/>
|
||||||
<project
|
|
||||||
name="development.git"
|
|
||||||
path="odoo15.0/addons/MathBenTech_development"
|
|
||||||
remote="ERPLibre_origin_MathBenTech"
|
|
||||||
revision="15.0"
|
|
||||||
groups="addons,odoo15.0,base"
|
|
||||||
/>
|
|
||||||
<project
|
<project
|
||||||
name="donation.git"
|
name="donation.git"
|
||||||
path="odoo15.0/addons/OCA_donation"
|
path="odoo15.0/addons/OCA_donation"
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,10 @@ if [[ ! -d "./mobile/erplibre_home_mobile" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORKSPACE="$(pwd)"
|
cd mobile/erplibre_home_mobile
|
||||||
|
|
||||||
cd mobile/erplibre_home_mobile || exit 1
|
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm run build || exit 1
|
npm run build && npx cap sync
|
||||||
|
|
||||||
# Le transfert des dépôts du manifeste DANS l'application est ce qui fait
|
|
||||||
# l'intérêt de son navigateur de code hors ligne, et il peut être vide sans que
|
|
||||||
# la compilation le dise. Ces dépôts entrent dans des conteneurs — un APK est
|
|
||||||
# un ZIP borné à 65535 entrées, quand un fichier par source en réclamait
|
|
||||||
# 124 350 —
|
|
||||||
# soit une archive tar.gz par dépôt, soit des tranches pack. Le vérificateur
|
|
||||||
# accepte les deux, prouve la présence de CHAQUE fichier promis, et relit un
|
|
||||||
# échantillon octet pour octet contre la source. Quatre pannes qu'un
|
|
||||||
# « build OK » passe sous silence : transfert vide, conteneur absent, index qui
|
|
||||||
# promet un fichier que son conteneur n'a pas, octets qui diffèrent.
|
|
||||||
#
|
|
||||||
# Même vérification que l'installation d'une VM, même script : une seule
|
|
||||||
# autorité.
|
|
||||||
"${WORKSPACE}/script/mobile/check_bundle_transfer.py" . --workspace "${WORKSPACE}" || exit 1
|
|
||||||
|
|
||||||
npx cap sync || exit 1
|
|
||||||
npx cap run android
|
npx cap run android
|
||||||
|
|
||||||
cd - || exit 1
|
cd -
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
# This is required to change environment for the running Odoo
|
# This is required to change environment for the running Odoo
|
||||||
source ./.venv.$(< .erplibre-version)/bin/activate
|
source ./.venv.$(< .erplibre-version)/bin/activate
|
||||||
|
|
||||||
# Le config.conf du DÉPÔT, pas le ~/.odoorc de qui lance. Un « -c »
|
|
||||||
# explicite l'emporte toujours : Odoo lit opt.config avant ODOO_RC.
|
|
||||||
source ./script/lib_odoo_rc.sh
|
|
||||||
odoo_rc_resolve "$(pwd)"
|
|
||||||
|
|
||||||
ODOO_PATH="$(pwd)/odoo$(< .odoo-version)"
|
ODOO_PATH="$(pwd)/odoo$(< .odoo-version)"
|
||||||
#export PATH=$ODOO_PATH:$PATH
|
#export PATH=$ODOO_PATH:$PATH
|
||||||
#echo $PATH
|
#echo $PATH
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "erplibre",
|
"name": "erplibre",
|
||||||
"version": "1.8.0",
|
"version": "1.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "erplibre",
|
"name": "erplibre",
|
||||||
"version": "1.8.0",
|
"version": "1.6.0",
|
||||||
"license": "AGPLv3",
|
"license": "AGPLv3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prettier/plugin-xml": "==2.2.0",
|
"@prettier/plugin-xml": "==2.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "erplibre",
|
"name": "erplibre",
|
||||||
"description": "Easy installation of Odoo",
|
"description": "Easy installation of Odoo",
|
||||||
"version": "1.8.0",
|
"version": "1.6.0",
|
||||||
"license": "AGPLv3",
|
"license": "AGPLv3",
|
||||||
"homepage": "https://erplibre.ca",
|
"homepage": "https://erplibre.ca",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
5
private/.gitignore
vendored
5
private/.gitignore
vendored
|
|
@ -4,8 +4,3 @@
|
||||||
# bump). They describe one specific database, never a shared default, so they
|
# bump). They describe one specific database, never a shared default, so they
|
||||||
# must not be versioned. Shared defaults belong to script/odoo/migration/.
|
# must not be versioned. Shared defaults belong to script/odoo/migration/.
|
||||||
odoo/
|
odoo/
|
||||||
|
|
||||||
# Mount point for a private repository of VPN site presets. Those files name
|
|
||||||
# an institution, its gateway and its authentication group, so they belong to
|
|
||||||
# a repository that is itself private, never to a public fork.
|
|
||||||
vpn/presets/
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,7 @@ selenium
|
||||||
uvloop
|
uvloop
|
||||||
python-randomword-fr
|
python-randomword-fr
|
||||||
isort
|
isort
|
||||||
# Formatage et tri des imports de l'outillage : il suit les versions de
|
|
||||||
# CPython, là où black 24.8.0 s'arrête à py313. Voir .ruff.toml.
|
|
||||||
ruff
|
|
||||||
pykeepass
|
pykeepass
|
||||||
cryptography
|
|
||||||
keyring
|
|
||||||
click
|
click
|
||||||
aioshutil
|
aioshutil
|
||||||
python-magic
|
python-magic
|
||||||
|
|
@ -32,22 +27,14 @@ urwid
|
||||||
# Borne recopiée de script/todo/textual_setup.py (TEXTUAL_SPEC) : les écrans TUI
|
# Borne recopiée de script/todo/textual_setup.py (TEXTUAL_SPEC) : les écrans TUI
|
||||||
# sont écrits pour Textual 8, qui casse son API entre majeures. Modifier les deux.
|
# sont écrits pour Textual 8, qui casse son API entre majeures. Modifier les deux.
|
||||||
textual>=8,<9
|
textual>=8,<9
|
||||||
# Dépendance DIRECTE, même si Textual l'installe : l'écran d'état décode
|
|
||||||
# l'ANSI avec rich.text.Text.from_ansi. Compter sur une dépendance
|
|
||||||
# transitive, c'est dépendre du jour où Textual changera la sienne.
|
|
||||||
rich
|
|
||||||
# Dépendance directe : les outils d'analyse comparent des arbres XML avec lxml.
|
# Dépendance directe : les outils d'analyse comparent des arbres XML avec lxml.
|
||||||
# Il n'arrivait que par pykeepass / openupgradelib / odoo-module-migrator, donc
|
# Il n'arrivait que par pykeepass / openupgradelib / odoo-module-migrator, donc
|
||||||
# un jour où l'un d'eux s'en passe, il disparaît sans que rien ne le réclame.
|
# un jour où l'un d'eux s'en passe, il disparaît sans que rien ne le réclame.
|
||||||
lxml
|
lxml
|
||||||
# Classeurs Excel de « Transform data », et les tests qui les éprouvent. Le
|
|
||||||
# moteur garde son venv dédié (script/todo/transform_setup.py) : un
|
|
||||||
# .venv.erplibre installé avant cette ligne n'a pas openpyxl, et le code ne
|
|
||||||
# l'importe donc jamais au niveau d'un module.
|
|
||||||
openpyxl
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
python-dateutil
|
python-dateutil
|
||||||
unidecode
|
unidecode
|
||||||
|
sshconf
|
||||||
cloudflare
|
cloudflare
|
||||||
psutil
|
psutil
|
||||||
mmg
|
mmg
|
||||||
|
|
@ -65,14 +52,6 @@ virtualenv==20.36.1
|
||||||
git+https://github.com/psf/black.git@24.8.0
|
git+https://github.com/psf/black.git@24.8.0
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
# Bac à sable du client courriel : un VRAI serveur SMTP et un VRAI serveur
|
|
||||||
# IMAP, sur loopback, pour tester ce qu'un faux transport ne peut pas —
|
|
||||||
# littéraux IMAP, en-têtes 8 bits, coupures en plein FETCH. Utilisés
|
|
||||||
# UNIQUEMENT par les tests, jamais par le client lui-même : les tests
|
|
||||||
# concernés s'ignorent d'eux-mêmes si ces paquets manquent.
|
|
||||||
aiosmtpd
|
|
||||||
twisted
|
|
||||||
|
|
||||||
odoo-module-migrator
|
odoo-module-migrator
|
||||||
|
|
||||||
# Ignore because need installation system
|
# Ignore because need installation system
|
||||||
|
|
|
||||||
|
|
@ -6322,17 +6322,6 @@ files = [
|
||||||
{file = "xlrd-1.2.0.tar.gz", hash = "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2"},
|
{file = "xlrd-1.2.0.tar.gz", hash = "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xlsxwriter"
|
|
||||||
version = "1.4.5"
|
|
||||||
description = "A Python module for creating Excel XLSX files."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "XlsxWriter-1.4.5-py2.py3-none-any.whl", hash = "sha256:f9335f1736e2c4fd80e940fe1b6d92d967bf454a1e5d639b0b7a4459ade790cc"},
|
|
||||||
{file = "XlsxWriter-1.4.5.tar.gz", hash = "sha256:0956747859567ec01907e561a7d8413de18a7aae36860f979f9da52b9d58bc19"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xlwt"
|
name = "xlwt"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -6631,4 +6620,4 @@ files = [
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.8.20,<3.9"
|
python-versions = ">=3.8.20,<3.9"
|
||||||
content-hash = "b627b33459aa69db80c29e21da288e828d4f7f2e002bb14565d050010c1bb12b"
|
content-hash = "660feb736151a54776a49b047a9dcf428145a2add97c89dddca2178be2c9d542"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
@ -166,7 +166,6 @@ werkzeug = "0.16.1"
|
||||||
wheel = "^0.45.1"
|
wheel = "^0.45.1"
|
||||||
win-unicode-console = "^0.5"
|
win-unicode-console = "^0.5"
|
||||||
xlrd = "1.2.0"
|
xlrd = "1.2.0"
|
||||||
xlsxwriter = "1.4.5"
|
|
||||||
xlwt = "1.3.0"
|
xlwt = "1.3.0"
|
||||||
xmlformatter = "^0.2.8"
|
xmlformatter = "^0.2.8"
|
||||||
xmlrunner = "^1.7.7"
|
xmlrunner = "^1.7.7"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ target-version = [ "py37",]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ERPLibre"
|
name = "ERPLibre"
|
||||||
version = "1.8.0"
|
version = "1.6.0"
|
||||||
description = "Easy way to configure Odoo community"
|
description = "Easy way to configure Odoo community"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
|
|
@ -20,22 +20,21 @@ authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.12.10,<3.13"
|
python = ">=3.12.10,<3.13"
|
||||||
a2wsgi = ">=1.10.6"
|
a2wsgi = ">=1.10.6"
|
||||||
aiobotocore = "3.9.1"
|
aiobotocore = "3.1.2"
|
||||||
altcha = ">=2.0.0"
|
ansi2html = "^1.9.2"
|
||||||
ansi2html = "^1.9.5"
|
|
||||||
apispec = "^6.10.0"
|
apispec = "^6.10.0"
|
||||||
asn1crypto = "1.5.1"
|
asn1crypto = "1.5.1"
|
||||||
astor = "^0.8.1"
|
astor = "^0.8.1"
|
||||||
avalara = "^26.9.0"
|
avalara = "^26.4.0"
|
||||||
babel = "2.10.3"
|
babel = "2.10.3"
|
||||||
bandit = "^1.9.4"
|
bandit = "^1.9.4"
|
||||||
base64io = "^1.0.3"
|
base64io = "^1.0.3"
|
||||||
beautifulsoup4 = "4.13.5"
|
beautifulsoup4 = "4.13.5"
|
||||||
bokeh = "3.6.3"
|
bokeh = "3.6.3"
|
||||||
boto3 = "1.43.75"
|
boto3 = "1.42.42"
|
||||||
botocore = "1.43.75"
|
botocore = "1.42.42"
|
||||||
cachetools = "^7.2.0"
|
cachetools = "^7.1.1"
|
||||||
cairosvg = "^2.9.1"
|
cairosvg = "^2.9.0"
|
||||||
caldav = "1.3.9"
|
caldav = "1.3.9"
|
||||||
cbor2 = "5.6.2"
|
cbor2 = "5.6.2"
|
||||||
cerberus = "^1.3.8"
|
cerberus = "^1.3.8"
|
||||||
|
|
@ -43,74 +42,72 @@ chardet = "5.2.0"
|
||||||
code-writer = "^1.3.0"
|
code-writer = "^1.3.0"
|
||||||
colorama = "^0.4.6"
|
colorama = "^0.4.6"
|
||||||
contextvars = "^2.4"
|
contextvars = "^2.4"
|
||||||
cryptography = "50.0.0"
|
cryptography = "46.0.5"
|
||||||
cssselect = "^1.5.0"
|
cssselect = "^1.4.0"
|
||||||
cython = "^3.3.0"
|
cython = "^3.2.4"
|
||||||
dateparser = "^1.4.3"
|
dateparser = "^1.4.0"
|
||||||
decorator = "5.1.1"
|
decorator = "5.1.1"
|
||||||
dnspython = "^2.8.0"
|
dnspython = "^2.8.0"
|
||||||
docutils = "0.20.1"
|
docutils = "0.20.1"
|
||||||
dropbox = "^12.2.2"
|
dropbox = "^12.0.2"
|
||||||
easypost = "7.15.0"
|
easypost = "7.15.0"
|
||||||
email-validator = "^2.3.0"
|
email-validator = "^2.3.0"
|
||||||
extendable = ">=0.0.4"
|
extendable = ">=0.0.4"
|
||||||
extendable-pydantic = ">=1.2.0"
|
extendable-pydantic = ">=1.2.0"
|
||||||
extract-msg = "^0.56.1"
|
extract-msg = "^0.55.0"
|
||||||
extruct = "^0.18.0"
|
extruct = "^0.18.0"
|
||||||
ezdxf = "^1.4.4"
|
ezdxf = "^1.4.3"
|
||||||
|
factur-x = "^4.2"
|
||||||
fastapi = ">=0.110.0"
|
fastapi = ">=0.110.0"
|
||||||
filetype = "^1.2.0"
|
filetype = "^1.2.0"
|
||||||
flake8 = "^7.4.1"
|
flake8 = "^7.3.0"
|
||||||
fonttools = "^4.66.0"
|
|
||||||
formio-data = "^2.1.7"
|
formio-data = "^2.1.7"
|
||||||
freezegun = "1.2.1"
|
freezegun = "1.2.1"
|
||||||
geocoder = "^1.38.1"
|
geocoder = "^1.38.1"
|
||||||
geoip2 = "2.9.0"
|
geoip2 = "2.9.0"
|
||||||
geojson = "^3.3.0"
|
geojson = "^3.2.0"
|
||||||
gevent = "24.2.1"
|
gevent = "24.2.1"
|
||||||
gitpython = "^3.1.62"
|
gitpython = "^3.1.50"
|
||||||
giturlparse = "0.12.0"
|
giturlparse = "0.12.0"
|
||||||
googletrans-py = "4.0.0"
|
googletrans-py = "4.0.0"
|
||||||
greenlet = "3.0.3"
|
greenlet = "3.0.3"
|
||||||
httpagentparser = "^1.9.9"
|
httpagentparser = "^1.9.9"
|
||||||
icalendar = "5.0.13"
|
icalendar = "5.0.13"
|
||||||
idna = "3.20"
|
idna = "3.6"
|
||||||
img2pdf = "^0.6.3"
|
img2pdf = "^0.6.3"
|
||||||
isort = "7.0.0"
|
isort = "7.0.0"
|
||||||
itsdangerous = "^2.2.0"
|
|
||||||
jinja2 = "3.1.2"
|
jinja2 = "3.1.2"
|
||||||
js2py = "^0.74"
|
js2py = "^0.74"
|
||||||
libsass = "0.22.0"
|
libsass = "0.22.0"
|
||||||
lottie = "^0.7.2"
|
lottie = "^0.7.2"
|
||||||
lxml = "5.2.1"
|
lxml = "5.2.1"
|
||||||
lxml-html-clean = "<0.4.5"
|
lxml-html-clean = "^0.4.4"
|
||||||
mako = "^1.4.3"
|
mako = "^1.3.12"
|
||||||
markdown2 = "2.5.1"
|
markdown2 = "2.5.1"
|
||||||
markdownify = "0.13.1"
|
markdownify = "0.13.1"
|
||||||
markupsafe = "2.1.5"
|
markupsafe = "2.1.5"
|
||||||
matplotlib = "^3.11.2"
|
matplotlib = "^3.10.9"
|
||||||
meteostat = "1.7.6"
|
meteostat = "1.6.8"
|
||||||
mock = "^5.2.0"
|
mock = "^5.2.0"
|
||||||
mpld3 = "0.5.10"
|
mpld3 = "0.5.10"
|
||||||
msgraphfs = "^0.5"
|
msgraphfs = "^0.4"
|
||||||
mysqlclient = "^2.3.0"
|
mysqlclient = "^2.2.8"
|
||||||
nextcloud-api-wrapper = "^0.2.3"
|
nextcloud-api-wrapper = "^0.2.3"
|
||||||
num2words = "0.5.13"
|
num2words = "0.5.13"
|
||||||
numpy = ">=1.15"
|
numpy = ">=1.15"
|
||||||
numpy-financial = "<=1.0.0"
|
numpy-financial = "<=1.0.0"
|
||||||
numpy-stl = "^4.0.1"
|
numpy-stl = "^3.2.0"
|
||||||
oauthlib = "^3.3.1"
|
oauthlib = "^3.3.1"
|
||||||
odoo-test-helper = "^2.1.3"
|
|
||||||
odoorpc = "^0.10.1"
|
odoorpc = "^0.10.1"
|
||||||
ofxparse = "0.21"
|
ofxparse = "0.21"
|
||||||
ollama = "^0.6.2"
|
ollama = "^0.6.2"
|
||||||
openai = "2.54.0"
|
openai = "^2.34.0"
|
||||||
openpyxl = "3.1.2"
|
openpyxl = "3.1.2"
|
||||||
openupgradelib = "^3.13.7"
|
openupgradelib = "^3.13.1"
|
||||||
openwebui-client = ">=0.3.0"
|
openwebui-client = ">=0.3.0"
|
||||||
orjson = "^3.12.0"
|
orjson = "^3.11.8"
|
||||||
ovh = "^1.2.0"
|
ovh = "^1.2.0"
|
||||||
pandas = "3.0.6"
|
pandas = "2.3.3"
|
||||||
paramiko = "<4.0.0"
|
paramiko = "<4.0.0"
|
||||||
parse-accept-language = "^0.1.2"
|
parse-accept-language = "^0.1.2"
|
||||||
passlib = "1.7.4"
|
passlib = "1.7.4"
|
||||||
|
|
@ -118,14 +115,14 @@ pathspec = "^1.1.1"
|
||||||
pdf2image = "^1.17.0"
|
pdf2image = "^1.17.0"
|
||||||
pdfminer-six = "20260107"
|
pdfminer-six = "20260107"
|
||||||
pexpect = "^4.9.0"
|
pexpect = "^4.9.0"
|
||||||
phonenumbers = "^9.0.40"
|
phonenumbers = "^9.0.29"
|
||||||
pillow = "12.3.0"
|
pillow = "12.2.0"
|
||||||
plaid-python = "^44.0.0"
|
plaid-python = "^39.2.0"
|
||||||
plotly = "^7.1.0"
|
plotly = "^6.7.0"
|
||||||
polib = "1.1.1"
|
polib = "1.1.1"
|
||||||
pre-commit = "^4.6.2"
|
pre-commit = "^4.6.0"
|
||||||
premailer = "^3.10.0"
|
premailer = "^3.10.0"
|
||||||
prometheus-client = "^0.26.0"
|
prometheus-client = "^0.25.0"
|
||||||
psutil = "5.9.8"
|
psutil = "5.9.8"
|
||||||
psycopg2 = "2.9.9"
|
psycopg2 = "2.9.9"
|
||||||
py3dns = "^4.0.2"
|
py3dns = "^4.0.2"
|
||||||
|
|
@ -136,16 +133,17 @@ pycups = "^2.0.4"
|
||||||
pydantic = ">=2.0.0"
|
pydantic = ">=2.0.0"
|
||||||
pydevd-odoo = "^1.2.2"
|
pydevd-odoo = "^1.2.2"
|
||||||
pyftpdlib = "^2.2.0"
|
pyftpdlib = "^2.2.0"
|
||||||
pygithub = "^2.10.0"
|
pygithub = "^2.9.1"
|
||||||
pyjsparser = "^2.7.1"
|
pyjsparser = "^2.7.1"
|
||||||
pyjwt = "^2.15.0"
|
pyjwt = "^2.12.1"
|
||||||
pylint = "^4.0.9"
|
pylint = "^4.0.5"
|
||||||
pylint-odoo = "^10.0.11"
|
pylint-odoo = "^10.0.2"
|
||||||
|
pymupdf = "^1.27.2.3"
|
||||||
pyncclient = "^0.7"
|
pyncclient = "^0.7"
|
||||||
pyopenssl = "26.4.0"
|
pyopenssl = "26.0.0"
|
||||||
pypdf = ">=3.1.0"
|
pypdf = ">=3.1.0"
|
||||||
pypdf2 = "2.12.1"
|
pypdf2 = "2.12.1"
|
||||||
pyproj = "^3.8.0"
|
pyproj = "^3.7.2"
|
||||||
pyquerystring = "^1.1"
|
pyquerystring = "^1.1"
|
||||||
pyserial = "3.5"
|
pyserial = "3.5"
|
||||||
pysftp = "^0.2.9"
|
pysftp = "^0.2.9"
|
||||||
|
|
@ -154,15 +152,15 @@ python-dateutil = "2.8.2"
|
||||||
python-docx = "^1.2.0"
|
python-docx = "^1.2.0"
|
||||||
python-dotenv = ">=1.0.0"
|
python-dotenv = ">=1.0.0"
|
||||||
python-jose = "^3.5.0"
|
python-jose = "^3.5.0"
|
||||||
python-json-logger = "^4.2.0"
|
python-json-logger = "^4.1.0"
|
||||||
python-ldap = "3.4.4"
|
python-ldap = "3.4.4"
|
||||||
python-multipart = "^0.0.32"
|
python-multipart = "^0.0.27"
|
||||||
python-pptx = "^1.0.2"
|
python-pptx = "^1.0.2"
|
||||||
python-resize-image = "^1.1.20"
|
python-resize-image = "^1.1.20"
|
||||||
python-slugify = "^9.1.1"
|
python-slugify = "^8.0.4"
|
||||||
python-stdnum = "1.19"
|
python-stdnum = "1.19"
|
||||||
python-telegram-bot = "^22.8"
|
python-telegram-bot = "^22.7"
|
||||||
pytz = "^2026.4"
|
pytz = "^2026.2"
|
||||||
pyusb = "1.2.1"
|
pyusb = "1.2.1"
|
||||||
pyyaml = "^6.0.3"
|
pyyaml = "^6.0.3"
|
||||||
pyzbar = "^0.1.9"
|
pyzbar = "^0.1.9"
|
||||||
|
|
@ -170,28 +168,27 @@ pyzk = "^0.9"
|
||||||
qifparse = "^0.5"
|
qifparse = "^0.5"
|
||||||
qrcode = "7.4.2"
|
qrcode = "7.4.2"
|
||||||
radon = "^6.0.1"
|
radon = "^6.0.1"
|
||||||
redis = "^8.1.0"
|
redis = "^7.4.0"
|
||||||
regex = "^2026.9.10"
|
regex = "^2026.4.4"
|
||||||
reportlab = "4.1.0"
|
reportlab = "4.1.0"
|
||||||
requests = "2.32.4"
|
requests = ">=2.31.0"
|
||||||
requests-oauthlib = "^2.0.0"
|
requests-oauthlib = "^2.0.0"
|
||||||
requests-toolbelt = "^1.0.0"
|
requests-toolbelt = "^1.0.0"
|
||||||
responses = "^0.26.3"
|
responses = "^0.26.0"
|
||||||
rjsmin = "1.2.0"
|
rjsmin = "1.2.0"
|
||||||
roulier = "^1.1.1"
|
roulier = "^1.1.1"
|
||||||
s3fs = "2026.9.0"
|
s3fs = "2026.4.0"
|
||||||
schwifty = "2024.4.0"
|
schwifty = "2024.4.0"
|
||||||
sentry-sdk = ">=2.0.0,<=2.22.0"
|
sentry-sdk = ">=2.0.0,<=2.22.0"
|
||||||
setuptools = "84.0.0"
|
setuptools = "80.10.2"
|
||||||
shapely = "^2.1.2"
|
shapely = "^2.1.2"
|
||||||
sqlalchemy = "^2.1.0"
|
sqlalchemy = "^2.0.49"
|
||||||
statsd = "^4.0.1"
|
statsd = "^4.0.1"
|
||||||
tldextract = "^5.3.2"
|
tldextract = "^5.3.1"
|
||||||
to-3mf = "^0.1.0"
|
tweepy = "^4.16.0"
|
||||||
tweepy = "^4.17.0"
|
twilio = "^9.10.5"
|
||||||
twilio = "^9.11.1"
|
typing-extensions = "^4.15.0"
|
||||||
typing-extensions = "^4.16.0"
|
ujson = "^5.12.1"
|
||||||
ujson = "^6.0.0"
|
|
||||||
unidecode = "^1.4.0"
|
unidecode = "^1.4.0"
|
||||||
urllib3 = "2.0.7"
|
urllib3 = "2.0.7"
|
||||||
uvloop = "^0.22.1"
|
uvloop = "^0.22.1"
|
||||||
|
|
@ -206,14 +203,6 @@ xlsxwriter = "3.1.9"
|
||||||
xlwt = "1.3.0"
|
xlwt = "1.3.0"
|
||||||
xmltodict = "0.13.0"
|
xmltodict = "0.13.0"
|
||||||
zeep = "4.2.1"
|
zeep = "4.2.1"
|
||||||
zklib = "^0.1.1"
|
|
||||||
[[tool.poetry.dependencies.factur-x]]
|
|
||||||
version = ">=6.8"
|
|
||||||
markers = "platform_machine != \"s390x\""
|
|
||||||
|
|
||||||
[[tool.poetry.dependencies.factur-x]]
|
|
||||||
version = "<4"
|
|
||||||
markers = "platform_machine == \"s390x\""
|
|
||||||
|
|
||||||
[tool.poetry.dependencies.black]
|
[tool.poetry.dependencies.black]
|
||||||
git = "https://github.com/psf/black.git"
|
git = "https://github.com/psf/black.git"
|
||||||
|
|
@ -225,11 +214,7 @@ rev = "update_imghdr_python3"
|
||||||
|
|
||||||
[tool.poetry.dependencies.fsspec]
|
[tool.poetry.dependencies.fsspec]
|
||||||
extras = [ "s3",]
|
extras = [ "s3",]
|
||||||
version = "^2026.9.0"
|
version = "^2026.4.0"
|
||||||
|
|
||||||
[tool.poetry.dependencies.pymupdf]
|
|
||||||
version = ">=1.28.2"
|
|
||||||
markers = "platform_machine != \"s390x\""
|
|
||||||
|
|
||||||
[tool.poetry.dependencies.pysaml2]
|
[tool.poetry.dependencies.pysaml2]
|
||||||
git = "https://github.com/prauscher/pysaml2.git"
|
git = "https://github.com/prauscher/pysaml2.git"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# For OSX
|
# For OSX
|
||||||
cython
|
cython
|
||||||
|
|
||||||
setuptools==84.0.0
|
setuptools==80.10.2
|
||||||
|
|
||||||
# Module voice_to_text
|
# Module voice_to_text
|
||||||
#SpeechRecognition==3.10.0
|
#SpeechRecognition==3.10.0
|
||||||
|
|
@ -16,11 +16,7 @@ setuptools==84.0.0
|
||||||
googletrans-py==4.0.0
|
googletrans-py==4.0.0
|
||||||
#googletrans==4.0.0-rc1
|
#googletrans==4.0.0-rc1
|
||||||
#chardet==3.0.4
|
#chardet==3.0.4
|
||||||
openai==2.54.0
|
openai
|
||||||
idna==3.20
|
|
||||||
# Odoo 18 épingle requests 2.31.0, touchée par CVE-2024-35195 et
|
|
||||||
# CVE-2024-47081, corrigées en 2.32.4. L'épingle d'ERPLibre l'emporte.
|
|
||||||
requests==2.32.4
|
|
||||||
|
|
||||||
# Module infobip_whatsapp_integration
|
# Module infobip_whatsapp_integration
|
||||||
# Ignore it, need another version of requests
|
# Ignore it, need another version of requests
|
||||||
|
|
@ -61,7 +57,7 @@ orjson
|
||||||
#python-dateutil>=2.8.2
|
#python-dateutil>=2.8.2
|
||||||
|
|
||||||
# Extra module
|
# Extra module
|
||||||
meteostat==1.7.6
|
meteostat==1.6.8
|
||||||
|
|
||||||
# Fix build
|
# Fix build
|
||||||
beautifulsoup4==4.13.5
|
beautifulsoup4==4.13.5
|
||||||
|
|
@ -72,42 +68,17 @@ isort==7.0.0
|
||||||
git+https://github.com/mathben/flanker.git@update_imghdr_python3
|
git+https://github.com/mathben/flanker.git@update_imghdr_python3
|
||||||
|
|
||||||
# Fix some OS
|
# Fix some OS
|
||||||
cryptography==50.0.0
|
cryptography==46.0.5
|
||||||
pyopenssl==26.4.0
|
pyopenssl==26.0.0
|
||||||
# This will ignore pyopenssl from pysaml2==7.5.4
|
# This will ignore pyopenssl from pysaml2==7.5.4
|
||||||
git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl
|
git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl
|
||||||
|
|
||||||
# Fix compilation, until pymssql==2.3.9 is release
|
# Fix compilation, until pymssql==2.3.9 is release
|
||||||
#git+https://github.com/pymssql/pymssql.git
|
#git+https://github.com/pymssql/pymssql.git
|
||||||
|
|
||||||
pandas==3.0.6
|
pandas==2.3.3
|
||||||
pillow==12.3.0
|
pillow==12.2.0
|
||||||
# s3fs exige fsspec à sa propre version exacte, alors qu'OCA_storage amène
|
s3fs==2026.4.0
|
||||||
# « fsspec[s3] » sans borne : laissé libre, fsspec prendrait la dernière version
|
aiobotocore==3.1.2
|
||||||
# publiée et casserait la résolution à chaque sortie. Les deux sont épinglés à
|
botocore==1.42.42
|
||||||
# la même version et se montent ensemble. aiobotocore doit rester dans la plage
|
boto3==1.42.42
|
||||||
# exigée par s3fs (>=2.19.0,<4.0.0).
|
|
||||||
s3fs==2026.9.0
|
|
||||||
fsspec==2026.9.0
|
|
||||||
aiobotocore==3.9.1
|
|
||||||
botocore==1.43.75
|
|
||||||
boto3==1.43.75
|
|
||||||
|
|
||||||
# factur-x >= 4.0 dépend de saxonche, qui encapsule SaxonC. Saxonica ne publie
|
|
||||||
# de roue que pour x86_64, aarch64, macOS et Windows — aucune pour s390x, et
|
|
||||||
# aucune distribution source : rien à installer, rien à compiler. La cause est
|
|
||||||
# en amont, GraalVM Native Image ne ciblant pas s390x.
|
|
||||||
# La 3.x n'exige que lxml et pypdf. On y retombe là où saxonche n'existe pas,
|
|
||||||
# au prix de la validation Schematron.
|
|
||||||
factur-x>=6.8 ; platform_machine != 's390x'
|
|
||||||
factur-x<4 ; platform_machine == 's390x'
|
|
||||||
|
|
||||||
# PyMuPDF (OCA_edi) : MuPDF ne se construit pas sur s390x. On l'y écarte, au
|
|
||||||
# prix des fonctions PDF qui en dépendent sur cette architecture.
|
|
||||||
pymupdf>=1.28.2 ; platform_machine != 's390x'
|
|
||||||
|
|
||||||
# lxml-html-clean 0.4.5 (2026-05-20) exige lxml>=6.1.1, alors qu'Odoo 18 épingle
|
|
||||||
# lxml==5.2.1 : le solveur poetry n'a plus de solution. Les versions jusqu'à
|
|
||||||
# 0.4.4 n'imposent aucune borne sur lxml — vérifié sur PyPI, release par
|
|
||||||
# release. À lever le jour où lxml montera.
|
|
||||||
lxml-html-clean<0.4.5
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# Lecteurs de fichiers externes pour « TODO › Execute › Transform data ».
|
|
||||||
#
|
|
||||||
# Bornes PROPRES à ce venv et indépendantes de .odoo-version : chacune
|
|
||||||
# existe pour ce que la bibliothèque fait ici, pas parce que
|
|
||||||
# pyproject.toml la déclare. Changer de version d'Odoo ne doit pas
|
|
||||||
# déplacer ce fichier, ni l'inverse.
|
|
||||||
#
|
|
||||||
# La borne openpyxl est EXACTE et non minimale : le nettoyage hors
|
|
||||||
# cellules touche _external_links, _charts, _pivots, _images et
|
|
||||||
# conditional_formatting._cf_rules, qui n'ont pas d'équivalent public en
|
|
||||||
# 3.1.2. Le test de fuite de test_transform_external.py est ce qui
|
|
||||||
# surveille cette borne à la montée de version.
|
|
||||||
openpyxl==3.1.5
|
|
||||||
xlsxwriter==3.2.9
|
|
||||||
xlrd==2.0.1
|
|
||||||
access-parser==0.0.6
|
|
||||||
chardet==5.2.0
|
|
||||||
defusedxml==0.7.1
|
|
||||||
# SANS Pillow, openpyxl retire xl/media/ en silence et ws._images reste
|
|
||||||
# toujours vide : les images disparaissent de la copie sans que rien ne
|
|
||||||
# puisse les compter ni l'annoncer. Il les rend VISIBLES au moteur, qui
|
|
||||||
# les efface ensuite.
|
|
||||||
Pillow==12.3.0
|
|
||||||
# 1404 mots français, hors réseau, un seul fichier. Le dictionnaire de la
|
|
||||||
# règle du texte ; import « randomwordfr », pas le nom du paquet. Déjà
|
|
||||||
# dans requirement/erplibre_require-ments.txt, donc .venv.erplibre l'a —
|
|
||||||
# ce venv-ci en a besoin pour son propre compte.
|
|
||||||
python-randomword-fr==1.0.0
|
|
||||||
85
run.sh
85
run.sh
|
|
@ -3,13 +3,6 @@ Red='\033[0;31m' # Red
|
||||||
Color_Off='\033[0m' # Text Reset
|
Color_Off='\033[0m' # Text Reset
|
||||||
|
|
||||||
# Search by default local configuration
|
# Search by default local configuration
|
||||||
# NixOS : meme geste qu'en tete d'env_var.sh, et pour la meme raison -- ce
|
|
||||||
# fichier est lance par l'unite systemd, qui ne source pas env_var.sh, et
|
|
||||||
# Odoo importe psycopg2 au demarrage.
|
|
||||||
if [ -n "${NIX_LD_LIBRARY_PATH:-}" ]; then
|
|
||||||
export LD_LIBRARY_PATH="${NIX_LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CONFIG_PATH="./config.conf"
|
CONFIG_PATH="./config.conf"
|
||||||
ORIGIN_CONFIG_PATH=CONFIG_PATH
|
ORIGIN_CONFIG_PATH=CONFIG_PATH
|
||||||
if [ ! -f "${CONFIG_PATH}" ]; then
|
if [ ! -f "${CONFIG_PATH}" ]; then
|
||||||
|
|
@ -20,84 +13,10 @@ if [ ! -f "${CONFIG_PATH}" ]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Deux options qui appartiennent à ERPLibre, retirées avant de passer la
|
|
||||||
# main : Odoo ne les connaît pas et mourrait sur « no such option ».
|
|
||||||
# --auto-erplibre arme le choix de la base à démarrer
|
|
||||||
# --no-cli-erplibre interdit le menu, sans interdire le choix
|
|
||||||
# --erplibre-disable-warmup-http n'envoie pas la requête de réveil
|
|
||||||
# Sans AUCUN argument, le choix s'arme de lui-même : c'est « make run »,
|
|
||||||
# quelqu'un devant son terminal. Ce défaut-là reste timide — il exige un
|
|
||||||
# terminal des deux côtés — parce que systemd lance lui aussi run.sh sans
|
|
||||||
# argument, avec Restart=always.
|
|
||||||
EL_ARGS=()
|
|
||||||
EL_AUTO=0
|
|
||||||
EL_AUTO_EXPLICITE=0
|
|
||||||
EL_NO_CLI=0
|
|
||||||
EL_WARMUP=1
|
|
||||||
[ $# -eq 0 ] && EL_AUTO=1
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--auto-erplibre)
|
|
||||||
EL_AUTO=1
|
|
||||||
EL_AUTO_EXPLICITE=1
|
|
||||||
;;
|
|
||||||
--no-cli-erplibre) EL_NO_CLI=1 ;;
|
|
||||||
--erplibre-disable-warmup-http) EL_WARMUP=0 ;;
|
|
||||||
# Rien à réveiller si personne n'écoute. On les laisse passer à Odoo,
|
|
||||||
# on se contente de ne pas sonder.
|
|
||||||
--no-http|--stop-after-init)
|
|
||||||
EL_WARMUP=0
|
|
||||||
EL_ARGS+=("$1")
|
|
||||||
;;
|
|
||||||
# Tout le reste passe tel quel, y compris les arguments vides et ceux
|
|
||||||
# qui portent des espaces : un tableau, jamais une chaîne reconstruite.
|
|
||||||
*) EL_ARGS+=("$1") ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
EL_DB=()
|
|
||||||
EL_LIB="./script/database/lib_db_select.sh"
|
|
||||||
if [ "${EL_AUTO}" = "1" ] && [ -f "${EL_LIB}" ]; then
|
|
||||||
# shellcheck source=script/database/lib_db_select.sh
|
|
||||||
. "${EL_LIB}"
|
|
||||||
EL_DB_NAME="$(el_db_select "${CONFIG_PATH}" "${EL_NO_CLI}" \
|
|
||||||
"${EL_AUTO_EXPLICITE}" "${EL_ARGS[@]}")"
|
|
||||||
retSelect=$?
|
|
||||||
# 130 : on a renoncé au menu. Ne pas démarrer Odoo pour autant, et ne pas
|
|
||||||
# rendre 1 non plus — run.sh:26 réserve déjà 1 à « Odoo a échoué ».
|
|
||||||
if [ ${retSelect} -eq 130 ]; then
|
|
||||||
exit 130
|
|
||||||
fi
|
|
||||||
if [ -n "${EL_DB_NAME}" ]; then
|
|
||||||
EL_DB=(-d "${EL_DB_NAME}")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Le réveil, EN PARALLÈLE. Odoo ne charge le registre d'une base qu'à la
|
|
||||||
# première requête qui la concerne ; sur une base migrée cela prend des
|
|
||||||
# dizaines de secondes, et c'est la personne qui ouvre la page qui les
|
|
||||||
# attend. La sonde prend ce temps à sa place, se tait, et meurt avec nous.
|
|
||||||
EL_WARMUP_PID=""
|
|
||||||
EL_WARMUP_BIN="./script/odoo/warmup_http.py"
|
|
||||||
if [ "${EL_WARMUP}" = "1" ] && [ "$ODOO_MODE_TEST" != "true" ] \
|
|
||||||
&& [ -f "${EL_WARMUP_BIN}" ]; then
|
|
||||||
EL_WARMUP_DB=()
|
|
||||||
[ -n "${EL_DB_NAME}" ] && EL_WARMUP_DB=(-d "${EL_DB_NAME}")
|
|
||||||
# `--` sépare NOS options de celles d'Odoo : sans lui, un « -c » destiné
|
|
||||||
# à Odoo serait lu comme le nôtre.
|
|
||||||
python3 "${EL_WARMUP_BIN}" -c "${CONFIG_PATH}" "${EL_WARMUP_DB[@]}" \
|
|
||||||
-- "${EL_ARGS[@]}" 2>/dev/null &
|
|
||||||
EL_WARMUP_PID=$!
|
|
||||||
# Elle ne doit pas survivre au serveur qu'elle réveille. Un Ctrl-C sur
|
|
||||||
# run.sh doit tout emporter.
|
|
||||||
trap '[ -n "${EL_WARMUP_PID}" ] && kill "${EL_WARMUP_PID}" 2>/dev/null' EXIT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$ODOO_MODE_TEST" = "true" ]; then
|
if [ "$ODOO_MODE_TEST" = "true" ]; then
|
||||||
./odoo_bin.sh -c "${CONFIG_PATH}" --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-hard=0 --log-level=test --test-enable --no-http --stop-after-init "${EL_DB[@]}" "${EL_ARGS[@]}"
|
./odoo_bin.sh -c "${CONFIG_PATH}" --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-hard=0 --log-level=test --test-enable --no-http --stop-after-init "$@"
|
||||||
else
|
else
|
||||||
./odoo_bin.sh -c "${CONFIG_PATH}" --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-hard=0 "${EL_DB[@]}" "${EL_ARGS[@]}"
|
./odoo_bin.sh -c "${CONFIG_PATH}" --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-hard=0 "$@"
|
||||||
fi
|
fi
|
||||||
# When need more memory RAM for instance by force
|
# When need more memory RAM for instance by force
|
||||||
#python3 ./odoo/odoo-bin -c ${CONFIG_PATH} --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-soft=8589934592 --limit-memory-hard=10737418240 $@
|
#python3 ./odoo/odoo-bin -c ${CONFIG_PATH} --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-soft=8589934592 --limit-memory-hard=10737418240 $@
|
||||||
|
|
|
||||||
|
|
@ -1,315 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
||||||
|
|
||||||
"""Ce qu'un thème laisse derrière lui une fois désinstallé. Lecture seule.
|
|
||||||
|
|
||||||
Décharger un thème retire ses copies de vues et rend au site sa configuration
|
|
||||||
par défaut. Restent des pièces jointes portant son chemin — SCSS compilé,
|
|
||||||
images téléversées dans ses dossiers — et parfois des vues dont la clé le
|
|
||||||
nomme encore.
|
|
||||||
|
|
||||||
Elles ne cassent rien tant que le module est parti : plus personne ne les
|
|
||||||
inclut dans un bundle. Mais elles traversent toutes les migrations suivantes,
|
|
||||||
et l'on finit par tomber sur un `/theme_x/static/...` dont le module n'existe
|
|
||||||
plus nulle part, sans savoir si c'est grave.
|
|
||||||
|
|
||||||
Rien n'est supprimé ici. Le contenu d'une pièce jointe peut être la seule
|
|
||||||
trace d'une personnalisation, et c'est une décision, pas un ménage.
|
|
||||||
|
|
||||||
Codes de sortie : 0 rien à signaler, 1 des restes, 2 l'outil a échoué.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.append(
|
|
||||||
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from script.todo.todo_i18n import t
|
|
||||||
except Exception: # pragma: no cover - repli si i18n indisponible
|
|
||||||
|
|
||||||
def t(key: str) -> str:
|
|
||||||
return key
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from script.todo import auto_ask
|
|
||||||
except Exception: # pragma: no cover - repli si le pilote est absent
|
|
||||||
auto_ask = None
|
|
||||||
|
|
||||||
|
|
||||||
def can_ask():
|
|
||||||
"""Peut-on poser une question ICI ?
|
|
||||||
|
|
||||||
Il faut deux choses, pas une : de quoi LIRE la réponse (stdin sur un
|
|
||||||
terminal) et de quoi MONTRER la question (stdout aussi). Ne tester que
|
|
||||||
stdin laisse poser une invite qui part dans un tube : elle reste en
|
|
||||||
tampon, invisible, pendant que le processus attend — on croit à un
|
|
||||||
blocage et l'on tape Entrée à l'aveugle.
|
|
||||||
"""
|
|
||||||
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
||||||
|
|
||||||
|
|
||||||
def run_psql(database, sql):
|
|
||||||
"""Interroger la base en lecture seule, garantie côté serveur.
|
|
||||||
|
|
||||||
`default_transaction_read_only` est posé par le serveur pour toute la
|
|
||||||
session : ce n'est pas une promesse de l'outil, c'est PostgreSQL qui
|
|
||||||
refusera l'écriture même si le SQL en contenait une.
|
|
||||||
"""
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["PGOPTIONS"] = (
|
|
||||||
"-c default_transaction_read_only=on -c statement_timeout=30000"
|
|
||||||
)
|
|
||||||
env["PSQLRC"] = ""
|
|
||||||
done = subprocess.run(
|
|
||||||
[
|
|
||||||
"psql",
|
|
||||||
"-X",
|
|
||||||
"-w",
|
|
||||||
"-v",
|
|
||||||
"ON_ERROR_STOP=1",
|
|
||||||
"-d",
|
|
||||||
database,
|
|
||||||
"-tAF",
|
|
||||||
"|",
|
|
||||||
"-c",
|
|
||||||
sql,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
env=env,
|
|
||||||
)
|
|
||||||
if done.returncode:
|
|
||||||
raise RuntimeError(done.stderr.strip() or "psql failed")
|
|
||||||
return [line for line in done.stdout.splitlines() if line]
|
|
||||||
|
|
||||||
|
|
||||||
def quote_literal(value):
|
|
||||||
return "'" + value.replace("'", "''") + "'"
|
|
||||||
|
|
||||||
|
|
||||||
def collect(database, theme):
|
|
||||||
"""Pièces jointes et vues qui nomment encore ce thème."""
|
|
||||||
like = quote_literal(f"%/{theme}/%")
|
|
||||||
key_like = quote_literal(f"{theme}.%")
|
|
||||||
attachments = run_psql(
|
|
||||||
database,
|
|
||||||
"SELECT id, COALESCE(url, name), create_date::date FROM ir_attachment"
|
|
||||||
f" WHERE url LIKE {like} ORDER BY id;",
|
|
||||||
)
|
|
||||||
views = run_psql(
|
|
||||||
database,
|
|
||||||
"SELECT id, key, website_id FROM ir_ui_view"
|
|
||||||
f" WHERE key LIKE {key_like} ORDER BY id;",
|
|
||||||
)
|
|
||||||
return attachments, views
|
|
||||||
|
|
||||||
|
|
||||||
def render(theme, attachments, views):
|
|
||||||
if not attachments and not views:
|
|
||||||
return f"✅ -> {t('No leftover for theme')} '{theme}'.\n"
|
|
||||||
lines = []
|
|
||||||
if attachments:
|
|
||||||
lines.append(
|
|
||||||
f"ℹ {len(attachments)} {t('attachment(s) still under')}"
|
|
||||||
f" /{theme}/ :"
|
|
||||||
)
|
|
||||||
for row in attachments[:20]:
|
|
||||||
lines.append(f" - {row}")
|
|
||||||
if len(attachments) > 20:
|
|
||||||
lines.append(f" … {len(attachments) - 20} {t('more')}")
|
|
||||||
if views:
|
|
||||||
lines.append(
|
|
||||||
f"ℹ {len(views)} {t('view(s) whose key still names it')} :"
|
|
||||||
)
|
|
||||||
for row in views[:20]:
|
|
||||||
lines.append(f" - {row}")
|
|
||||||
if len(views) > 20:
|
|
||||||
lines.append(f" … {len(views) - 20} {t('more')}")
|
|
||||||
lines.append(
|
|
||||||
f" {t('Nothing was deleted: their content may be the only trace')}"
|
|
||||||
f" {t('of a customization. Read before removing.')}"
|
|
||||||
)
|
|
||||||
return "\n".join(lines) + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def backup_attachments(database, theme, lst_row, filestore=None):
|
|
||||||
"""Écrire le contenu des pièces jointes AVANT de les supprimer.
|
|
||||||
|
|
||||||
C'est la condition pour pouvoir répondre « efface ». Sans elle, on
|
|
||||||
détruirait ce dont on vient d'écrire qu'il peut être la seule trace
|
|
||||||
d'une personnalisation.
|
|
||||||
"""
|
|
||||||
base = filestore or os.path.join(
|
|
||||||
os.path.expanduser("~"),
|
|
||||||
".local",
|
|
||||||
"share",
|
|
||||||
"Odoo",
|
|
||||||
"filestore",
|
|
||||||
database,
|
|
||||||
)
|
|
||||||
directory = os.path.join(
|
|
||||||
"private", "odoo", "migration", database, "theme_backup", theme
|
|
||||||
)
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
lst_saved = []
|
|
||||||
for row in lst_row:
|
|
||||||
att_id = row.split("|")[0]
|
|
||||||
rows = run_psql(
|
|
||||||
database,
|
|
||||||
"SELECT COALESCE(store_fname, '') FROM ir_attachment"
|
|
||||||
f" WHERE id = {int(att_id)};",
|
|
||||||
)
|
|
||||||
store_fname = rows[0].strip() if rows else ""
|
|
||||||
if not store_fname:
|
|
||||||
continue
|
|
||||||
source = os.path.join(base, store_fname)
|
|
||||||
if not os.path.isfile(source):
|
|
||||||
continue
|
|
||||||
target = os.path.join(
|
|
||||||
directory, f"{att_id}_" + os.path.basename(row.split("|")[1])
|
|
||||||
)
|
|
||||||
with open(source, "rb") as src, open(target, "wb") as dst:
|
|
||||||
dst.write(src.read())
|
|
||||||
lst_saved.append(target)
|
|
||||||
return lst_saved
|
|
||||||
|
|
||||||
|
|
||||||
def delete_attachments(database, lst_row, config_path="./config.conf"):
|
|
||||||
"""Supprimer par le shell d'Odoo, pour qu'il gère aussi le filestore.
|
|
||||||
|
|
||||||
Un DELETE en SQL laisserait les fichiers orphelins et les caches
|
|
||||||
incohérents ; unlink() fait le ménage complet, dans toutes les versions.
|
|
||||||
"""
|
|
||||||
# int(), pas la chaîne du psql : browse(['4457']) fait échouer Odoo sur
|
|
||||||
# « n'a pas les identifiants (('4457',)) et a des identifiants
|
|
||||||
# supplémentaires ((4457,)) » — il compare des chaînes à des entiers.
|
|
||||||
# Mesuré sur une vraie base, l'effacement n'a rien retiré.
|
|
||||||
lst_id = [int(row.split("|")[0]) for row in lst_row]
|
|
||||||
script = (
|
|
||||||
f"env['ir.attachment'].browse({lst_id!r}).unlink()\n"
|
|
||||||
"env.cr.commit()\n"
|
|
||||||
)
|
|
||||||
done = subprocess.run(
|
|
||||||
["./odoo_bin.sh", "shell", "-c", config_path, "-d", database],
|
|
||||||
input=script,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
return done.returncode, done.stdout + done.stderr
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ANSWER = "d"
|
|
||||||
|
|
||||||
|
|
||||||
def prompt(database, theme, attachments, views, config_path, ask=None):
|
|
||||||
"""Effacer ou garder. Effacer par défaut, et la sauvegarde D'ABORD.
|
|
||||||
|
|
||||||
Ce qui rend ce défaut tenable, c'est l'ordre : le contenu part dans un
|
|
||||||
fichier avant que la base ne soit touchée. Sans cette sauvegarde, le
|
|
||||||
défaut aurait dû rester « garder » — on ne fait pas d'une décision
|
|
||||||
irréversible la réponse que l'on obtient en ne répondant pas.
|
|
||||||
"""
|
|
||||||
if not attachments:
|
|
||||||
return False
|
|
||||||
if ask is None:
|
|
||||||
# Lancé à part par la migration : sans lecteur temporisé, cette
|
|
||||||
# question arrêtait net une exécution automatique.
|
|
||||||
ask = (
|
|
||||||
auto_ask.make_ask(DEFAULT_ANSWER)
|
|
||||||
if auto_ask
|
|
||||||
else (lambda prompt="": input(prompt) or DEFAULT_ANSWER)
|
|
||||||
)
|
|
||||||
answer = (
|
|
||||||
ask(
|
|
||||||
f"💬 {t('Delete these leftovers, or keep them?')}"
|
|
||||||
f" ({t('Enter = delete, after saving them')},"
|
|
||||||
f" k = {t('keep')}) : "
|
|
||||||
)
|
|
||||||
.strip()
|
|
||||||
.lower()
|
|
||||||
)
|
|
||||||
if answer != "d":
|
|
||||||
print(f"ℹ -> {t('Kept. Nothing was deleted.')}")
|
|
||||||
return False
|
|
||||||
lst_saved = backup_attachments(database, theme, attachments)
|
|
||||||
print(f"📦 {t('Saved before deleting')} : {len(lst_saved)}")
|
|
||||||
if lst_saved:
|
|
||||||
print(f" {os.path.dirname(lst_saved[0])}")
|
|
||||||
status, output = delete_attachments(database, attachments, config_path)
|
|
||||||
print(output.strip()[-1500:])
|
|
||||||
if status:
|
|
||||||
print(f"❌ {t('Deletion failed, nothing was removed.')}")
|
|
||||||
return False
|
|
||||||
print(f"✅ -> {len(attachments)} {t('attachment(s) deleted.')}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=("List what an uninstalled theme left behind (read-only).")
|
|
||||||
)
|
|
||||||
parser.add_argument("-d", "--database", required=True)
|
|
||||||
parser.add_argument("-t", "--theme", required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--config",
|
|
||||||
default="./config.conf",
|
|
||||||
help="Odoo config used by the shell for --delete",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--delete",
|
|
||||||
action="store_true",
|
|
||||||
help="delete them (WRITES; saves their content first)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--report-only",
|
|
||||||
action="store_true",
|
|
||||||
help="never ask anything, even in front of a terminal",
|
|
||||||
)
|
|
||||||
config = parser.parse_args(argv)
|
|
||||||
try:
|
|
||||||
attachments, views = collect(config.database, config.theme)
|
|
||||||
except RuntimeError as exc:
|
|
||||||
print(f"❌ {exc}")
|
|
||||||
return 2
|
|
||||||
print(render(config.theme, attachments, views))
|
|
||||||
if not attachments and not views:
|
|
||||||
return 0
|
|
||||||
if config.delete:
|
|
||||||
lst_saved = backup_attachments(
|
|
||||||
config.database, config.theme, attachments
|
|
||||||
)
|
|
||||||
print(f"📦 {t('Saved before deleting')} : {len(lst_saved)}")
|
|
||||||
status, output = delete_attachments(
|
|
||||||
config.database, attachments, config.config
|
|
||||||
)
|
|
||||||
print(output.strip()[-1500:])
|
|
||||||
if status:
|
|
||||||
print(f"❌ {t('Deletion failed, nothing was removed.')}")
|
|
||||||
return 2
|
|
||||||
print(f"✅ -> {len(attachments)} {t('attachment(s) deleted.')}")
|
|
||||||
return 0
|
|
||||||
# Voir ET pouvoir répondre : une invite dont la sortie part dans un
|
|
||||||
# tube reste invisible — Python bufferise par blocs — pendant que le
|
|
||||||
# processus attend. Mesuré : l'utilisateur tape Entrée à l'aveugle.
|
|
||||||
if not config.report_only and can_ask():
|
|
||||||
if prompt(
|
|
||||||
config.database,
|
|
||||||
config.theme,
|
|
||||||
attachments,
|
|
||||||
views,
|
|
||||||
config.config,
|
|
||||||
):
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Désinstaller un thème comme Odoo le fait lui-même.
|
|
||||||
#
|
|
||||||
# « ./run.sh --uninstall theme_x » retire le MODULE, pas le THÈME. Choisir un
|
|
||||||
# thème (button_choose_theme, ce que pose --install-theme) fait DEUX choses :
|
|
||||||
# copier ses vues et ressources dans chaque site, et écrire dans
|
|
||||||
# user_values.scss une personnalisation qui DÉFINIT $o-theme-font-number et
|
|
||||||
# ses trois voisines. Le chemin de retrait d'Odoo, _theme_remove(), défait les
|
|
||||||
# deux.
|
|
||||||
#
|
|
||||||
# Le désinstaller sans lui laisse donc les copies, et surtout n'écrit jamais
|
|
||||||
# ces définitions. Mesuré sur une migration réelle 12 -> 13 : le bundle
|
|
||||||
# web.assets_frontend s'arrête sur « Undefined variable: $o-theme-font-number
|
|
||||||
# ». La variable venait des fichiers option_font_body_* d'Odoo 12, supprimés
|
|
||||||
# en 13.0 ; seul le thème la redéfinissait encore, et le retirer a mis à nu
|
|
||||||
# une personnalisation figée depuis des années.
|
|
||||||
#
|
|
||||||
# Usage : ./script/addons/uninstall_addons_theme.sh <base> <theme> [config]
|
|
||||||
|
|
||||||
Red='\033[0;31m' # Red
|
|
||||||
Color_Off='\033[0m' # Text Reset
|
|
||||||
|
|
||||||
if [[ $# -lt 2 ]]; then
|
|
||||||
echo "Usage: $0 <database> <theme_module> [config]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DATABASE="$1"
|
|
||||||
THEME="$2"
|
|
||||||
CONFIG="${3:-./config.conf}"
|
|
||||||
|
|
||||||
if [[ $# -eq 3 ]]; then
|
|
||||||
./script/addons/check_addons_exist.py -m "$THEME" -c "$3"
|
|
||||||
else
|
|
||||||
./script/addons/check_addons_exist.py -m "$THEME"
|
|
||||||
fi
|
|
||||||
retVal=$?
|
|
||||||
if [[ $retVal -ne 0 ]]; then
|
|
||||||
echo -e "${Red}Error${Color_Off} check_addons_exist.py into uninstall_addons_theme.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Unload theme '$THEME' from every website of BD '$DATABASE'"
|
|
||||||
|
|
||||||
# Le déchargement passe par le shell : _theme_remove() n'a pas d'option de
|
|
||||||
# ligne de commande, et l'écrire ici évite d'ajouter une option au fork pour
|
|
||||||
# chaque version d'Odoo. Les journaux se mêlent à la sortie, d'où la
|
|
||||||
# sentinelle : on ne conclut que sur ce qui la suit.
|
|
||||||
./odoo_bin.sh shell -c "$CONFIG" -d "$DATABASE" --no-http --log-level=warn <<PYTHON
|
|
||||||
theme_name = "$THEME"
|
|
||||||
Module = env["ir.module.module"]
|
|
||||||
theme = Module.search([("name", "=", theme_name)], limit=1)
|
|
||||||
if not theme:
|
|
||||||
print("ERPLIBRE_THEME_UNLOAD: unknown %s" % theme_name)
|
|
||||||
else:
|
|
||||||
lst_website = env["website"].search([])
|
|
||||||
for website in lst_website:
|
|
||||||
# _theme_remove décharge le thème COURANT du site et, avant tout,
|
|
||||||
# rappelle _reset_default_config() : c'est cet appel qui écrit
|
|
||||||
# font-number & co. dans user_values.scss. Il vaut même quand le
|
|
||||||
# site n'a plus de thème — c'est précisément le cas à réparer.
|
|
||||||
theme.with_context(website_id=website.id)._theme_remove(website)
|
|
||||||
env.cr.commit()
|
|
||||||
print("ERPLIBRE_THEME_UNLOAD: done %s website(s)" % len(lst_website))
|
|
||||||
PYTHON
|
|
||||||
|
|
||||||
retVal=$?
|
|
||||||
if [[ $retVal -ne 0 ]]; then
|
|
||||||
echo -e "${Red}Error${Color_Off} odoo_bin.sh shell into uninstall_addons_theme.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Uninstall theme module '$THEME' on BD '$DATABASE'"
|
|
||||||
|
|
||||||
if [[ $# -eq 3 ]]; then
|
|
||||||
./run.sh --no-http --stop-after-init -d "$DATABASE" --uninstall "$THEME" -c "$3"
|
|
||||||
else
|
|
||||||
./run.sh --no-http --stop-after-init -d "$DATABASE" --uninstall "$THEME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
retVal=$?
|
|
||||||
if [[ $retVal -ne 0 ]]; then
|
|
||||||
echo -e "${Red}Error${Color_Off} run.sh into uninstall_addons_theme.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ce que le déchargement ne prend pas : les pièces jointes que le thème a
|
|
||||||
# laissées sous son propre chemin. Elles ne cassent rien tant que ses vues
|
|
||||||
# sont parties, mais elles survivent à toutes les migrations suivantes et
|
|
||||||
# personne ne sait plus d'où elles viennent. On les signale, on ne les
|
|
||||||
# supprime pas : leur contenu peut être la seule trace d'une personnalisation.
|
|
||||||
# Son code de sortie dit « il reste des choses », pas « la désinstallation a
|
|
||||||
# échoué » : le laisser devenir celui du script faisait annoncer une erreur
|
|
||||||
# sur un thème correctement retiré.
|
|
||||||
./script/addons/theme_leftover.py -d "$DATABASE" -t "$THEME" -c "$CONFIG" || true
|
|
||||||
exit 0
|
|
||||||
|
|
@ -5,7 +5,7 @@ Color_Off='\033[0m' # Text Reset
|
||||||
# This script will remove mail configuration, remove backup configuration, and force admin user to test/test
|
# This script will remove mail configuration, remove backup configuration, and force admin user to test/test
|
||||||
echo "Update prod to dev on BD '$1'"
|
echo "Update prod to dev on BD '$1'"
|
||||||
|
|
||||||
./script/addons/install_addons_dev.sh "$1" user_test,disable_mail_server,disable_auto_backup,disable_payment_provider
|
./script/addons/install_addons_dev.sh "$1" user_test,disable_mail_server,disable_auto_backup
|
||||||
|
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [[ $retVal -ne 0 ]]; then
|
if [[ $retVal -ne 0 ]]; then
|
||||||
|
|
@ -15,7 +15,7 @@ fi
|
||||||
|
|
||||||
echo "Update trace of prod to dev on BD '$1'"
|
echo "Update trace of prod to dev on BD '$1'"
|
||||||
|
|
||||||
./script/addons/uninstall_addons.sh "$1" user_test,disable_mail_server,disable_auto_backup,disable_payment_provider
|
./script/addons/uninstall_addons.sh "$1" user_test,disable_mail_server,disable_auto_backup
|
||||||
|
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [[ $retVal -ne 0 ]]; then
|
if [[ $retVal -ne 0 ]]; then
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue