« 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)
129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
#!/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()
|