From b7d68cd199b150569d6c390f768c8b5dd5ae2820 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 27 Aug 2026 16:29:00 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20odoo=5Fbin=20:=20lire=20le=20config.con?= =?UTF-8?q?f=20du=20d=C3=A9p=C3=B4t,=20pas=20le=20~/.odoorc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit « odoo_bin.sh db --drop » échoue par AccessDenied à chaque palier de migration, et le clone bute ensuite sur « database already exists ». db_restore.py lit ./config.conf, y voit « admin_passwd = admin » et n'envoie donc aucun mot de passe ; odoo_bin.sh ne passait pas de « -c », donc Odoo lisait ~/.odoorc, qui porte un mot de passe haché. ODOO_RC ferme la couture en un point plutôt qu'à vingt sites d'appel : les versions 12 à 18 le lisent après « -c » et avant ~/.odoorc, donc un choix explicite l'emporte. Vérifié : le drop passe, la précédence est testée. --- EN --- "odoo_bin.sh db --drop" fails with AccessDenied at every migration step, and the clone then hits "database already exists". db_restore.py reads ./config.conf, sees "admin_passwd = admin" and so sends no master password; odoo_bin.sh passed no "-c", so Odoo read ~/.odoorc, which carries a hashed password. ODOO_RC closes the seam in one place instead of twenty call sites: versions 12 to 18 read it after "-c" and before ~/.odoorc, so an explicit choice wins. Verified: the drop succeeds, and a test covers precedence. Assisted-by: Claude Opus 5 (cherry picked from commit 9503d80982b725034513b610b527f29d49025381) --- odoo_bin.sh | 5 ++ script/lib_odoo_rc.sh | 38 +++++++++++++ test/test_odoo_rc.py | 129 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100755 script/lib_odoo_rc.sh create mode 100644 test/test_odoo_rc.py diff --git a/odoo_bin.sh b/odoo_bin.sh index 12aeb29..bd7a53a 100755 --- a/odoo_bin.sh +++ b/odoo_bin.sh @@ -2,6 +2,11 @@ # This is required to change environment for the running Odoo 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)" #export PATH=$ODOO_PATH:$PATH #echo $PATH diff --git a/script/lib_odoo_rc.sh b/script/lib_odoo_rc.sh new file mode 100755 index 0000000..ecd575f --- /dev/null +++ b/script/lib_odoo_rc.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Quel fichier de configuration Odoo doit lire, quand personne ne le dit. +# +# Sans cela, Odoo retombe sur ~/.odoorc — un fichier PERSONNEL, hors du +# dépôt, que rien ne synchronise avec `config.conf`. Mesuré : un ~/.odoorc +# portant un mot de passe maître haché faisait échouer +# « odoo_bin.sh db --drop » par AccessDenied, alors que db_restore.py +# venait de lire `admin_passwd = admin` dans config.conf et d'en conclure +# qu'aucun mot de passe n'était nécessaire. Les deux avaient raison : ils +# ne parlaient pas du même fichier. +# +# La précédence d'Odoo est la même en 12 et en 18 (tools/config.py) : +# +# self.config_file or opt.config or ODOO_RC or OPENERP_SERVER or ~/.odoorc +# +# Poser ODOO_RC ne retire donc rien à personne : un « -c » explicite +# l'emporte toujours, et un ODOO_RC déjà posé n'est pas écrasé. +# +# L'ordre des candidats est celui de db_restore.py, pour que la +# vérification qu'il fait porte sur le fichier qu'Odoo lira vraiment. + +odoo_rc_resolve() { + if [[ -n "${ODOO_RC:-}" ]]; then + return 0 + fi + local racine="${1:-$(pwd)}" + local candidat + for candidat in "${racine}/config.conf" /etc/odoo/odoo.conf; do + if [[ -f "${candidat}" ]]; then + export ODOO_RC="${candidat}" + return 0 + fi + done + return 0 +} diff --git a/test/test_odoo_rc.py b/test/test_odoo_rc.py new file mode 100644 index 0000000..1529ddb --- /dev/null +++ b/test/test_odoo_rc.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +"""Quel fichier de configuration Odoo lit, quand personne ne le dit. + +Sans ODOO_RC, Odoo retombe sur `~/.odoorc` — un fichier PERSONNEL que +rien ne synchronise avec le `config.conf` du dépôt. Mesuré sur une vraie +migration : un `~/.odoorc` portant un mot de passe maître haché faisait +échouer « odoo_bin.sh db --drop » par AccessDenied, alors que +`db_restore.py` venait de lire « admin_passwd = admin » dans config.conf +et d'en conclure qu'aucun mot de passe n'était nécessaire. + +Les deux avaient raison. Ils ne parlaient pas du même fichier — et rien, +nulle part, ne les confrontait. Le drop échouait, le clone butait ensuite +sur « database already exists », et la boucle de reprise faisait passer +les deux pour un accident. Huit fois par migration. +""" + +import io +import os +import subprocess +import unittest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LIB = os.path.join(REPO, "script", "lib_odoo_rc.sh") + + +class Base(unittest.TestCase): + def resoudre(self, racine, environ=None): + """Ce que la résolution pose dans ODOO_RC, pour cette racine.""" + env = dict(os.environ) + env.pop("ODOO_RC", None) + env.update(environ or {}) + done = subprocess.run( + [ + "bash", + "-c", + f'source {LIB!r}; odoo_rc_resolve "$1"; echo "${{ODOO_RC:-}}"', + "bash", + racine, + ], + capture_output=True, + text=True, + env=env, + ) + self.assertEqual(0, done.returncode, done.stderr) + return done.stdout.strip() + + +class TestWhichFileIsChosen(Base): + def setUp(self): + import shutil + import tempfile + + self.dossier = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.dossier) + + def config(self): + chemin = os.path.join(self.dossier, "config.conf") + with io.open(chemin, "w", encoding="utf-8") as handle: + handle.write("[options]\nadmin_passwd = admin\n") + return chemin + + def test_the_repository_config_wins_over_the_personal_one(self): + # C'est tout le propos : ~/.odoorc n'est plus consulté. + self.assertEqual(self.config(), self.resoudre(self.dossier)) + + def test_an_explicit_choice_is_never_overridden(self): + # Odoo lit ODOO_RC avant ~/.odoorc, et « -c » avant ODOO_RC : + # écraser une valeur posée exprès retirerait ce choix. + self.config() + self.assertEqual( + "/ailleurs/odoo.conf", + self.resoudre(self.dossier, {"ODOO_RC": "/ailleurs/odoo.conf"}), + ) + + def test_without_a_repository_config_nothing_is_forced(self): + # Sur une machine sans config.conf ni /etc/odoo/odoo.conf, on + # laisse Odoo faire ce qu'il a toujours fait. + vide = self.resoudre(self.dossier) + self.assertIn(vide, ("", "/etc/odoo/odoo.conf")) + + def test_it_never_fails_the_command_that_sourced_it(self): + # Elle est appelée au tout début d'odoo_bin.sh : y échouer + # empêcherait de lancer Odoo, pour une question de configuration. + done = subprocess.run( + ["bash", "-c", f"source {LIB!r}; odoo_rc_resolve /inexistant"], + capture_output=True, + text=True, + env={k: v for k, v in os.environ.items() if k != "ODOO_RC"}, + ) + self.assertEqual(0, done.returncode, done.stderr) + + +class TestTheEntryPointUsesIt(unittest.TestCase): + def source(self, nom): + with io.open(os.path.join(REPO, nom), encoding="utf-8") as handle: + return handle.read() + + def test_odoo_bin_resolves_before_running_odoo(self): + source = self.source("odoo_bin.sh") + self.assertIn("odoo_rc_resolve", source) + self.assertLess( + source.index("odoo_rc_resolve"), source.index("odoo-bin") + ) + + def test_the_order_matches_what_db_restore_checks(self): + # `db_restore.py` lit ./config.conf puis /etc/odoo/odoo.conf pour + # décider s'il faut un mot de passe maître. Si Odoo en lisait un + # autre, cette vérification ne vérifierait rien. + lib = self.source(os.path.join("script", "lib_odoo_rc.sh")) + restore = self.source( + os.path.join("script", "database", "db_restore.py") + ) + for candidat in ("config.conf", "/etc/odoo/odoo.conf"): + self.assertIn(candidat, lib, candidat) + self.assertIn(candidat, restore, candidat) + self.assertLess( + lib.index("config.conf"), lib.index("/etc/odoo/odoo.conf") + ) + self.assertLess( + restore.index("config.conf"), + restore.index("/etc/odoo/odoo.conf"), + ) + + +if __name__ == "__main__": + unittest.main()