From 6cc53c172ae752c471be0b83f56865acc608f535 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 25 Aug 2026 21:11:19 +0000 Subject: [PATCH 01/25] Port Odoo appliance to Trixie Replace the expired Odoo 16 backports package with supported Odoo 19 Community from the official daily channel. Bind the repository key and exact package digest, use a least-privileged PostgreSQL role, and keep generated and firstboot credentials out of traces and process arguments. Add a non-mutating update check and focused acceptance for HTTPS admin login, business-record creation and readback, PostgreSQL persistence, database management credentials, and administration components. Exact runtime acceptance remains pending on the known shared runner blocker; syntax and retained signed-package gates pass. --- README.rst | 14 +-- changelog | 15 +++ conf.d/main | 72 ++++++++++--- docs/v19.0-testing.md | 60 +++++++++++ overlay/usr/lib/inithooks/bin/odoo.py | 33 +++--- .../inithooks/firstboot.d/20-odoo-db-secrets | 6 +- overlay/usr/lib/inithooks/firstboot.d/40odoo | 4 +- overlay/usr/local/sbin/odoo-update | 38 +++++++ plan/main | 1 - tests/v19.sh | 102 ++++++++++++++++++ 10 files changed, 302 insertions(+), 43 deletions(-) create mode 100644 docs/v19.0-testing.md create mode 100755 overlay/usr/local/sbin/odoo-update create mode 100755 tests/v19.sh diff --git a/README.rst b/README.rst index c68e98f..c874b22 100644 --- a/README.rst +++ b/README.rst @@ -12,13 +12,15 @@ or paid commercial ones. This appliance includes all the standard features in `TurnKey Core`_: -- Odoo configurations for TurnKey v18.x: +- Odoo configurations for TurnKey v19.x: - - Odoo v16 installed from debian backports apt repo (v18.x). + - Odoo 19 Community installed from Odoo's official package repository. - Includes modules from base install of Odoo. -- **Security note**: As of 18.0, due to using the debian backports repo - Updates to Odoo **ARE NOT** configured to install automatically. +- **Security note**: Odoo application updates require supervision and are not + configured to install automatically. Check the official daily channel with + ``odoo-update --check``, back up the database, then apply the selected APT + update. - SSL support out of the box. - `Adminer`_ administration frontend for PostgreSQL (listening on @@ -31,7 +33,7 @@ To create a new Odoo DB, the Odoo config file needs to be edited and the Odoo service restarted. Overview of process: -1. Edit /etc/odoo/odoo +1. Edit /etc/odoo/odoo.conf - change value of "db_name" from "TurnkeylinuxExample" to your desired DB name 2. Restart odoo.service @@ -52,7 +54,7 @@ Webmin steps: CLI steps: 1. - - Edit /etc/odoo/odoo as per step 1 + - Edit /etc/odoo/odoo.conf as per step 1 2. - Restart odoo.service:: diff --git a/changelog b/changelog index 37dd4e1..d3d65e9 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,18 @@ +turnkey-odoo-19.0 (1) turnkey; urgency=low + + * Install supported Odoo 19 Community from its official daily package + channel with an exact package digest and bound repository key. + + * Use a non-superuser PostgreSQL application role and keep generated + database and firstboot credentials out of process arguments and traces. + + * Add a non-mutating update check, v19 acceptance coverage, and README + evidence crosswalk. + + * Upgrade the base distribution to Debian 13 Trixie. + + -- TurnKey Linux release engineering Tue, 25 Aug 2026 00:00:00 +0000 + turnkey-odoo-18.0 (1) turnkey; urgency=low * Install Odoo v16.x from Debian backports (bookworm-backports) apt diff --git a/conf.d/main b/conf.d/main index 74d377a..0c0ed1d 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,8 +1,55 @@ #!/bin/bash -ex +# Install the supported Odoo Community package from Odoo's official channel. +ODOO_VERSION=19.0.20260825 +ODOO_REPO=https://nightly.odoo.com/19.0/nightly/deb +ODOO_PACKAGE_URL="$ODOO_REPO/odoo_${ODOO_VERSION}_all.deb" +ODOO_PACKAGE_SHA256=e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 +ODOO_KEY_URL=https://nightly.odoo.com/odoo.key +ODOO_KEY_SHA256=1d169c013a727fa00238ba3f6417d5f33f162f8fbae32159a87ecab6966bfe8e +ODOO_KEY_FINGERPRINT=5D134C924CB06330DCEFE2A1DEF2A2198183CBB5 +SOURCE_RECORD=/usr/local/share/turnkey-odoo/source + +key=$(mktemp) +package=$(mktemp --suffix=.deb) +trap 'find "$key" "$package" -maxdepth 0 -type f -delete' EXIT +curl -fsSL "$ODOO_KEY_URL" -o "$key" +echo "$ODOO_KEY_SHA256 $key" | sha256sum -c - +test "$(gpg --show-keys --with-colons "$key" | awk -F: '$1 == "fpr" { print $10; exit }')" = "$ODOO_KEY_FINGERPRINT" +gpg --batch --yes --dearmor --output /usr/share/keyrings/odoo-archive-keyring.gpg "$key" + +cat >/etc/apt/sources.list.d/odoo.list </etc/apt/preferences.d/odoo <"$SOURCE_RECORD" </dev/null && break + sleep 2 done - -URL="https://127.0.0.1/web" -CURL="curl --insecure -c /tmp/cookie -b /tmp/cookie" - -$CURL $URL/login -sleep 1 -$CURL $URL/database/manager -sleep 1 -$CURL $URL/database/change_password --data-raw "master_pwd=admin&master_pwd_new=${ODOO_ADMIN_PASSWORD}" +curl --insecure --fail --silent https://127.0.0.1/web/login >/dev/null systemctl stop odoo systemctl stop postgresql -systemctl stop apache2 +systemctl stop apache2 +set -x diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..c9965af --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,60 @@ +# Odoo v19 acceptance + +## Source decision + +Debian Trixie does not package Odoo. The appliance installs Odoo 19 Community +from Odoo's official daily Debian channel. Odoo 19 receives standard on-premise +support and security updates through September 2028. The build pins the exact +daily package, verifies its SHA-256, and binds the signed repository key used +for later updates. + +The selected evidence is: + +- version `19.0.20260825`; +- repository key fingerprint + `5D134C924CB06330DCEFE2A1DEF2A2198183CBB5`; +- repository key SHA-256 + `1d169c013a727fa00238ba3f6417d5f33f162f8fbae32159a87ecab6966bfe8e`; +- package SHA-256 + `e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69`. + +## README crosswalk + +| Contract | Focused acceptance | Required result | +| --- | --- | --- | +| Example administrator | Authenticate to the Odoo JSON session through Apache HTTPS with `TKL_TEST_APP_PASS` | Odoo identifies the example `admin` user | +| Integrated business records | Create and read a throwaway contact through Odoo's authenticated model API | Its name and email round trip through Odoo | +| PostgreSQL persistence | Read the contact directly, restart PostgreSQL and Odoo, then read it again | The database row survives the service restart | +| Database management password | Verify the configured master password with Odoo's own configuration API | `TKL_TEST_APP_PASS` verifies without exposing the stored hash | +| HTTPS proxy | Validate Apache and use only its HTTPS endpoint for application calls | Apache configuration passes and proxies Odoo normally | +| Adminer and Webmin | Require Adminer plus Apache, PostgreSQL, and Postfix module directories | Appliance-specific administration components are installed | +| Outbound mail | Require the Postfix unit | Postfix is enabled and active; public delivery is outside local acceptance | +| Supervised updates | Run `odoo-update --check` | The installed daily version, signed candidate, and channel are reported without mutation | +| Inherited administration | Cite unchanged SSH, Webmin, and common platform behavior | Core 19 PASS run `20260824t010251z-1634-32241` at source `24c82ee3540ce545422742b0e28ba6b687c53ec2` remains applicable | + +The acceptance fixture deletes its contact through Odoo on exit. + +## Exact acceptance + +```sh +TKLDEV_CONTAINER=tkldev19-wave2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +/sandboxed-git/turnkey/tools/test-v19-appliance odoo \ + --source /home/agent/.local/worktrees/turnkey-apps/odoo/wish-odoo-v19-trixie +``` + +Exact retained acceptance remains pending because the shared runner is missing +its required `rg` command and has exhausted its Docker allocation. This +infrastructure blocker has not exercised the Odoo candidate and costs zero +appliance product loops. + +## Deferred issues + +- The Odoo repository publishes supported branch packages daily. Core updates + remain supervised so administrators can back up PostgreSQL first. +- The packaged `wkhtmltopdf` is retained for report rendering. Header and + footer fidelity is not part of the primary record workflow. +- Public Postfix delivery and optional localization dependencies are not + exercised by local acceptance. +- Docker acceptance does not repeat installer, kernel, or hardware checks. diff --git a/overlay/usr/lib/inithooks/bin/odoo.py b/overlay/usr/lib/inithooks/bin/odoo.py index 76cbf67..10fcd09 100755 --- a/overlay/usr/lib/inithooks/bin/odoo.py +++ b/overlay/usr/lib/inithooks/bin/odoo.py @@ -5,19 +5,14 @@ --pass= unless provided, will ask interactively """ -import re -import sys import getopt - -import crypt -import random -import hashlib -import configparser - +import os import subprocess +import sys + from libinithooks.dialog_wrapper import Dialog from pgsqlconf import PostgreSQL -from passlib.context import CryptContext + def usage(s=None): if s: @@ -26,6 +21,7 @@ def usage(s=None): print(__doc__, file=sys.stderr) sys.exit(1) + def main(): try: opts, args = getopt.gnu_getopt(sys.argv[1:], "h", @@ -33,7 +29,7 @@ def main(): except getopt.GetoptError as e: usage(e) - password = "" + password = os.environ.pop("APP_PASS", "") for opt, val in opts: if opt in ('-h', '--help'): usage() @@ -44,12 +40,15 @@ def main(): if not password: d = Dialog('TurnKey Linux - First boot configuration') password = d.get_password( - "Odoo Database Managment & example 'admin' Password", + "Odoo Database Management & example 'admin' Password", "Enter new password for Odoo Database Management - create/delete/manage Odoo DBs. " "This password will also login to 'admin' account of default/example Odoo.", blacklist=['\\', '/']) - processed_password = CryptContext(['pbkdf2_sha512']).hash(password) + sys.path.insert(0, '/usr/lib/python3/dist-packages') + import odoo + + processed_password = odoo.tools.config.crypt_context.hash(password) default_db = 'TurnkeylinuxExample' default_db_exists = True @@ -57,18 +56,18 @@ def main(): p = PostgreSQL(default_db) p.execute("UPDATE res_users SET password='{}' WHERE id=2".format( processed_password).encode('utf8')) - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError: default_db_exists = False - print(f"Default DB ({default_db}) not found - skipping setting passsword for that") + print(f"Default DB ({default_db}) not found - skipping its password update") - sys.path.insert(0, '/usr/lib/python3/dist-packages') - import odoo odoo.tools.config.parse_config(['--config=/etc/odoo/odoo.conf']) odoo.tools.config.set_admin_password(password) odoo.tools.config.save() + subprocess.run(['chown', 'root:odoo', '/etc/odoo/odoo.conf'], check=True) + subprocess.run(['chmod', '0640', '/etc/odoo/odoo.conf'], check=True) # restart odoo to apply updated password - subprocess.run(['systemctl', 'restart', 'odoo']) + subprocess.run(['systemctl', 'restart', 'odoo'], check=True) if not default_db_exists: sys.exit(1) diff --git a/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets b/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets index 3a3b039..fabd3f2 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets +++ b/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets @@ -12,8 +12,10 @@ DB_PASS=$(mcookie) # Set new password in config file sed -i "s|db_password =.*|db_password = $DB_PASS|" $CONF -# Update the pastgres user password -$INITHOOKS_PATH/bin/pgsqlconf.py --user=$DB_USER --pass="$DB_PASS" +# Update the PostgreSQL role without exposing the generated password in argv. +su postgres -c 'psql --set ON_ERROR_STOP=on postgres' <&2 + exit 2 +fi + +source_record=/usr/local/share/turnkey-odoo/source +keyring=/usr/share/keyrings/odoo-archive-keyring.gpg + +# shellcheck disable=SC1090 +. "$source_record" + +installed=$(dpkg-query -W -f='${Version}' odoo) +candidate=$(apt-cache policy odoo | awk '/Candidate:/ { print $2; exit }') +fingerprint=$(gpg --show-keys --with-colons "$keyring" | + awk -F: '$1 == "fpr" { print $10; exit }') + +test "$installed" = "$installed_version" +test "$fingerprint" = "$repository_key_fingerprint" +test "$candidate" != "(none)" +grep -Fq 'https://nightly.odoo.com/19.0/nightly/deb/ ./' \ + /etc/apt/sources.list.d/odoo.list + +if dpkg --compare-versions "$candidate" eq "$installed"; then + status=up-to-date +else + status=supervised-update-available +fi + +cat < #include -odoo-16 xfonts-75dpi wkhtmltopdf python3-psycogreen diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..c9d10dd --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,102 @@ +#!/bin/bash +set -Eeuo pipefail +umask 077 + +result=${TKL_TEST_RESULT:?TKL_TEST_RESULT is required} +app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} +source_file=/usr/local/share/turnkey-odoo/source +database=TurnkeylinuxExample +fixture="TurnKey v19 contact $(date +%s)-$$" +email="odoo-v19-$$@example.invalid" +work=$(mktemp -d /tmp/odoo-v19.XXXXXXXX) +cookie=$work/cookie +partner_id= + +ocurl() { + curl --insecure --fail --silent --show-error \ + --cookie "$cookie" --cookie-jar "$cookie" \ + --header 'Content-Type: application/json' "$@" +} + +cleanup() { + set +e + if [[ -n $partner_id ]]; then + ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"unlink\",\"args\":[[$partner_id]],\"kwargs\":{}}}" \ + https://127.0.0.1/web/dataset/call_kw/res.partner/unlink >/dev/null + fi + find "$work" -depth -delete +} +trap cleanup EXIT + +for unit in apache2.service postgresql.service odoo.service postfix.service; do + systemctl --quiet is-active "$unit" + systemctl --quiet is-enabled "$unit" +done +apache2ctl configtest +grep -Fxq 'VERSION_CODENAME=trixie' /etc/os-release +grep -Eq '^turnkey-odoo-19\.0' /etc/turnkey_version +test "$(stat -c '%U:%G:%a' /etc/odoo/odoo.conf)" = root:odoo:640 +test -d /usr/share/adminer +for module in apache postgresql postfix; do + test -d "/usr/share/webmin/$module" +done + +# shellcheck disable=SC1090 +. "$source_file" +test "$installed_version" = 19.0.20260825 +test "$(dpkg-query -W -f='${Version}' odoo)" = "$installed_version" +test "$package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 +test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.gpg | + awk -F: '$1 == "fpr" { print $10; exit }')" = \ + "$repository_key_fingerprint" +odoo --version | grep -Fq '19.0' + +auth=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"db\":\"$database\",\"login\":\"admin\",\"password\":\"$app_password\"}}" \ + https://127.0.0.1/web/session/authenticate) +jq -e '.result.uid == 2' <<<"$auth" >/dev/null + +created=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"create\",\"args\":[{\"name\":\"$fixture\",\"email\":\"$email\"}],\"kwargs\":{}}}" \ + https://127.0.0.1/web/dataset/call_kw/res.partner/create) +partner_id=$(jq -er '.result' <<<"$created") +[[ $partner_id =~ ^[0-9]+$ ]] + +readback=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"search_read\",\"args\":[[[\"id\",\"=\",$partner_id]]],\"kwargs\":{\"fields\":[\"id\",\"name\",\"email\"]}}}" \ + https://127.0.0.1/web/dataset/call_kw/res.partner/search_read) +jq -e --arg name "$fixture" --arg email "$email" \ + '.result[0].name == $name and .result[0].email == $email' \ + <<<"$readback" >/dev/null +su postgres -c "psql --tuples-only --no-align '$database'" </dev/null 2>&1 && break + sleep 2 +done +su postgres -c "psql --tuples-only --no-align '$database'" <"$work/update" +candidate=$(sed -n 's/^candidate=//p' "$work/update") +status=$(sed -n 's/^status=//p' "$work/update") +test -n "$candidate" +grep -Fxq 'channel=official-odoo-19-community-daily' "$work/update" +grep -Fxq "integrity=APT-signed-by-$repository_key_fingerprint" "$work/update" + +cat >"$result" < Date: Wed, 26 Aug 2026 10:32:51 +0000 Subject: [PATCH 02/25] Correct Odoo 19 deployment and acceptance Run Odoo with nonzero workers and proxy mode, route WebSocket traffic through Apache to the gevent port, and remove stale Odoo 16 backports controls. Load the build database credential from the protected application config instead of exposing it in the initializer argv. Exercise the supported Odoo database CLI with a throwaway initialized database, require valid authentication and API readback after restart, and replace the obsolete database-creation instructions. Focused shell, Python, diff, secret-path, proxy, and seven-field evidence checks pass; the shared runtime builder remains reserved by Observium. --- Makefile | 3 -- README.rst | 47 +++++------------ changelog | 8 ++- conf.d/main | 11 ++-- docs/v19.0-testing.md | 5 +- overlay/etc/apache2/sites-available/odoo.conf | 7 ++- tests/v19.sh | 52 +++++++++++++++++-- 7 files changed, 83 insertions(+), 50 deletions(-) diff --git a/Makefile b/Makefile index ba971ab..7d62ba6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,4 @@ CREDIT_ANCHORTEXT = Turnkey Odoo Appliance -BACKPORTS=y # install Odoo v16.x from backports -BACKPORTS_PINS=odoo-16 - include $(FAB_PATH)/common/mk/turnkey/lapp.mk include $(FAB_PATH)/common/mk/turnkey.mk diff --git a/README.rst b/README.rst index c874b22..148a8ea 100644 --- a/README.rst +++ b/README.rst @@ -27,40 +27,19 @@ This appliance includes all the standard features in `TurnKey Core`_: port 12322 - uses SSL). - Webmin modules for configuring Apache2, PostgreSQL and Postfix. -**To create a new Odoo Databse (i.e. site)** - -To create a new Odoo DB, the Odoo config file needs to be edited and the -Odoo service restarted. - -Overview of process: -1. Edit /etc/odoo/odoo.conf - - change value of "db_name" from "TurnkeylinuxExample" to your desired DB - name -2. Restart odoo.service -3. Reload Odoo login screen in your browser and you should be good to go - -Webmin steps: -1. - - Browse to 'Tools' >> 'FileManager' - - Navigate to '/etc/odoo' and edit the 'odoo.conf' file - - Edit as per step 1 above - - Save changes -2. - - Navigate to 'System' >> 'Bootup and Shutdown' - - Find the "odoo.service" in the list and select it via the checkbox - - Scroll to the bottom and select 'Restart' -3. - - As per step 3 above - -CLI steps: -1. - - Edit /etc/odoo/odoo.conf as per step 1 -2. - - Restart odoo.service:: - - systemctl restart odoo -3. - - As per step 3 above +Creating another Odoo database +------------------------------ + +Use Odoo 19's database command to create and initialize the database:: + + runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init example + +Add the new name to the comma-separated ``db_name`` setting in +``/etc/odoo/odoo.conf``, then restart Odoo:: + + systemctl restart odoo + +The database command also provides ``drop``, ``dump``, and ``load`` operations. **Notice for special Odoo Localization** diff --git a/changelog b/changelog index d3d65e9..d8aedc9 100644 --- a/changelog +++ b/changelog @@ -3,8 +3,12 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low * Install supported Odoo 19 Community from its official daily package channel with an exact package digest and bound repository key. - * Use a non-superuser PostgreSQL application role and keep generated - database and firstboot credentials out of process arguments and traces. + * Use a non-superuser PostgreSQL application role, load its generated + credential from the protected Odoo config during initialization, and keep + firstboot credentials out of process arguments and traces. + + * Configure Odoo's multiprocess proxy mode and route WebSocket traffic to + its gevent worker. * Add a non-mutating update check, v19 acceptance coverage, and README evidence crosswalk. diff --git a/conf.d/main b/conf.d/main index 0c0ed1d..3e5a662 100755 --- a/conf.d/main +++ b/conf.d/main @@ -82,7 +82,13 @@ EOF # Set password/name in config file sed -i "s|db_password =.*|db_password = $DB_PASS|" $CONF sed -i "s|db_host =.*|db_host = localhost|" $CONF -echo "db_name = $DB_NAME" >> $CONF +cat >>$CONF </dev/null + fi if [[ -n $partner_id ]]; then ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"unlink\",\"args\":[[$partner_id]],\"kwargs\":{}}}" \ https://127.0.0.1/web/dataset/call_kw/res.partner/unlink >/dev/null @@ -33,6 +40,8 @@ for unit in apache2.service postgresql.service odoo.service postfix.service; do systemctl --quiet is-enabled "$unit" done apache2ctl configtest +grep -Fq 'ProxyPass /websocket ws://127.0.0.1:8072/websocket' \ + /etc/apache2/sites-enabled/odoo.conf grep -Fxq 'VERSION_CODENAME=trixie' /etc/os-release grep -Eq '^turnkey-odoo-19\.0' /etc/turnkey_version test "$(stat -c '%U:%G:%a' /etc/odoo/odoo.conf)" = root:odoo:640 @@ -50,6 +59,32 @@ test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.g awk -F: '$1 == "fpr" { print $10; exit }')" = \ "$repository_key_fingerprint" odoo --version | grep -Fq '19.0' +runuser -u odoo -- python3 - <<'PY' +import odoo + +odoo.tools.config.parse_config(['--config=/etc/odoo/odoo.conf']) +assert odoo.tools.config['proxy_mode'] is True +assert odoo.tools.config['workers'] > 0 +assert odoo.tools.config['gevent_port'] == 8072 +PY +ss -ltn | awk '$4 ~ /:8072$/ { found = 1 } END { exit !found }' + +test_database_created=1 +runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init "$test_database" +database_evidence=$(su postgres -c \ + "psql --tuples-only --no-align '$test_database'" <<'EOF' +SELECT name || ':' || state FROM ir_module_module WHERE name = 'base'; +SELECT login FROM res_users WHERE id = 2; +EOF +) +grep -Fxq 'base:installed' <<<"$database_evidence" +grep -Fxq 'admin' <<<"$database_evidence" +runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf drop "$test_database" +test_database_created= +test "$(su postgres -c "psql --tuples-only --no-align postgres" </dev/null 2>&1 && break + if candidate_auth=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"db\":\"$database\",\"login\":\"admin\",\"password\":\"$app_password\"}}" \ + https://127.0.0.1/web/session/authenticate 2>/dev/null) && \ + jq -e '.result.uid == 2' <<<"$candidate_auth" >/dev/null; then + restart_auth=$candidate_auth + break + fi sleep 2 done +test -n "$restart_auth" +restart_readback=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"search_read\",\"args\":[[[\"id\",\"=\",$partner_id]]],\"kwargs\":{\"fields\":[\"id\",\"name\",\"email\"]}}}" \ + https://127.0.0.1/web/dataset/call_kw/res.partner/search_read) +jq -e --arg name "$fixture" --arg email "$email" \ + '.result[0].name == $name and .result[0].email == $email' \ + <<<"$restart_readback" >/dev/null su postgres -c "psql --tuples-only --no-align '$database'" <"$result" < Date: Wed, 26 Aug 2026 10:55:03 +0000 Subject: [PATCH 03/25] Protect Odoo database initialization credentials Require every documented and acceptance-created Odoo database to receive a non-default administrator password, then authenticate the throwaway fixture with Odoo's own API before deletion. Route generated database and master-password config updates through a stdin-only helper so neither secret is exposed in process arguments. Preserve special characters exactly and narrow the changelog claim to this verified boundary. Verified with the focused live-argv and config replacement fixture, Bash syntax checks, Python AST parsing, and git diff validation. --- README.rst | 11 +++- changelog | 3 +- conf.d/main | 10 ++-- docs/v19.0-testing.md | 8 ++- .../inithooks/firstboot.d/20-odoo-db-secrets | 4 +- .../usr/local/sbin/odoo-set-config-secrets | 32 ++++++++++ tests/config-secrets.sh | 60 +++++++++++++++++++ tests/v19.sh | 24 +++++++- 8 files changed, 139 insertions(+), 13 deletions(-) create mode 100755 overlay/usr/local/sbin/odoo-set-config-secrets create mode 100755 tests/config-secrets.sh diff --git a/README.rst b/README.rst index 148a8ea..1c1828f 100644 --- a/README.rst +++ b/README.rst @@ -30,9 +30,16 @@ This appliance includes all the standard features in `TurnKey Core`_: Creating another Odoo database ------------------------------ -Use Odoo 19's database command to create and initialize the database:: +Choose a non-default password for the new database administrator, then use +Odoo 19's database command to create and initialize the database:: - runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init example + read -rsp 'New Odoo database administrator password: ' ODOO_DB_ADMIN_PASSWORD + printf '\n' + runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init example \ + --password "$ODOO_DB_ADMIN_PASSWORD" + unset ODOO_DB_ADMIN_PASSWORD + +Do not use Odoo's default ``admin`` password. Add the new name to the comma-separated ``db_name`` setting in ``/etc/odoo/odoo.conf``, then restart Odoo:: diff --git a/changelog b/changelog index d8aedc9..5991275 100644 --- a/changelog +++ b/changelog @@ -5,7 +5,8 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low * Use a non-superuser PostgreSQL application role, load its generated credential from the protected Odoo config during initialization, and keep - firstboot credentials out of process arguments and traces. + generated database and master passwords out of configuration-update + process arguments. * Configure Odoo's multiprocess proxy mode and route WebSocket traffic to its gevent worker. diff --git a/conf.d/main b/conf.d/main index 3e5a662..740e60f 100755 --- a/conf.d/main +++ b/conf.d/main @@ -79,8 +79,12 @@ su postgres -c "psql postgres" << EOF create database "$DB_NAME" owner $DB_USER; EOF -# Set password/name in config file -sed -i "s|db_password =.*|db_password = $DB_PASS|" $CONF +# Set passwords without placing either generated secret in process arguments. +printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$DB_PASS" "$ODOO_ADMIN_PASSWORD" | + odoo-set-config-secrets "$CONF" + +# Set database name and runtime options in config file. sed -i "s|db_host =.*|db_host = localhost|" $CONF cat >>$CONF <"$config" <<'EOF' +[options] +db_password = old database secret +admin_passwd = old master secret +db_host = localhost +EOF +mkfifo "$input" + +# Hold the FIFO's write end open so the process can be inspected while it +# waits for records. +"$helper" "$config" <"$input" & +helper_pid=$! +exec 3>"$input" +for _ in {1..50}; do + if tr '\0' '\n' <"/proc/$helper_pid/cmdline" | grep -Fxq "$helper"; then + break + fi + sleep 0.01 +done +argv=$(tr '\0' '\n' <"/proc/$helper_pid/cmdline") +grep -Fxq "$helper" <<<"$argv" +! grep -Fq "$db_secret" <<<"$argv" +! grep -Fq "$master_secret" <<<"$argv" + +printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$db_secret" "$master_secret" >&3 +exec 3>&- +wait "$helper_pid" + +grep -Fxq "db_password = $db_secret" "$config" +grep -Fxq "admin_passwd = $master_secret" "$config" +grep -Fxq 'db_host = localhost' "$config" + +python3 - "$helper" <<'PY' +import ast +import pathlib +import sys + +ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) +PY + +bash -n "$root/conf.d/main" +bash -n "$root/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets" +bash -n "$root/tests/v19.sh" +! grep -E 'sed .*\$(DB_PASS|ODOO_ADMIN_PASSWORD)' \ + "$root/conf.d/main" \ + "$root/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets" +git -C "$root" diff --check diff --git a/tests/v19.sh b/tests/v19.sh index 8df986c..c9f6b22 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -7,6 +7,7 @@ app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} source_file=/usr/local/share/turnkey-odoo/source database=TurnkeylinuxExample test_database=turnkey_v19_acceptance_$$ +test_admin_password=$(mcookie) fixture="TurnKey v19 contact $(date +%s)-$$" email="odoo-v19-$$@example.invalid" work=$(mktemp -d /tmp/odoo-v19.XXXXXXXX) @@ -70,7 +71,9 @@ PY ss -ltn | awk '$4 ~ /:8072$/ { found = 1 } END { exit !found }' test_database_created=1 -runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init "$test_database" +test "$test_admin_password" != admin +runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init \ + "$test_database" --password "$test_admin_password" database_evidence=$(su postgres -c \ "psql --tuples-only --no-align '$test_database'" <<'EOF' SELECT name || ':' || state FROM ir_module_module WHERE name = 'base'; @@ -79,6 +82,25 @@ EOF ) grep -Fxq 'base:installed' <<<"$database_evidence" grep -Fxq 'admin' <<<"$database_evidence" +printf '%s\n' "$test_admin_password" | runuser -u odoo -- \ + python3 -c ' +import sys + +from odoo import SUPERUSER_ID, api +from odoo.modules.registry import Registry +from odoo.tools import config + +database = sys.argv[1] +password = sys.stdin.readline().rstrip("\n") +config.parse_config(["--config=/etc/odoo/odoo.conf"]) +with Registry(database).cursor() as cursor: + environment = api.Environment(cursor, SUPERUSER_ID, {}) + authentication = environment["res.users"].authenticate( + {"login": "admin", "password": password, "type": "password"}, + {"interactive": False}, + ) + assert authentication["uid"] == 2 +' "$test_database" runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf drop "$test_database" test_database_created= test "$(su postgres -c "psql --tuples-only --no-align postgres" < Date: Wed, 26 Aug 2026 12:07:49 +0000 Subject: [PATCH 04/25] Remove unavailable Odoo PDF renderer packages Debian Trixie no longer provides wkhtmltopdf, so the legacy plan entry prevents the appliance root from building. Odoo 19 does not declare it as a package dependency; remove it and its X font companion while recording the resulting PDF-report limitation. --- docs/v19.0-testing.md | 5 +++-- plan/main | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index c3704f7..d6220c8 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -58,8 +58,9 @@ appliance product loops. - The Odoo repository publishes supported branch packages daily. Core updates remain supervised so administrators can back up PostgreSQL first. -- The packaged `wkhtmltopdf` is retained for report rendering. Header and - footer fidelity is not part of the primary record workflow. +- PDF report rendering that requires `wkhtmltopdf` is not exercised because + Debian Trixie no longer provides that package. It is outside the primary + business-record workflow. - Public Postfix delivery and optional localization dependencies are not exercised by local acceptance. - Docker acceptance does not repeat installer, kernel, or hardware checks. diff --git a/plan/main b/plan/main index bcafa1d..8a9752f 100644 --- a/plan/main +++ b/plan/main @@ -1,8 +1,6 @@ #include #include -xfonts-75dpi -wkhtmltopdf python3-psycogreen adduser From 121776f087b17622b7477a121ae465a0a6e3209a Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 12:22:49 +0000 Subject: [PATCH 05/25] Record blocked Odoo acceptance evidence Replace the stale infrastructure caveat with the exact retained build failure. Record the official package dependency missing from Trixie, evidence hashes, successful cleanup checks, exhausted product fix budget, and the bounded decision required to resume. --- docs/v19.0-testing.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index d6220c8..6936c70 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -49,10 +49,30 @@ TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.l --source /home/agent/.local/worktrees/turnkey-apps/odoo/wish-odoo-v19-trixie ``` -Exact retained acceptance remains pending because the shared runner is missing -its required `rg` command and has exhausted its Docker allocation. This -infrastructure blocker has not exercised the Odoo candidate and costs zero -appliance product loops. +Run `20260826t120759z-5128-9592` exercised source +`662e047d690c5207d53518c2ff1a9941649066da`. Source staging and the HTTPS +preflight passed, but the build failed before runtime acceptance. The pinned +official Odoo package has a hard dependency on `python3-pypdf2`, which Debian +Trixie does not provide. Cleanup, source identity, and disk checks passed. + +The retained failure evidence is: + +- report SHA-256 + `fad635e70932e0a92628442d1eeb1be47db5a2814e543e765b8a6593a635064a`; +- run log SHA-256 + `766d03f63a0ca61260e489d001fba15c2fe98da3b1bb81188fa41f176389142c`; +- source archive SHA-256 + `1a40879d734b1a143d89bdfed9415b3511e64d1a806f2bad7b61fb5c1eda362f`; +- source input tree SHA-256 + `77cbfca67219e22e3b3daed96b10df8a7628c276d3d4b6a1cc1f509e7bb5c92c`; +- source build tree SHA-256 + `c427ae0e7aa5648fa1b0d3d7b5ca011012b93b4774d93b18990f687f65f3b2bd`; +- retained manifest SHA-256 + `b15bf413c71a88edb7ccd964b38b0312174edc0e00b234138b605632b6a68dbf`. + +The appliance is lifecycle `BLOCKED` after three of three product fix loops. +It has no runtime PASS. Resume requires an explicit additional fix loop to +select and validate a supported Odoo package dependency route. ## Deferred issues From 6b6c43f72fa63cc4ed74075f27635936a57871d4 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 14:03:48 +0000 Subject: [PATCH 06/25] Harden Odoo 19 runtime and acceptance --- Makefile | 3 - README.rst | 8 +- conf.d/main | 55 ++++-- docs/v19.0-testing.md | 26 +-- overlay/etc/apache2/sites-available/odoo.conf | 7 +- .../system/odoo.service.d/turnkey.conf | 6 + overlay/usr/lib/inithooks/bin/odoo.py | 6 +- .../inithooks/firstboot.d/20-odoo-db-secrets | 15 +- overlay/usr/lib/inithooks/firstboot.d/40odoo | 7 +- overlay/usr/local/sbin/odoo-update | 12 +- tests/odoo-update-fixture.sh | 82 +++++++++ tests/v19.sh | 165 +++++++++++++++--- 12 files changed, 317 insertions(+), 75 deletions(-) create mode 100644 overlay/etc/systemd/system/odoo.service.d/turnkey.conf create mode 100755 tests/odoo-update-fixture.sh diff --git a/Makefile b/Makefile index ba971ab..7d62ba6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,4 @@ CREDIT_ANCHORTEXT = Turnkey Odoo Appliance -BACKPORTS=y # install Odoo v16.x from backports -BACKPORTS_PINS=odoo-16 - include $(FAB_PATH)/common/mk/turnkey/lapp.mk include $(FAB_PATH)/common/mk/turnkey.mk diff --git a/README.rst b/README.rst index c874b22..955bc89 100644 --- a/README.rst +++ b/README.rst @@ -18,16 +18,16 @@ This appliance includes all the standard features in `TurnKey Core`_: - Includes modules from base install of Odoo. - **Security note**: Odoo application updates require supervision and are not - configured to install automatically. Check the official daily channel with - ``odoo-update --check``, back up the database, then apply the selected APT - update. + configured to install automatically. Refresh APT metadata, check the official + daily channel with ``odoo-update --check``, back up the database, then apply + the selected APT update. - SSL support out of the box. - `Adminer`_ administration frontend for PostgreSQL (listening on port 12322 - uses SSL). - Webmin modules for configuring Apache2, PostgreSQL and Postfix. -**To create a new Odoo Databse (i.e. site)** +**To create a new Odoo Database (i.e. site)** To create a new Odoo DB, the Odoo config file needs to be edited and the Odoo service restarted. diff --git a/conf.d/main b/conf.d/main index 0c0ed1d..aca6dd4 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,5 +1,7 @@ #!/bin/bash -ex +set -o pipefail + # Install the supported Odoo Community package from Odoo's official channel. ODOO_VERSION=19.0.20260825 ODOO_REPO=https://nightly.odoo.com/19.0/nightly/deb @@ -15,7 +17,9 @@ package=$(mktemp --suffix=.deb) trap 'find "$key" "$package" -maxdepth 0 -type f -delete' EXIT curl -fsSL "$ODOO_KEY_URL" -o "$key" echo "$ODOO_KEY_SHA256 $key" | sha256sum -c - -test "$(gpg --show-keys --with-colons "$key" | awk -F: '$1 == "fpr" { print $10; exit }')" = "$ODOO_KEY_FINGERPRINT" +test "$(gpg --show-keys --with-colons "$key" | + awk -F: '$1 == "fpr" && !fingerprint { fingerprint=$10 } END { print fingerprint }')" = \ + "$ODOO_KEY_FINGERPRINT" gpg --batch --yes --dearmor --output /usr/share/keyrings/odoo-archive-keyring.gpg "$key" cat >/etc/apt/sources.list.d/odoo.list <"$SOURCE_RECORD" <&2 + exit 1 +fi +runuser -u postgres -- createdb --owner="$DB_USER" "$DB_NAME" + +runuser -u postgres -- psql --no-psqlrc --set ON_ERROR_STOP=on \ + "$DB_NAME" <> $CONF +cat >>"$CONF" <"$work/source" <<'EOF' +installed_version=19.0.20260825 +repository_key_fingerprint=5D134C924CB06330DCEFE2A1DEF2A2198183CBB5 +EOF +cat >"$work/odoo.list" <<'EOF' +deb [signed-by=/usr/share/keyrings/odoo-archive-keyring.gpg] https://nightly.odoo.com/19.0/nightly/deb/ ./ +EOF +touch "$work/keyring" + +cat >"$work/bin/dpkg-query" <<'EOF' +#!/bin/bash +printf '%s' "${FIXTURE_INSTALLED:-19.0.20260825}" +EOF +cat >"$work/bin/apt-cache" <<'EOF' +#!/bin/bash +printf 'odoo:\n Installed: %s\n Candidate: %s\n' \ + "${FIXTURE_INSTALLED:-19.0.20260825}" \ + "${FIXTURE_CANDIDATE-19.0.20260825}" +awk -v lines="${FIXTURE_TRAILER_LINES:-0}" \ + 'BEGIN { for (i = 0; i < lines; i++) print " fixture-source " i }' +EOF +cat >"$work/bin/gpg" <<'EOF' +#!/bin/bash +printf 'fpr:::::::::%s:\n' \ + "${FIXTURE_FINGERPRINT:-5D134C924CB06330DCEFE2A1DEF2A2198183CBB5}" +printf 'fpr:::::::::ABA924A9766870116E97090D78958C39ADE51428:\n' +EOF +cat >"$work/bin/dpkg" <<'EOF' +#!/bin/bash +exec /usr/bin/dpkg "$@" +EOF +chmod 0755 "$work/bin/"* + +sed \ + -e "s|^source_record=.*|source_record=$work/source|" \ + -e "s|^keyring=.*|keyring=$work/keyring|" \ + -e "s| /etc/apt/sources.list.d/odoo.list| $work/odoo.list|" \ + "$repo_root/overlay/usr/local/sbin/odoo-update" >"$work/odoo-update" +chmod 0755 "$work/odoo-update" + +run_check() { + env PATH="$work/bin:/usr/bin:/bin" "$@" "$work/odoo-update" --check +} + +expect_failure() { + local name=$1 + shift + + if run_check "$@" >"$work/$name.out" 2>"$work/$name.err"; then + echo "$name unexpectedly passed" >&2 + exit 1 + fi +} + +if env PATH="$work/bin:/usr/bin:/bin" "$work/odoo-update" \ + >"$work/usage.out" 2>"$work/usage.err"; then + echo 'missing argument unexpectedly passed' >&2 + exit 1 +else + test $? -eq 2 +fi + +run_check env FIXTURE_TRAILER_LINES=100000 >"$work/current.out" +grep -Fxq 'status=up-to-date' "$work/current.out" +run_check env FIXTURE_CANDIDATE=19.0.20260826 >"$work/newer.out" +grep -Fxq 'status=supervised-update-available' "$work/newer.out" + +expect_failure missing-candidate env FIXTURE_CANDIDATE= +expect_failure no-candidate env FIXTURE_CANDIDATE='(none)' +expect_failure downgrade env FIXTURE_CANDIDATE=19.0.20260824 +expect_failure wrong-key env FIXTURE_FINGERPRINT=0000000000000000000000000000000000000000 +expect_failure wrong-install env FIXTURE_INSTALLED=19.0.20260824 + +echo 'odoo updater fixture: PASS' diff --git a/tests/v19.sh b/tests/v19.sh index c9d10dd..b71228f 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -8,9 +8,11 @@ source_file=/usr/local/share/turnkey-odoo/source database=TurnkeylinuxExample fixture="TurnKey v19 contact $(date +%s)-$$" email="odoo-v19-$$@example.invalid" +cron_marker="TKL-v19-cron-$(date +%s)-$$" work=$(mktemp -d /tmp/odoo-v19.XXXXXXXX) cookie=$work/cookie partner_id= +cron_id= ocurl() { curl --insecure --fail --silent --show-error \ @@ -18,11 +20,55 @@ ocurl() { --header 'Content-Type: application/json' "$@" } +rpc_result() { + jq -er ' + if has("error") then + error(.error.data.message // .error.message // "Odoo RPC failed") + else + .result + end + ' +} + +ocall() { + local model=$1 + local method=$2 + local args=$3 + local kwargs=${4:-'{}'} + local payload + + payload=$(jq -cn \ + --arg model "$model" \ + --arg method "$method" \ + --argjson args "$args" \ + --argjson kwargs "$kwargs" \ + '{jsonrpc:"2.0", method:"call", params:{model:$model, method:$method, args:$args, kwargs:$kwargs}}') + ocurl --data "$payload" \ + "https://127.0.0.1/web/dataset/call_kw/$model/$method" +} + +authenticate() { + local payload response + + payload=$(jq -cn \ + --arg db "$database" \ + --arg login admin \ + --arg password "$app_password" \ + '{jsonrpc:"2.0", method:"call", params:{db:$db, login:$login, password:$password}}') + response=$(ocurl --data "$payload" \ + https://127.0.0.1/web/session/authenticate) + jq -e '.result.uid == 2 and (.error | not)' <<<"$response" >/dev/null +} + cleanup() { set +e + if [[ -n $cron_id ]]; then + ocall ir.cron unlink "$(jq -cn --argjson id "$cron_id" '[[$id]]')" | + rpc_result >/dev/null + fi if [[ -n $partner_id ]]; then - ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"unlink\",\"args\":[[$partner_id]],\"kwargs\":{}}}" \ - https://127.0.0.1/web/dataset/call_kw/res.partner/unlink >/dev/null + ocall res.partner unlink "$(jq -cn --argjson id "$partner_id" '[[$id]]')" | + rpc_result >/dev/null fi find "$work" -depth -delete } @@ -33,54 +79,127 @@ for unit in apache2.service postgresql.service odoo.service postfix.service; do systemctl --quiet is-enabled "$unit" done apache2ctl configtest +apache_modules=$(apache2ctl -M) +grep -Fq 'proxy_module' <<<"$apache_modules" +grep -Fq 'proxy_http_module' <<<"$apache_modules" +grep -Fq 'proxy_wstunnel_module' <<<"$apache_modules" grep -Fxq 'VERSION_CODENAME=trixie' /etc/os-release grep -Eq '^turnkey-odoo-19\.0' /etc/turnkey_version test "$(stat -c '%U:%G:%a' /etc/odoo/odoo.conf)" = root:odoo:640 +test "$(stat -c '%U:%G:%a' "$source_file")" = root:root:644 test -d /usr/share/adminer for module in apache postgresql postfix; do test -d "/usr/share/webmin/$module" done +runuser -u odoo -- python3 <<'PY' +import odoo + +odoo.tools.config.parse_config(['--config=/etc/odoo/odoo.conf']) +assert odoo.tools.config['proxy_mode'] is True +assert odoo.tools.config['workers'] == 2 +assert odoo.tools.config['max_cron_threads'] == 1 +assert odoo.tools.config['gevent_port'] == 8072 +PY + +service_environment=$(systemctl show odoo.service --property=Environment --value) +[[ $service_environment == *ODOO_NOTIFY_CRON_CHANGES=1* ]] + # shellcheck disable=SC1090 . "$source_file" +: "${package_source:?package_source is missing from $source_file}" +: "${installed_version:?installed_version is missing from $source_file}" +: "${package_url:?package_url is missing from $source_file}" +: "${package_sha256:?package_sha256 is missing from $source_file}" +: "${repository_key_fingerprint:?repository_key_fingerprint is missing from $source_file}" test "$installed_version" = 19.0.20260825 test "$(dpkg-query -W -f='${Version}' odoo)" = "$installed_version" test "$package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.gpg | - awk -F: '$1 == "fpr" { print $10; exit }')" = \ + awk -F: '$1 == "fpr" && !fingerprint { fingerprint=$10 } END { print fingerprint }')" = \ "$repository_key_fingerprint" +grep -Fq 'signed-by=/usr/share/keyrings/odoo-archive-keyring.gpg' \ + /etc/apt/sources.list.d/odoo.list odoo --version | grep -Fq '19.0' -auth=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"db\":\"$database\",\"login\":\"admin\",\"password\":\"$app_password\"}}" \ - https://127.0.0.1/web/session/authenticate) -jq -e '.result.uid == 2' <<<"$auth" >/dev/null +role_state=$(runuser -u postgres -- psql --no-psqlrc --tuples-only \ + --no-align postgres --command=" + SELECT rolsuper, rolcreatedb, rolcreaterole, rolreplication + FROM pg_roles WHERE rolname = 'odoo';") +test "$role_state" = 'f|t|f|f' +database_owner=$(runuser -u postgres -- psql --no-psqlrc --tuples-only \ + --no-align postgres --command=" + SELECT pg_get_userbyid(datdba) FROM pg_database + WHERE datname = '$database';") +test "$database_owner" = odoo +runuser -u postgres -- psql --no-psqlrc --tuples-only --no-align \ + "$database" --command='SELECT 1 FROM res_users LIMIT 1;' | + grep -Fxq 1 -created=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"create\",\"args\":[{\"name\":\"$fixture\",\"email\":\"$email\"}],\"kwargs\":{}}}" \ - https://127.0.0.1/web/dataset/call_kw/res.partner/create) -partner_id=$(jq -er '.result' <<<"$created") +authenticate + +contact_args=$(jq -cn --arg name "$fixture" --arg email "$email" \ + '[{name:$name, email:$email}]') +partner_id=$(ocall res.partner create "$contact_args" | rpc_result) [[ $partner_id =~ ^[0-9]+$ ]] -readback=$(ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"model\":\"res.partner\",\"method\":\"search_read\",\"args\":[[[\"id\",\"=\",$partner_id]]],\"kwargs\":{\"fields\":[\"id\",\"name\",\"email\"]}}}" \ - https://127.0.0.1/web/dataset/call_kw/res.partner/search_read) +contact_domain=$(jq -cn --argjson id "$partner_id" '[[["id","=",$id]]]') +contact_fields='{"fields":["id","name","email","phone"]}' +readback=$(ocall res.partner search_read "$contact_domain" "$contact_fields" | + rpc_result) jq -e --arg name "$fixture" --arg email "$email" \ - '.result[0].name == $name and .result[0].email == $email' \ - <<<"$readback" >/dev/null -su postgres -c "psql --tuples-only --no-align '$database'" </dev/null +runuser -u postgres -- psql --no-psqlrc --tuples-only --no-align \ + "$database" --command="SELECT name FROM res_partner WHERE id = $partner_id;" | grep -Fxq "$fixture" -systemctl restart postgresql.service odoo.service +systemctl restart postgresql.service +systemctl restart odoo.service +ready= for attempt in {1..60}; do - ocurl --data "{\"jsonrpc\":\"2.0\",\"method\":\"call\",\"params\":{\"db\":\"$database\",\"login\":\"admin\",\"password\":\"$app_password\"}}" \ - https://127.0.0.1/web/session/authenticate >/dev/null 2>&1 && break + if authenticate >/dev/null 2>&1; then + ready=1 + break + fi sleep 2 done -su postgres -c "psql --tuples-only --no-align '$database'" <= 4 )) + +runuser -u postgres -- psql --no-psqlrc --tuples-only --no-align \ + "$database" --command="SELECT name FROM res_partner WHERE id = $partner_id;" | grep -Fxq "$fixture" +model_domain='[[["model","=","res.partner"]]]' +model_id=$(ocall ir.model search_read "$model_domain" \ + '{"fields":["id"],"limit":1}' | rpc_result | jq -er '.[0].id') +cron_code="model.browse($partner_id).write({'phone': '$cron_marker'})" +cron_args=$(jq -cn \ + --arg name "TurnKey v19 cron $partner_id" \ + --arg code "$cron_code" \ + --arg nextcall "$(date -u '+%Y-%m-%d %H:%M:%S')" \ + --argjson model_id "$model_id" \ + '[{name:$name, model_id:$model_id, state:"code", code:$code, interval_number:1, interval_type:"months", nextcall:$nextcall}]') +cron_id=$(ocall ir.cron create "$cron_args" | rpc_result) +[[ $cron_id =~ ^[0-9]+$ ]] + +cron_complete= +for attempt in {1..60}; do + readback=$(ocall res.partner search_read "$contact_domain" "$contact_fields" | + rpc_result) + if jq -e --arg marker "$cron_marker" \ + '.[0].phone == $marker' <<<"$readback" >/dev/null; then + cron_complete=1 + break + fi + sleep 2 +done +test "$cron_complete" = 1 + printf '%s\n' "$app_password" | runuser -u odoo -- python3 -c \ 'import sys; import odoo; odoo.tools.config.parse_config(["--config=/etc/odoo/odoo.conf"]); assert odoo.tools.config.verify_admin_password(sys.stdin.readline().rstrip("\n"))' @@ -94,7 +213,7 @@ grep -Fxq "integrity=APT-signed-by-$repository_key_fingerprint" "$work/update" cat >"$result" < Date: Wed, 26 Aug 2026 14:56:18 +0000 Subject: [PATCH 07/25] Install pinned wkhtmltopdf renderer --- README.rst | 1 + changelog | 3 +++ conf.d/main | 21 ++++++++++++++++++++- docs/v19.0-testing.md | 30 ++++++++++++++++++++++++++---- overlay/usr/local/sbin/odoo-update | 11 +++++++++++ plan/main | 1 - tests/odoo-update-fixture.sh | 18 +++++++++++++++++- tests/v19.sh | 25 ++++++++++++++++++++++--- 8 files changed, 100 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 955bc89..7efc805 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,7 @@ This appliance includes all the standard features in `TurnKey Core`_: - Odoo 19 Community installed from Odoo's official package repository. - Includes modules from base install of Odoo. + - Includes the patched wkhtmltopdf 0.12.6 report renderer required by Odoo. - **Security note**: Odoo application updates require supervision and are not configured to install automatically. Refresh APT metadata, check the official diff --git a/changelog b/changelog index d3d65e9..250feb3 100644 --- a/changelog +++ b/changelog @@ -3,6 +3,9 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low * Install supported Odoo 19 Community from its official daily package channel with an exact package digest and bound repository key. + * Install Odoo's required patched wkhtmltopdf 0.12.6 series from an exact + upstream package digest for PDF report rendering. + * Use a non-superuser PostgreSQL application role and keep generated database and firstboot credentials out of process arguments and traces. diff --git a/conf.d/main b/conf.d/main index aca6dd4..c87ab30 100755 --- a/conf.d/main +++ b/conf.d/main @@ -10,11 +10,16 @@ ODOO_PACKAGE_SHA256=e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1 ODOO_KEY_URL=https://nightly.odoo.com/odoo.key ODOO_KEY_SHA256=1d169c013a727fa00238ba3f6417d5f33f162f8fbae32159a87ecab6966bfe8e ODOO_KEY_FINGERPRINT=5D134C924CB06330DCEFE2A1DEF2A2198183CBB5 +WKHTMLTOX_VERSION=1:0.12.6.1-3.bookworm +WKHTMLTOX_ARCHITECTURE=amd64 +WKHTMLTOX_URL=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb +WKHTMLTOX_SHA256=98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d SOURCE_RECORD=/usr/local/share/turnkey-odoo/source key=$(mktemp) package=$(mktemp --suffix=.deb) -trap 'find "$key" "$package" -maxdepth 0 -type f -delete' EXIT +wkhtmltox=$(mktemp --suffix=.deb) +trap 'find "$key" "$package" "$wkhtmltox" -maxdepth 0 -type f -delete' EXIT curl -fsSL "$ODOO_KEY_URL" -o "$key" echo "$ODOO_KEY_SHA256 $key" | sha256sum -c - test "$(gpg --show-keys --with-colons "$key" | @@ -36,6 +41,16 @@ Pin-Priority: 500 EOF apt-get update +curl -fsSL "$WKHTMLTOX_URL" -o "$wkhtmltox" +echo "$WKHTMLTOX_SHA256 $wkhtmltox" | sha256sum -c - +test "$(dpkg-deb --field "$wkhtmltox" Package)" = wkhtmltox +test "$(dpkg-deb --field "$wkhtmltox" Version)" = "$WKHTMLTOX_VERSION" +test "$(dpkg-deb --field "$wkhtmltox" Architecture)" = "$WKHTMLTOX_ARCHITECTURE" +test "$(dpkg --print-architecture)" = "$WKHTMLTOX_ARCHITECTURE" +DEBIAN_FRONTEND=noninteractive apt-get install -y "$wkhtmltox" +test "$(dpkg-query -W -f='${Version}' wkhtmltox)" = "$WKHTMLTOX_VERSION" +wkhtmltopdf --version | grep -Fq 'wkhtmltopdf 0.12.6.1 (with patched qt)' + curl -fsSL "$ODOO_PACKAGE_URL" -o "$package" echo "$ODOO_PACKAGE_SHA256 $package" | sha256sum -c - DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" @@ -50,6 +65,10 @@ package_url=$ODOO_PACKAGE_URL package_sha256=$ODOO_PACKAGE_SHA256 repository_key_fingerprint=$ODOO_KEY_FINGERPRINT repository_key_sha256=$ODOO_KEY_SHA256 +wkhtmltox_version=$WKHTMLTOX_VERSION +wkhtmltox_architecture=$WKHTMLTOX_ARCHITECTURE +wkhtmltox_url=$WKHTMLTOX_URL +wkhtmltox_sha256=$WKHTMLTOX_SHA256 EOF chmod 0644 "$SOURCE_RECORD" diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 5a28b6d..1c2142f 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -11,6 +11,12 @@ for later updates. Odoo documents the [September 2028 support horizon](https://www.odoo.com/documentation/19.0/administration/supported_versions.html), and the recommended prefork plus cron-worker deployment in its [system configuration guide](https://www.odoo.com/documentation/19.0/administration/on_premise/deploy.html). +Debian Trixie no longer packages `wkhtmltopdf`; Odoo requires a manually +installed patched 0.12.6 renderer for report headers and footers. The appliance +therefore pins the latest +[upstream 0.12.6.1-3 release](https://github.com/wkhtmltopdf/packaging/releases/tag/0.12.6.1-3) +Bookworm package, whose legacy library dependency names are provided by +Trixie's ABI-compatible `t64` packages. The selected evidence is: @@ -21,6 +27,8 @@ The selected evidence is: `1d169c013a727fa00238ba3f6417d5f33f162f8fbae32159a87ecab6966bfe8e`; - package SHA-256 `e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69`. +- patched wkhtmltox AMD64 version `1:0.12.6.1-3.bookworm` and package SHA-256 + `98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d`. ## README crosswalk @@ -31,6 +39,7 @@ The selected evidence is: | Database creation and privilege | Inspect PostgreSQL's catalog and initialized Odoo schema | The example database belongs to a login/create-database role without superuser, role-create, or replication privileges | | PostgreSQL persistence | Read the contact directly, restart PostgreSQL and Odoo, then read it again | The database row survives the service restart | | Workers and background jobs | Require two prefork HTTP workers and one cron worker, then schedule a throwaway action | The cron worker updates the contact through Odoo and the value is readable through the model API | +| PDF report renderer | Verify the patched upstream version and render a minimal HTML document | `wkhtmltopdf` produces a PDF on Trixie | | Database management password | Verify the configured master password with Odoo's own configuration API | `TKL_TEST_APP_PASS` verifies without exposing the stored hash | | HTTPS proxy | Validate Apache and use only its HTTPS endpoint for application calls | Apache configuration passes and proxies Odoo normally | | Adminer and Webmin | Require Adminer plus Apache, PostgreSQL, and Postfix module directories | Appliance-specific administration components are installed | @@ -52,15 +61,28 @@ TKL_HARNESS_DOCKER_OBJECT_LABEL=org.turnkeylinux.v19-harness.wave2-2 \ --source /home/agent/.local/worktrees/turnkey-apps/odoo/wish-odoo-v19-trixie ``` -Exact retained acceptance is waiting behind the active Canvas and Jenkins runs -in the isolated builder queue. No Odoo product loop has been spent yet. +Loop 1 (`20260826t143314z-3386451-1936`) passed HTTPS preflight but stopped in +package resolution because Debian Trixie has no `wkhtmltopdf` package. Cleanup, +builder identity, and disk checks passed. The fix replaces that nonexistent +plan package with the exact upstream renderer described above. Loop 2 is +waiting behind the active Canvas run. + +Loop 1 used source `6b6c43f72fa63cc4ed74075f27635936a57871d4` +and harness `152bc9b876557266b90ed4ded77b611d9b817aff`. Its source commit +and transport archive SHA-256 was +`01b97ba3c5e46ad3189df690e1ca525f2c43088b6cb0f9fb15bf6e07d73eb981`; +the staged input tree SHA-256 was +`3ccee560801b3262174c0d782d3ef1d9a6edfa025646e1b899720ba5a567c15a`. +The retained failure manifest validated every retained file with +`sha256sum -c RETAINED-SHA256SUMS` before the retry. ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates remain supervised so administrators can back up PostgreSQL first. -- The packaged `wkhtmltopdf` is retained for report rendering. Header and - footer fidelity is not part of the primary record workflow. +- The patched `wkhtmltopdf` project is archived upstream. Its exact package is + retained because Odoo 19 still requires the 0.12.6 series; future renderer + changes require explicit compatibility and security review. - Public Postfix delivery and optional localization dependencies are not exercised by local acceptance. - Docker acceptance does not repeat installer, kernel, or hardware checks. diff --git a/overlay/usr/local/sbin/odoo-update b/overlay/usr/local/sbin/odoo-update index 30c03ec..685d85e 100755 --- a/overlay/usr/local/sbin/odoo-update +++ b/overlay/usr/local/sbin/odoo-update @@ -13,8 +13,13 @@ keyring=/usr/share/keyrings/odoo-archive-keyring.gpg . "$source_record" : "${installed_version:?installed_version is missing from $source_record}" : "${repository_key_fingerprint:?repository_key_fingerprint is missing from $source_record}" +: "${wkhtmltox_version:?wkhtmltox_version is missing from $source_record}" +: "${wkhtmltox_architecture:?wkhtmltox_architecture is missing from $source_record}" +: "${wkhtmltox_sha256:?wkhtmltox_sha256 is missing from $source_record}" installed=$(dpkg-query -W -f='${Version}' odoo) +renderer=$(dpkg-query -W -f='${Version}' wkhtmltox) +renderer_architecture=$(dpkg-query -W -f='${Architecture}' wkhtmltox) policy=$(apt-cache policy odoo) candidate=$(awk ' /^[[:space:]]*Candidate:/ && !candidate { candidate=$2 } @@ -24,6 +29,8 @@ fingerprint=$(gpg --show-keys --with-colons "$keyring" | awk -F: '$1 == "fpr" && !fingerprint { fingerprint=$10 } END { print fingerprint }') test "$installed" = "$installed_version" +test "$renderer" = "$wkhtmltox_version" +test "$renderer_architecture" = "$wkhtmltox_architecture" test "$fingerprint" = "$repository_key_fingerprint" test -n "$candidate" test "$candidate" != "(none)" @@ -43,4 +50,8 @@ candidate=$candidate channel=official-odoo-19-community-daily status=$status integrity=APT-signed-by-$fingerprint +renderer=wkhtmltox-$renderer +renderer_architecture=$renderer_architecture +renderer_policy=pinned-manual-security-review +renderer_integrity=SHA256-$wkhtmltox_sha256 EOF diff --git a/plan/main b/plan/main index bcafa1d..7a73f58 100644 --- a/plan/main +++ b/plan/main @@ -2,7 +2,6 @@ #include xfonts-75dpi -wkhtmltopdf python3-psycogreen adduser diff --git a/tests/odoo-update-fixture.sh b/tests/odoo-update-fixture.sh index 1fa6f1f..ebdfbce 100755 --- a/tests/odoo-update-fixture.sh +++ b/tests/odoo-update-fixture.sh @@ -9,6 +9,9 @@ mkdir -p "$work/bin" cat >"$work/source" <<'EOF' installed_version=19.0.20260825 repository_key_fingerprint=5D134C924CB06330DCEFE2A1DEF2A2198183CBB5 +wkhtmltox_version=1:0.12.6.1-3.bookworm +wkhtmltox_architecture=amd64 +wkhtmltox_sha256=98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d EOF cat >"$work/odoo.list" <<'EOF' deb [signed-by=/usr/share/keyrings/odoo-archive-keyring.gpg] https://nightly.odoo.com/19.0/nightly/deb/ ./ @@ -17,7 +20,15 @@ touch "$work/keyring" cat >"$work/bin/dpkg-query" <<'EOF' #!/bin/bash -printf '%s' "${FIXTURE_INSTALLED:-19.0.20260825}" +if [[ $* == *wkhtmltox* ]]; then + if [[ $* == *Architecture* ]]; then + printf '%s' "${FIXTURE_RENDERER_ARCHITECTURE:-amd64}" + else + printf '%s' "${FIXTURE_RENDERER:-1:0.12.6.1-3.bookworm}" + fi +else + printf '%s' "${FIXTURE_INSTALLED:-19.0.20260825}" +fi EOF cat >"$work/bin/apt-cache" <<'EOF' #!/bin/bash @@ -70,6 +81,9 @@ fi run_check env FIXTURE_TRAILER_LINES=100000 >"$work/current.out" grep -Fxq 'status=up-to-date' "$work/current.out" +grep -Fxq 'renderer=wkhtmltox-1:0.12.6.1-3.bookworm' "$work/current.out" +grep -Fxq 'renderer_architecture=amd64' "$work/current.out" +grep -Fxq 'renderer_policy=pinned-manual-security-review' "$work/current.out" run_check env FIXTURE_CANDIDATE=19.0.20260826 >"$work/newer.out" grep -Fxq 'status=supervised-update-available' "$work/newer.out" @@ -78,5 +92,7 @@ expect_failure no-candidate env FIXTURE_CANDIDATE='(none)' expect_failure downgrade env FIXTURE_CANDIDATE=19.0.20260824 expect_failure wrong-key env FIXTURE_FINGERPRINT=0000000000000000000000000000000000000000 expect_failure wrong-install env FIXTURE_INSTALLED=19.0.20260824 +expect_failure wrong-renderer env FIXTURE_RENDERER=1:0.12.6.1-2.bookworm +expect_failure wrong-renderer-architecture env FIXTURE_RENDERER_ARCHITECTURE=arm64 echo 'odoo updater fixture: PASS' diff --git a/tests/v19.sh b/tests/v19.sh index b71228f..6e17e56 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -112,6 +112,10 @@ service_environment=$(systemctl show odoo.service --property=Environment --value : "${package_url:?package_url is missing from $source_file}" : "${package_sha256:?package_sha256 is missing from $source_file}" : "${repository_key_fingerprint:?repository_key_fingerprint is missing from $source_file}" +: "${wkhtmltox_version:?wkhtmltox_version is missing from $source_file}" +: "${wkhtmltox_architecture:?wkhtmltox_architecture is missing from $source_file}" +: "${wkhtmltox_url:?wkhtmltox_url is missing from $source_file}" +: "${wkhtmltox_sha256:?wkhtmltox_sha256 is missing from $source_file}" test "$installed_version" = 19.0.20260825 test "$(dpkg-query -W -f='${Version}' odoo)" = "$installed_version" test "$package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 @@ -121,6 +125,17 @@ test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.g grep -Fq 'signed-by=/usr/share/keyrings/odoo-archive-keyring.gpg' \ /etc/apt/sources.list.d/odoo.list odoo --version | grep -Fq '19.0' +test "$wkhtmltox_version" = '1:0.12.6.1-3.bookworm' +test "$wkhtmltox_architecture" = amd64 +test "$wkhtmltox_url" = https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb +test "$(dpkg-query -W -f='${Version}' wkhtmltox)" = "$wkhtmltox_version" +test "$(dpkg-query -W -f='${Architecture}' wkhtmltox)" = "$wkhtmltox_architecture" +test "$wkhtmltox_sha256" = 98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d +wkhtmltopdf --version | grep -Fq 'wkhtmltopdf 0.12.6.1 (with patched qt)' +wkhtmltopdf --quiet - "$work/report.pdf" <<'EOF' +

TurnKey Odoo v19 report probe

+EOF +test "$(head -c 4 "$work/report.pdf")" = '%PDF' role_state=$(runuser -u postgres -- psql --no-psqlrc --tuples-only \ --no-align postgres --command=" @@ -209,13 +224,17 @@ status=$(sed -n 's/^status=//p' "$work/update") test -n "$candidate" grep -Fxq 'channel=official-odoo-19-community-daily' "$work/update" grep -Fxq "integrity=APT-signed-by-$repository_key_fingerprint" "$work/update" +grep -Fxq "renderer=wkhtmltox-$wkhtmltox_version" "$work/update" +grep -Fxq "renderer_architecture=$wkhtmltox_architecture" "$work/update" +grep -Fxq 'renderer_policy=pinned-manual-security-review' "$work/update" +grep -Fxq "renderer_integrity=SHA256-$wkhtmltox_sha256" "$work/update" cat >"$result" < Date: Wed, 26 Aug 2026 16:00:00 +0000 Subject: [PATCH 08/25] Bridge Odoo pypdf dependency on Trixie --- changelog | 3 ++ conf.d/main | 42 ++++++++++++++++++++++++- docs/v19.0-testing.md | 31 ++++++++++++++++-- overlay/usr/local/sbin/odoo-update | 14 +++++++++ tests/odoo-update-fixture.sh | 50 ++++++++++++++++++++++++------ tests/v19.sh | 32 +++++++++++++++++-- 6 files changed, 157 insertions(+), 15 deletions(-) diff --git a/changelog b/changelog index 250feb3..5ba83bc 100644 --- a/changelog +++ b/changelog @@ -6,6 +6,9 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low * Install Odoo's required patched wkhtmltopdf 0.12.6 series from an exact upstream package digest for PDF report rendering. + * Bridge Odoo's stale python3-pypdf2 package dependency to Trixie's + maintained python3-pypdf implementation without modifying Odoo's payload. + * Use a non-superuser PostgreSQL application role and keep generated database and firstboot credentials out of process arguments and traces. diff --git a/conf.d/main b/conf.d/main index c87ab30..3127e7a 100755 --- a/conf.d/main +++ b/conf.d/main @@ -14,12 +14,16 @@ WKHTMLTOX_VERSION=1:0.12.6.1-3.bookworm WKHTMLTOX_ARCHITECTURE=amd64 WKHTMLTOX_URL=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb WKHTMLTOX_SHA256=98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d +PYPDF_COMPAT_PACKAGE=turnkey-odoo-pypdf-compat +PYPDF_COMPAT_VERSION=1.0+turnkey19.0.1 SOURCE_RECORD=/usr/local/share/turnkey-odoo/source key=$(mktemp) package=$(mktemp --suffix=.deb) wkhtmltox=$(mktemp --suffix=.deb) -trap 'find "$key" "$package" "$wkhtmltox" -maxdepth 0 -type f -delete' EXIT +pypdf_compat_root=$(mktemp -d) +pypdf_compat=$(mktemp --suffix=.deb) +trap 'find "$key" "$package" "$wkhtmltox" "$pypdf_compat_root" "$pypdf_compat" -depth -delete' EXIT curl -fsSL "$ODOO_KEY_URL" -o "$key" echo "$ODOO_KEY_SHA256 $key" | sha256sum -c - test "$(gpg --show-keys --with-colons "$key" | @@ -51,10 +55,44 @@ DEBIAN_FRONTEND=noninteractive apt-get install -y "$wkhtmltox" test "$(dpkg-query -W -f='${Version}' wkhtmltox)" = "$WKHTMLTOX_VERSION" wkhtmltopdf --version | grep -Fq 'wkhtmltopdf 0.12.6.1 (with patched qt)' +# Odoo's 19.0 payload supports pypdf and its own Trixie upgrade installs +# python3-pypdf, but the daily Debian package still names Bookworm's removed +# python3-pypdf2 package. Keep the verified Odoo payload intact and bridge only +# that stale, unversioned package name to Debian's maintained implementation. +mkdir -p "$pypdf_compat_root/DEBIAN" +cat >"$pypdf_compat_root/DEBIAN/control" < +Architecture: all +Depends: python3-pypdf +Provides: python3-pypdf2 +Description: Odoo 19 pypdf dependency compatibility + Satisfies Odoo's stale python3-pypdf2 package dependency with Trixie's + maintained python3-pypdf implementation. +EOF +dpkg-deb --build --root-owner-group "$pypdf_compat_root" "$pypdf_compat" +test "$(dpkg-deb --field "$pypdf_compat" Package)" = "$PYPDF_COMPAT_PACKAGE" +test "$(dpkg-deb --field "$pypdf_compat" Version)" = "$PYPDF_COMPAT_VERSION" +test "$(dpkg-deb --field "$pypdf_compat" Architecture)" = all +test "$(dpkg-deb --field "$pypdf_compat" Depends)" = python3-pypdf +test "$(dpkg-deb --field "$pypdf_compat" Provides)" = python3-pypdf2 +DEBIAN_FRONTEND=noninteractive apt-get install -y "$pypdf_compat" +test "$(dpkg-query -W -f='${Version}' "$PYPDF_COMPAT_PACKAGE")" = \ + "$PYPDF_COMPAT_VERSION" +test "$(dpkg-query -W -f='${Provides}' "$PYPDF_COMPAT_PACKAGE")" = \ + python3-pypdf2 +python3 -c 'import pypdf; assert pypdf.__version__' + curl -fsSL "$ODOO_PACKAGE_URL" -o "$package" echo "$ODOO_PACKAGE_SHA256 $package" | sha256sum -c - DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" test "$(dpkg-query -W -f='${Version}' odoo)" = "$ODOO_VERSION" +apt-get check +runuser -u odoo -- python3 -c \ + 'from odoo.tools import pdf; assert pdf.SUBMOD == "._pypdf"; assert pdf.pypdf.__version__' systemctl stop odoo mkdir -p "$(dirname "$SOURCE_RECORD")" @@ -69,6 +107,8 @@ wkhtmltox_version=$WKHTMLTOX_VERSION wkhtmltox_architecture=$WKHTMLTOX_ARCHITECTURE wkhtmltox_url=$WKHTMLTOX_URL wkhtmltox_sha256=$WKHTMLTOX_SHA256 +pypdf_compat_package=$PYPDF_COMPAT_PACKAGE +pypdf_compat_version=$PYPDF_COMPAT_VERSION EOF chmod 0644 "$SOURCE_RECORD" diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 1c2142f..72444cd 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -17,6 +17,12 @@ therefore pins the latest [upstream 0.12.6.1-3 release](https://github.com/wkhtmltopdf/packaging/releases/tag/0.12.6.1-3) Bookworm package, whose legacy library dependency names are provided by Trixie's ABI-compatible `t64` packages. +The official Odoo payload imports modern `pypdf` and Odoo's own Trixie upgrade +script installs Debian's `python3-pypdf`, but the daily package metadata still +depends on Bookworm's removed `python3-pypdf2` name. A minimal versioned +`turnkey-odoo-pypdf-compat` package therefore depends on Trixie's maintained +implementation and provides only the stale package name. It does not replace +or modify the hash-verified Odoo payload. The selected evidence is: @@ -29,6 +35,8 @@ The selected evidence is: `e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69`. - patched wkhtmltox AMD64 version `1:0.12.6.1-3.bookworm` and package SHA-256 `98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d`. +- pypdf compatibility package `turnkey-odoo-pypdf-compat` version + `1.0+turnkey19.0.1`, backed by Debian Trixie's `python3-pypdf`. ## README crosswalk @@ -44,7 +52,7 @@ The selected evidence is: | HTTPS proxy | Validate Apache and use only its HTTPS endpoint for application calls | Apache configuration passes and proxies Odoo normally | | Adminer and Webmin | Require Adminer plus Apache, PostgreSQL, and Postfix module directories | Appliance-specific administration components are installed | | Outbound mail | Require the Postfix unit | Postfix is enabled and active; public delivery is outside local acceptance | -| Supervised updates | Refresh APT metadata, then run `odoo-update --check` | The installed daily version, signed candidate, and channel are reported without changing the installed application | +| Supervised updates | Refresh APT metadata, then run `odoo-update --check` | The installed daily version, signed candidate, channel, and simulated dependency resolution are reported without changing the installed application | | Inherited administration | Cite unchanged SSH, Webmin, and common platform behavior | Core 19 PASS run `20260824t010251z-1634-32241` at source `24c82ee3540ce545422742b0e28ba6b687c53ec2` remains applicable | The acceptance fixture deletes its contact through Odoo on exit. @@ -57,7 +65,7 @@ TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2-2 \ TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2-2/build.lock \ TKL_HARNESS_DOCKER_LIMIT_BYTES=137438953472 \ TKL_HARNESS_DOCKER_OBJECT_LABEL=org.turnkeylinux.v19-harness.wave2-2 \ -/home/agent/turnkey/tools/test-v19-appliance odoo \ +/home/agent/.local/worktrees/turnkey/harness-wave2-2-152bc9b/tools/test-v19-appliance odoo \ --source /home/agent/.local/worktrees/turnkey-apps/odoo/wish-odoo-v19-trixie ``` @@ -74,7 +82,24 @@ and transport archive SHA-256 was the staged input tree SHA-256 was `3ccee560801b3262174c0d782d3ef1d9a6edfa025646e1b899720ba5a567c15a`. The retained failure manifest validated every retained file with -`sha256sum -c RETAINED-SHA256SUMS` before the retry. +`sha256sum -c RETAINED-SHA256SUMS` before the retry. Loop 2 +(`20260826t145637z-3507144-30487`) used source +`9a09c276be5570006c560aada43ff44445eabccf` with the same exact harness. +The patched renderer installed and verified, then the official Odoo package +failed dependency resolution because Trixie has no `python3-pypdf2`. Cleanup, +builder identity, and disk checks passed. Its source commit and transport +archive SHA-256 was +`4ee53cf5f9d8ea0a14d605071087bbd0ded3f580707f1993382708d0e4b3553e`; +the staged input tree SHA-256 was +`70f30087f61d835cb3ce034eb565043a6a3f4442b1d1e15fef8da3c318a39a59`. +The retained loop 2 manifest also validated every retained file before the +compatibility fix. Loop 3 is waiting for the scoped builder. + +A disposable `debian:trixie-slim` probe installed Debian +`python3-pypdf` `5.4.0-1`, the generated compatibility provider, and the exact +hash-verified Odoo package. `apt-get check`, `dpkg --audit`, Odoo's `._pypdf` +selection, and a one-page PDF write/read roundtrip all passed; the labeled +probe containers were removed on exit. ## Deferred issues diff --git a/overlay/usr/local/sbin/odoo-update b/overlay/usr/local/sbin/odoo-update index 685d85e..5087e73 100755 --- a/overlay/usr/local/sbin/odoo-update +++ b/overlay/usr/local/sbin/odoo-update @@ -16,10 +16,15 @@ keyring=/usr/share/keyrings/odoo-archive-keyring.gpg : "${wkhtmltox_version:?wkhtmltox_version is missing from $source_record}" : "${wkhtmltox_architecture:?wkhtmltox_architecture is missing from $source_record}" : "${wkhtmltox_sha256:?wkhtmltox_sha256 is missing from $source_record}" +: "${pypdf_compat_package:?pypdf_compat_package is missing from $source_record}" +: "${pypdf_compat_version:?pypdf_compat_version is missing from $source_record}" installed=$(dpkg-query -W -f='${Version}' odoo) renderer=$(dpkg-query -W -f='${Version}' wkhtmltox) renderer_architecture=$(dpkg-query -W -f='${Architecture}' wkhtmltox) +compat_version=$(dpkg-query -W -f='${Version}' "$pypdf_compat_package") +compat_provides=$(dpkg-query -W -f='${Provides}' "$pypdf_compat_package") +pypdf_version=$(dpkg-query -W -f='${Version}' python3-pypdf) policy=$(apt-cache policy odoo) candidate=$(awk ' /^[[:space:]]*Candidate:/ && !candidate { candidate=$2 } @@ -31,12 +36,17 @@ fingerprint=$(gpg --show-keys --with-colons "$keyring" | test "$installed" = "$installed_version" test "$renderer" = "$wkhtmltox_version" test "$renderer_architecture" = "$wkhtmltox_architecture" +test "$compat_version" = "$pypdf_compat_version" +grep -Eq '(^|, )[[:space:]]*python3-pypdf2([[:space:]]|$)' <<<"$compat_provides" +test -n "$pypdf_version" test "$fingerprint" = "$repository_key_fingerprint" test -n "$candidate" test "$candidate" != "(none)" dpkg --compare-versions "$candidate" ge "$installed" grep -Fq 'https://nightly.odoo.com/19.0/nightly/deb/ ./' \ /etc/apt/sources.list.d/odoo.list +DEBIAN_FRONTEND=noninteractive apt-get --simulate --quiet=2 \ + install "odoo=$candidate" >/dev/null if dpkg --compare-versions "$candidate" eq "$installed"; then status=up-to-date @@ -54,4 +64,8 @@ renderer=wkhtmltox-$renderer renderer_architecture=$renderer_architecture renderer_policy=pinned-manual-security-review renderer_integrity=SHA256-$wkhtmltox_sha256 +dependency_bridge=$pypdf_compat_package-$compat_version +dependency_bridge_provides=python3-pypdf2 +pypdf=python3-pypdf-$pypdf_version +candidate_resolution=apt-simulated EOF diff --git a/tests/odoo-update-fixture.sh b/tests/odoo-update-fixture.sh index ebdfbce..f575d18 100755 --- a/tests/odoo-update-fixture.sh +++ b/tests/odoo-update-fixture.sh @@ -12,6 +12,8 @@ repository_key_fingerprint=5D134C924CB06330DCEFE2A1DEF2A2198183CBB5 wkhtmltox_version=1:0.12.6.1-3.bookworm wkhtmltox_architecture=amd64 wkhtmltox_sha256=98ba0d157b50d36f23bd0dedf4c0aa28c7b0c50fcdcdc54aa5b6bbba81a3941d +pypdf_compat_package=turnkey-odoo-pypdf-compat +pypdf_compat_version=1.0+turnkey19.0.1 EOF cat >"$work/odoo.list" <<'EOF' deb [signed-by=/usr/share/keyrings/odoo-archive-keyring.gpg] https://nightly.odoo.com/19.0/nightly/deb/ ./ @@ -20,15 +22,28 @@ touch "$work/keyring" cat >"$work/bin/dpkg-query" <<'EOF' #!/bin/bash -if [[ $* == *wkhtmltox* ]]; then - if [[ $* == *Architecture* ]]; then - printf '%s' "${FIXTURE_RENDERER_ARCHITECTURE:-amd64}" - else - printf '%s' "${FIXTURE_RENDERER:-1:0.12.6.1-3.bookworm}" - fi -else - printf '%s' "${FIXTURE_INSTALLED:-19.0.20260825}" -fi +case $* in + *wkhtmltox*) + if [[ $* == *Architecture* ]]; then + printf '%s' "${FIXTURE_RENDERER_ARCHITECTURE:-amd64}" + else + printf '%s' "${FIXTURE_RENDERER:-1:0.12.6.1-3.bookworm}" + fi + ;; + *turnkey-odoo-pypdf-compat*) + if [[ $* == *Provides* ]]; then + printf '%s' "${FIXTURE_COMPAT_PROVIDES:-python3-pypdf2}" + else + printf '%s' "${FIXTURE_COMPAT_VERSION:-1.0+turnkey19.0.1}" + fi + ;; + *python3-pypdf*) + printf '%s' "${FIXTURE_PYPDF_VERSION-5.4.0-1}" + ;; + *) + printf '%s' "${FIXTURE_INSTALLED:-19.0.20260825}" + ;; +esac EOF cat >"$work/bin/apt-cache" <<'EOF' #!/bin/bash @@ -48,6 +63,14 @@ cat >"$work/bin/dpkg" <<'EOF' #!/bin/bash exec /usr/bin/dpkg "$@" EOF +cat >"$work/bin/apt-get" <<'EOF' +#!/bin/bash +if [[ ${FIXTURE_RESOLUTION_FAILURE:-0} == 1 ]]; then + exit 100 +fi +[[ $* == *--simulate* ]] +[[ $* == *odoo=* ]] +EOF chmod 0755 "$work/bin/"* sed \ @@ -84,6 +107,11 @@ grep -Fxq 'status=up-to-date' "$work/current.out" grep -Fxq 'renderer=wkhtmltox-1:0.12.6.1-3.bookworm' "$work/current.out" grep -Fxq 'renderer_architecture=amd64' "$work/current.out" grep -Fxq 'renderer_policy=pinned-manual-security-review' "$work/current.out" +grep -Fxq 'dependency_bridge=turnkey-odoo-pypdf-compat-1.0+turnkey19.0.1' \ + "$work/current.out" +grep -Fxq 'dependency_bridge_provides=python3-pypdf2' "$work/current.out" +grep -Fxq 'pypdf=python3-pypdf-5.4.0-1' "$work/current.out" +grep -Fxq 'candidate_resolution=apt-simulated' "$work/current.out" run_check env FIXTURE_CANDIDATE=19.0.20260826 >"$work/newer.out" grep -Fxq 'status=supervised-update-available' "$work/newer.out" @@ -94,5 +122,9 @@ expect_failure wrong-key env FIXTURE_FINGERPRINT=0000000000000000000000000000000 expect_failure wrong-install env FIXTURE_INSTALLED=19.0.20260824 expect_failure wrong-renderer env FIXTURE_RENDERER=1:0.12.6.1-2.bookworm expect_failure wrong-renderer-architecture env FIXTURE_RENDERER_ARCHITECTURE=arm64 +expect_failure wrong-compat env FIXTURE_COMPAT_VERSION=1.0+turnkey19.0.0 +expect_failure wrong-provides env FIXTURE_COMPAT_PROVIDES=python3-pypdf +expect_failure missing-pypdf env FIXTURE_PYPDF_VERSION= +expect_failure unresolved-candidate env FIXTURE_RESOLUTION_FAILURE=1 echo 'odoo updater fixture: PASS' diff --git a/tests/v19.sh b/tests/v19.sh index 6e17e56..9388186 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -116,6 +116,8 @@ service_environment=$(systemctl show odoo.service --property=Environment --value : "${wkhtmltox_architecture:?wkhtmltox_architecture is missing from $source_file}" : "${wkhtmltox_url:?wkhtmltox_url is missing from $source_file}" : "${wkhtmltox_sha256:?wkhtmltox_sha256 is missing from $source_file}" +: "${pypdf_compat_package:?pypdf_compat_package is missing from $source_file}" +: "${pypdf_compat_version:?pypdf_compat_version is missing from $source_file}" test "$installed_version" = 19.0.20260825 test "$(dpkg-query -W -f='${Version}' odoo)" = "$installed_version" test "$package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 @@ -136,6 +138,27 @@ wkhtmltopdf --quiet - "$work/report.pdf" <<'EOF'

TurnKey Odoo v19 report probe

EOF test "$(head -c 4 "$work/report.pdf")" = '%PDF' +test "$pypdf_compat_package" = turnkey-odoo-pypdf-compat +test "$pypdf_compat_version" = 1.0+turnkey19.0.1 +test "$(dpkg-query -W -f='${Version}' "$pypdf_compat_package")" = \ + "$pypdf_compat_version" +test "$(dpkg-query -W -f='${Provides}' "$pypdf_compat_package")" = \ + python3-pypdf2 +apt-get check +runuser -u odoo -- python3 <<'PY' +import io + +import pypdf +from odoo.tools import pdf as odoo_pdf + +assert odoo_pdf.SUBMOD == '._pypdf' +writer = pypdf.PdfWriter() +writer.add_blank_page(width=72, height=72) +stream = io.BytesIO() +writer.write(stream) +stream.seek(0) +assert len(pypdf.PdfReader(stream).pages) == 1 +PY role_state=$(runuser -u postgres -- psql --no-psqlrc --tuples-only \ --no-align postgres --command=" @@ -228,13 +251,18 @@ grep -Fxq "renderer=wkhtmltox-$wkhtmltox_version" "$work/update" grep -Fxq "renderer_architecture=$wkhtmltox_architecture" "$work/update" grep -Fxq 'renderer_policy=pinned-manual-security-review' "$work/update" grep -Fxq "renderer_integrity=SHA256-$wkhtmltox_sha256" "$work/update" +grep -Fxq "dependency_bridge=$pypdf_compat_package-$pypdf_compat_version" \ + "$work/update" +grep -Fxq 'dependency_bridge_provides=python3-pypdf2' "$work/update" +grep -Eq '^pypdf=python3-pypdf-.+' "$work/update" +grep -Fxq 'candidate_resolution=apt-simulated' "$work/update" cat >"$result" < Date: Wed, 26 Aug 2026 16:18:06 +0000 Subject: [PATCH 09/25] Fix Odoo 19 config acceptance imports --- docs/v19.0-testing.md | 8 ++++++++ tests/v19.sh | 14 +++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 72444cd..727157f 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -101,6 +101,14 @@ hash-verified Odoo package. `apt-get check`, `dpkg --audit`, Odoo's `._pypdf` selection, and a one-page PDF write/read roundtrip all passed; the labeled probe containers were removed on exit. +The compatibility candidate then passed exact build, configured-root import, +inithooks, and supervised runtime in run +`20260826t160045z-3839726-26947`. The acceptance script stopped at its first +Python assertion because Odoo 19 lazily exposes `tools`; the fixture now +imports `config` explicitly at both call sites. No application assertion had +run, cleanup, builder identity, and disk checks passed, and the retained +manifest validated all configured-root and runtime evidence before the retry. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates diff --git a/tests/v19.sh b/tests/v19.sh index 9388186..be55447 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -93,13 +93,13 @@ for module in apache postgresql postfix; do done runuser -u odoo -- python3 <<'PY' -import odoo +from odoo.tools import config -odoo.tools.config.parse_config(['--config=/etc/odoo/odoo.conf']) -assert odoo.tools.config['proxy_mode'] is True -assert odoo.tools.config['workers'] == 2 -assert odoo.tools.config['max_cron_threads'] == 1 -assert odoo.tools.config['gevent_port'] == 8072 +config.parse_config(['--config=/etc/odoo/odoo.conf']) +assert config['proxy_mode'] is True +assert config['workers'] == 2 +assert config['max_cron_threads'] == 1 +assert config['gevent_port'] == 8072 PY service_environment=$(systemctl show odoo.service --property=Environment --value) @@ -239,7 +239,7 @@ done test "$cron_complete" = 1 printf '%s\n' "$app_password" | runuser -u odoo -- python3 -c \ - 'import sys; import odoo; odoo.tools.config.parse_config(["--config=/etc/odoo/odoo.conf"]); assert odoo.tools.config.verify_admin_password(sys.stdin.readline().rstrip("\n"))' + 'import sys; from odoo.tools import config; config.parse_config(["--config=/etc/odoo/odoo.conf"]); assert config.verify_admin_password(sys.stdin.readline().rstrip("\n"))' odoo-update --check >"$work/update" candidate=$(sed -n 's/^candidate=//p' "$work/update") From c4e657cb9beec0cdbd7e5ff0e185e90b5d6b0a48 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 17:03:27 +0000 Subject: [PATCH 10/25] Fix Odoo 19 firstboot config import --- docs/v19.0-testing.md | 9 +++++++++ overlay/usr/lib/inithooks/bin/odoo.py | 10 +++++----- tests/v19.sh | 3 +++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 727157f..fb0ca45 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -109,6 +109,15 @@ imports `config` explicitly at both call sites. No application assertion had run, cleanup, builder identity, and disk checks passed, and the retained manifest validated all configured-root and runtime evidence before the retry. +The test-only retry `20260826t164023z-4008888-12740` again passed build, +configured-root import, and supervised boot, then correctly detected that +`odoo.service` was inactive. Its retained journal showed the production +`40odoo` firstboot hook had the same stale top-level `odoo.tools` access and +failed after rotating the database credential. The hook now imports Odoo's +`config` object explicitly before hashing the example administrator password, +setting the database-management password, and restarting Odoo. Acceptance also +reports a failing line and shell command without expanding secret variables. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates diff --git a/overlay/usr/lib/inithooks/bin/odoo.py b/overlay/usr/lib/inithooks/bin/odoo.py index 7ae8957..5f98f30 100755 --- a/overlay/usr/lib/inithooks/bin/odoo.py +++ b/overlay/usr/lib/inithooks/bin/odoo.py @@ -48,9 +48,9 @@ def main(): blacklist=['\\', '/']) sys.path.insert(0, '/usr/lib/python3/dist-packages') - import odoo + from odoo.tools import config - processed_password = odoo.tools.config.crypt_context.hash(password) + processed_password = config.crypt_context.hash(password) default_db = 'TurnkeylinuxExample' default_db_exists = True @@ -62,9 +62,9 @@ def main(): default_db_exists = False print(f"Default DB ({default_db}) not found - skipping its password update") - odoo.tools.config.parse_config(['--config=/etc/odoo/odoo.conf']) - odoo.tools.config.set_admin_password(password) - odoo.tools.config.save() + config.parse_config(['--config=/etc/odoo/odoo.conf']) + config.set_admin_password(password) + config.save() subprocess.run(['chown', 'root:odoo', '/etc/odoo/odoo.conf'], check=True) subprocess.run(['chmod', '0640', '/etc/odoo/odoo.conf'], check=True) diff --git a/tests/v19.sh b/tests/v19.sh index be55447..54a7006 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -2,6 +2,8 @@ set -Eeuo pipefail umask 077 +trap 'status=$?; printf "odoo acceptance failed: line=%s status=%s command=%q\n" "$LINENO" "$status" "$BASH_COMMAND" >&2; exit "$status"' ERR + result=${TKL_TEST_RESULT:?TKL_TEST_RESULT is required} app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} source_file=/usr/local/share/turnkey-odoo/source @@ -61,6 +63,7 @@ authenticate() { } cleanup() { + trap - ERR set +e if [[ -n $cron_id ]]; then ocall ir.cron unlink "$(jq -cn --argjson id "$cron_id" '[[$id]]')" | From 7e562aac607d28bb5c73e6220850601beb31d932 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 18:00:21 +0000 Subject: [PATCH 11/25] Avoid Odoo namespace shadow at firstboot --- docs/v19.0-testing.md | 32 +++++++++++++++++--- overlay/usr/lib/inithooks/bin/odoo.py | 23 ++++++++++++-- overlay/usr/lib/inithooks/firstboot.d/40odoo | 5 +++ tests/v19.sh | 12 ++++++-- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index fb0ca45..cdaabe8 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -71,9 +71,8 @@ TKL_HARNESS_DOCKER_OBJECT_LABEL=org.turnkeylinux.v19-harness.wave2-2 \ Loop 1 (`20260826t143314z-3386451-1936`) passed HTTPS preflight but stopped in package resolution because Debian Trixie has no `wkhtmltopdf` package. Cleanup, -builder identity, and disk checks passed. The fix replaces that nonexistent -plan package with the exact upstream renderer described above. Loop 2 is -waiting behind the active Canvas run. +builder identity, and disk checks passed. The fix replaced that nonexistent +plan package with the exact upstream renderer described above. Loop 1 used source `6b6c43f72fa63cc4ed74075f27635936a57871d4` and harness `152bc9b876557266b90ed4ded77b611d9b817aff`. Its source commit @@ -93,7 +92,7 @@ archive SHA-256 was the staged input tree SHA-256 was `70f30087f61d835cb3ce034eb565043a6a3f4442b1d1e15fef8da3c318a39a59`. The retained loop 2 manifest also validated every retained file before the -compatibility fix. Loop 3 is waiting for the scoped builder. +compatibility fix. A disposable `debian:trixie-slim` probe installed Debian `python3-pypdf` `5.4.0-1`, the generated compatibility provider, and the exact @@ -118,6 +117,31 @@ failed after rotating the database credential. The hook now imports Odoo's setting the database-management password, and restarting Odoo. Acceptance also reports a failing line and shell command without expanding secret variables. +The production retry `20260826t172526z-4178915-31801` proved that the hook's +own `odoo.py` filename can shadow the installed package in the exact inithooks +layout. The loader now removes its script directory from module search and +evicts only a same-file `odoo` module before importing the official package. +The same retained journal showed Apache racing firstboot TLS replacement: it +started, exited while the certificate was being regenerated, and the common +hook skipped it because it was no longer active. `40odoo` now starts Apache +after the certificate and Odoo configuration are stable. Unit failures print +the exact unit and full status in later acceptance. + +A disposable exact-layout Trixie probe installed the official Odoo package, +the pypdf compatibility provider, TurnKey `inithooks` `2.3.6`, and the exact +product hook path. With the hook deliberately pre-registered as module +`odoo`, the loader evicted the shadow and resolved the installed Odoo namespace +through Python's `NamespaceLoader`; the imported object was Odoo's real +`configmanager`. `apt-get check` and `dpkg --audit` passed. A separate +firstboot-order probe proved Apache recovery occurs only after successful Odoo +configuration and does not record the application password. All labeled probe +containers were removed. + +The product-loop ledger before the next retry is four of six: renderer package +availability, stale Odoo pypdf metadata, firstboot's Odoo 19 config API, and +the namespace-shadow plus firstboot TLS race. The explicit-config acceptance +import retry was instrumentation-only and did not consume a product loop. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates diff --git a/overlay/usr/lib/inithooks/bin/odoo.py b/overlay/usr/lib/inithooks/bin/odoo.py index 5f98f30..1eccacb 100755 --- a/overlay/usr/lib/inithooks/bin/odoo.py +++ b/overlay/usr/lib/inithooks/bin/odoo.py @@ -13,6 +13,26 @@ from libinithooks.dialog_wrapper import Dialog from pgsqlconf import PostgreSQL +ODOO_DIST_PACKAGES = '/usr/lib/python3/dist-packages' + + +def load_odoo_config(): + """Load Odoo without resolving this inithook as the ``odoo`` module.""" + script = os.path.realpath(__file__) + script_dir = os.path.dirname(script) + sys.path[:] = [ + path for path in sys.path + if os.path.realpath(path or os.curdir) != script_dir + ] + sys.path.insert(0, ODOO_DIST_PACKAGES) + + loaded = sys.modules.get('odoo') + if loaded and os.path.realpath(getattr(loaded, '__file__', '')) == script: + del sys.modules['odoo'] + + from odoo.tools import config + return config + def usage(s=None): if s: @@ -47,8 +67,7 @@ def main(): "This password will also login to 'admin' account of default/example Odoo.", blacklist=['\\', '/']) - sys.path.insert(0, '/usr/lib/python3/dist-packages') - from odoo.tools import config + config = load_odoo_config() processed_password = config.crypt_context.hash(password) diff --git a/overlay/usr/lib/inithooks/firstboot.d/40odoo b/overlay/usr/lib/inithooks/firstboot.d/40odoo index 049ac0c..632ddea 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40odoo +++ b/overlay/usr/lib/inithooks/firstboot.d/40odoo @@ -6,3 +6,8 @@ [[ -e $INITHOOKS_CONF ]] && . "$INITHOOKS_CONF" APP_PASS="$APP_PASS" "$INITHOOKS_PATH/bin/odoo.py" + +# Apache may race the firstboot TLS certificate replacement and enter failed +# state before the common regeneration hook can restart it. At this point the +# new certificate is stable and the Odoo backend has been configured. +service apache2 restart diff --git a/tests/v19.sh b/tests/v19.sh index 54a7006..b05ca2e 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -78,8 +78,16 @@ cleanup() { trap cleanup EXIT for unit in apache2.service postgresql.service odoo.service postfix.service; do - systemctl --quiet is-active "$unit" - systemctl --quiet is-enabled "$unit" + if ! systemctl --quiet is-active "$unit"; then + echo "$unit is not active" >&2 + systemctl --no-pager --full status "$unit" >&2 || true + exit 1 + fi + if ! systemctl --quiet is-enabled "$unit"; then + echo "$unit is not enabled" >&2 + systemctl --no-pager --full status "$unit" >&2 || true + exit 1 + fi done apache2ctl configtest apache_modules=$(apache2ctl -M) From aee4e49fa3f4d13560c4441371654cbd25fc02c5 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 18:18:05 +0000 Subject: [PATCH 12/25] Install Odoo JSON acceptance dependency --- docs/v19.0-testing.md | 18 ++++++++++++++++++ plan/main | 1 + tests/v19.sh | 2 ++ 3 files changed, 21 insertions(+) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index cdaabe8..e604262 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -142,6 +142,24 @@ availability, stale Odoo pypdf metadata, firstboot's Odoo 19 config API, and the namespace-shadow plus firstboot TLS race. The explicit-config acceptance import retry was instrumentation-only and did not consume a product loop. +Run `20260826t180138z-98149-31228` at source +`7e562aac607d28bb5c73e6220850601beb31d932` passed the exact build, +configured-root import, firstboot, and supervised runtime boundaries. Runtime +reported ready, multi-user active, inithooks complete, and application service +active, proving the namespace loader and late Apache recovery in the exact +appliance. Acceptance then exited with status 127 before its first JSON request +because the fixture's `jq` dependency was not installed. The plan now declares +that dependency and acceptance checks it before starting. This is another +instrumentation-only retry, so the product-loop ledger remains four of six. +Cleanup, builder identity, and disk checks passed, and the retained manifest +validated every file. The retained report SHA-256 was +`491615a0bee1a8484fbfe8c1b806a4bcd03df60d678b60a0dff878db1ddf4628`; +the run log SHA-256 was +`6419ebceb9a069b22473670adb1f9264930eed7545d7efe3155b88a9a3097f8b`. +A disposable Trixie probe installed Debian `jq` `1.7.1-6+deb13u3`, built and +queried the fixture's login JSON shape, passed `apt-get check` and +`dpkg --audit`, and removed its scoped container. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates diff --git a/plan/main b/plan/main index 7a73f58..2a0d4db 100644 --- a/plan/main +++ b/plan/main @@ -5,6 +5,7 @@ xfonts-75dpi python3-psycogreen adduser +jq /* JSON-RPC acceptance and administrative diagnostics */ postgresql-client python3 diff --git a/tests/v19.sh b/tests/v19.sh index b05ca2e..9b3edfa 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -16,6 +16,8 @@ cookie=$work/cookie partner_id= cron_id= +command -v jq >/dev/null + ocurl() { curl --insecure --fail --silent --show-error \ --cookie "$cookie" --cookie-jar "$cookie" \ From f2a9f6eeddf10af05398f8f83c4e36b148a96043 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 21:36:48 +0000 Subject: [PATCH 13/25] Set Odoo admin password through ORM --- docs/v19.0-testing.md | 32 ++++++++++++++++++ overlay/usr/lib/inithooks/bin/odoo.py | 48 +++++++++++++++++---------- tests/v19.sh | 7 ++++ 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index e604262..9abb0c3 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -160,6 +160,38 @@ A disposable Trixie probe installed Debian `jq` `1.7.1-6+deb13u3`, built and queried the fixture's login JSON shape, passed `apt-get check` and `dpkg --audit`, and removed its scoped container. +The instrumentation retry `20260826t203245z-644554-15759` waited behind +Canvas for 1,648,174 ms, then used source +`aee4e49fa3f4d13560c4441371654cbd25fc02c5`. Its source archive SHA-256 +was `bc9bb4ef550be5c91c5b5a58a8b1b7732ab076a3593ee7f5f8d1bdb50d58741f` +and its input tree SHA-256 was +`f76ac1470114285c74d5410218c9b0a5502ecdae84b96ca8eebfc5ca73d41ab1`. +Build, configured-root import, supervised boot, and the `jq` boundary passed. +The first login then failed because the retained journal showed that Odoo 19's +configuration object no longer exposes `crypt_context`; the firstboot hook had +therefore not changed the example administrator password. Cleanup, builder +identity, and disk checks passed, and the retained manifest validated every +file. The retained report SHA-256 was +`c73037eee6e3248714e1a9525b9cce45fc8fac77b6b1e3e12cd56b86d75f1b79`; +the run log SHA-256 was +`ce81aff7e6f3af6323ccac2a59274d82ee1e81103f828140b3dc68afcd83e7ad`. + +Odoo's official 19.0 +[`res.users` implementation](https://github.com/odoo/odoo/blob/19.0/odoo/addons/base/models/res_users.py) +owns password hashing, and its +[`service.db` implementation](https://github.com/odoo/odoo/blob/19.0/odoo/service/db.py) +writes `base.user_admin.password` through the ORM. The hook now follows that +supported path through Odoo's noninteractive shell, passing the cleartext only +in the child environment and never in argv or SQL. An exact-package disposable +Trixie probe initialized a real PostgreSQL database, ran the complete hook with +a password containing spaces, a dollar sign, and quotes, and proved the stored +value was a non-plaintext Odoo hash that the model's own crypt context verifies. +It also verified the database-management password, supervised Odoo restart, +`apt-get check`, `dpkg --audit`, absence of the secret from process argv, and +scoped container cleanup. Acceptance independently requires the stored +administrator credential to be hashed. This password-API correction is product +loop five of six. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates diff --git a/overlay/usr/lib/inithooks/bin/odoo.py b/overlay/usr/lib/inithooks/bin/odoo.py index 1eccacb..abd61c6 100755 --- a/overlay/usr/lib/inithooks/bin/odoo.py +++ b/overlay/usr/lib/inithooks/bin/odoo.py @@ -11,9 +11,34 @@ import sys from libinithooks.dialog_wrapper import Dialog -from pgsqlconf import PostgreSQL ODOO_DIST_PACKAGES = '/usr/lib/python3/dist-packages' +ODOO_CONFIG = '/etc/odoo/odoo.conf' +ODOO_PASSWORD_ENV = 'TURNKEY_ODOO_ADMIN_PASSWORD' + + +def set_example_admin_password(database, password): + """Set the example administrator password through Odoo's ORM.""" + environment = os.environ.copy() + environment[ODOO_PASSWORD_ENV] = password + script = f"""\ +import os +password = os.environ.pop({ODOO_PASSWORD_ENV!r}) +admin = env.ref('base.user_admin') +admin.write({{'password': password}}) +env.cr.commit() +""" + subprocess.run( + [ + '/usr/sbin/runuser', '-u', 'odoo', '--', '/usr/bin/odoo', + 'shell', f'--config={ODOO_CONFIG}', f'--database={database}', + '--no-http', + ], + input=script, + text=True, + env=environment, + check=True, + ) def load_odoo_config(): @@ -69,29 +94,18 @@ def main(): config = load_odoo_config() - processed_password = config.crypt_context.hash(password) - default_db = 'TurnkeylinuxExample' - default_db_exists = True - try: - p = PostgreSQL(default_db) - p.execute("UPDATE res_users SET password='{}' WHERE id=2".format( - processed_password).encode('utf8')) - except subprocess.CalledProcessError: - default_db_exists = False - print(f"Default DB ({default_db}) not found - skipping its password update") - - config.parse_config(['--config=/etc/odoo/odoo.conf']) + set_example_admin_password(default_db, password) + + config.parse_config([f'--config={ODOO_CONFIG}']) config.set_admin_password(password) config.save() - subprocess.run(['chown', 'root:odoo', '/etc/odoo/odoo.conf'], check=True) - subprocess.run(['chmod', '0640', '/etc/odoo/odoo.conf'], check=True) + subprocess.run(['chown', 'root:odoo', ODOO_CONFIG], check=True) + subprocess.run(['chmod', '0640', ODOO_CONFIG], check=True) # restart odoo to apply updated password subprocess.run(['service', 'odoo', 'restart'], check=True) - if not default_db_exists: - sys.exit(1) if __name__ == "__main__": main() diff --git a/tests/v19.sh b/tests/v19.sh index 9b3edfa..5e6f121 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -186,6 +186,13 @@ test "$database_owner" = odoo runuser -u postgres -- psql --no-psqlrc --tuples-only --no-align \ "$database" --command='SELECT 1 FROM res_users LIMIT 1;' | grep -Fxq 1 +admin_password_hash=$(runuser -u postgres -- psql --no-psqlrc --tuples-only \ + --no-align "$database" --command=" + SELECT password FROM res_users + WHERE id = (SELECT res_id FROM ir_model_data + WHERE module = 'base' AND name = 'user_admin');") +[[ $admin_password_hash == \$* ]] +[[ $admin_password_hash != "$app_password" ]] authenticate From fe9e7fd563737295225bc51e3e964851bbbc7b9a Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 22:45:15 +0000 Subject: [PATCH 14/25] Retain Odoo authentication diagnostics --- docs/v19.0-testing.md | 27 ++++++ tests/odoo-auth-diagnostics-fixture.sh | 128 +++++++++++++++++++++++++ tests/odoo-auth-diagnostics.sh | 120 +++++++++++++++++++++++ tests/v19.sh | 22 ++--- 4 files changed, 283 insertions(+), 14 deletions(-) create mode 100755 tests/odoo-auth-diagnostics-fixture.sh create mode 100755 tests/odoo-auth-diagnostics.sh diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 9abb0c3..117e25e 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -192,6 +192,33 @@ scoped container cleanup. Acceptance independently requires the stored administrator credential to be hashed. This password-API correction is product loop five of six. +The failed validation run `20260826t213718z-906461-9321` used source +`f2a9f6eeddf10af05398f8f83c4e36b148a96043`. Its source archive SHA-256 was +`5a360b8c3da000e5cba2ea9fcad6472fe7d3b6c6f647ab19518d977ec7424943`, +its staged input tree SHA-256 was +`7d84b1e0d7605f8bcd540cf5de222d24b39bb9c77c2ab4aeb969f8de2398f618`, +and its built tree SHA-256 was +`bf77408fa2de3b9b03f3a27eb1a291d4310609a3f0adf18dde0c498533e9d8ef`. +Build, import, supervised boot, source integrity, renderer, pypdf compatibility, +PostgreSQL ownership, and the non-plaintext example-administrator hash all +passed. The first proxied authentication request returned HTTP 500 before a +JSON-RPC result was available. The retained evidence did not include Odoo's +file log, Apache's request logs, or curl's discarded error response, so it could +not distinguish an Odoo failure from the proxy or readiness boundary. Cleanup, +builder identity, and disk checks passed; the retained manifest validated every +file. Its report SHA-256 was +`4da32708cec400bacfc327c6316438fd20ec09ff1f70df96815e919c525fee02` and +run-log SHA-256 was +`7718e99d3c831ccc94382d48596a1ceb5550d510baf58de6a66a245eba5f06d1`. + +Per the execution wish, that failed validation does not consume the remaining +product loop: the ledger remains five of six. The diagnostic fixture now keeps +the proxied response headers and body, compares the same request directly with +Odoo on `127.0.0.1:8069`, and emits secret- and session-cookie-redacted copies +plus Odoo and Apache log tails on failure. This instrumentation does not retry +the initial request or change application behavior; it exists only to classify +the failure before the final product loop is used. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates diff --git a/tests/odoo-auth-diagnostics-fixture.sh b/tests/odoo-auth-diagnostics-fixture.sh new file mode 100755 index 0000000..9eb1943 --- /dev/null +++ b/tests/odoo-auth-diagnostics-fixture.sh @@ -0,0 +1,128 @@ +#!/bin/bash +set -Eeuo pipefail + +repo_root=$(cd "$(dirname "$0")/.." && pwd -P) +fixture_root=$(mktemp -d /tmp/odoo-auth-diagnostics.XXXXXXXX) +trap 'find "$fixture_root" -depth -delete' EXIT +mkdir -p "$fixture_root/bin" "$fixture_root/work" + +app_password='fixture password with spaces and $pecial characters' +database=TurnkeylinuxExample +work=$fixture_root/work +cookie=$work/cookie +auth_proxy_url=https://proxy.invalid/web/session/authenticate +auth_direct_url=http://127.0.0.1:8069/web/session/authenticate +auth_log_files=( + "$fixture_root/odoo-server.log" + "$fixture_root/apache-access.log" + "$fixture_root/apache-error.log" +) + +cat >"$fixture_root/bin/curl" <<'EOF' +#!/bin/bash +set -Eeuo pipefail + +headers= +body= +stderr= +payload= +url= +while (($#)); do + case $1 in + --dump-header) + headers=$2 + shift 2 + ;; + --output) + body=$2 + shift 2 + ;; + --stderr) + stderr=$2 + shift 2 + ;; + --data) + payload=$2 + shift 2 + ;; + --cookie|--cookie-jar|--header|--max-time|--write-out) + shift 2 + ;; + --insecure|--silent|--show-error) + shift + ;; + *) + url=$1 + shift + ;; + esac +done + +printf '%s\t%s\n' "$url" "$payload" >>"$FIXTURE_CALLS" +: >"$stderr" +if [[ $url == https://* ]]; then + printf 'HTTP/1.1 %s Fixture\r\nSet-Cookie: session_id=fixture-session-secret; HttpOnly\r\n\r\n' \ + "${FIXTURE_PROXY_STATUS:-500}" >"$headers" + if [[ -v FIXTURE_PROXY_BODY ]]; then + printf '%s' "$FIXTURE_PROXY_BODY" >"$body" + else + printf '{"error":{"data":{"debug":"password=%s"}}}' \ + "$FIXTURE_SECRET" >"$body" + fi + printf '%s' "${FIXTURE_PROXY_STATUS:-500}" + exit "${FIXTURE_PROXY_CURL_STATUS:-0}" +fi + +printf 'HTTP/1.1 %s Fixture\r\nSet-Cookie: session_id=direct-session-secret; HttpOnly\r\n\r\n' \ + "${FIXTURE_DIRECT_STATUS:-200}" >"$headers" +if [[ -v FIXTURE_DIRECT_BODY ]]; then + printf '%s' "$FIXTURE_DIRECT_BODY" >"$body" +else + printf '%s' '{"result":{"uid":2}}' >"$body" +fi +printf '%s' "${FIXTURE_DIRECT_STATUS:-200}" +exit "${FIXTURE_DIRECT_CURL_STATUS:-0}" +EOF +chmod 0755 "$fixture_root/bin/curl" + +printf 'Odoo traceback containing %s\n' "$app_password" >"${auth_log_files[0]}" +printf 'proxy access fixture\n' >"${auth_log_files[1]}" +printf 'proxy error fixture\n' >"${auth_log_files[2]}" + +# shellcheck source=odoo-auth-diagnostics.sh +. "$repo_root/tests/odoo-auth-diagnostics.sh" + +export FIXTURE_CALLS=$fixture_root/calls +export FIXTURE_SECRET=$app_password +export PATH="$fixture_root/bin:/usr/bin:/bin" + +if authenticate diagnose >"$fixture_root/failure.out" 2>&1; then + echo 'proxy HTTP 500 unexpectedly authenticated' >&2 + exit 1 +fi +grep -Fxq 'auth_probe=proxy curl_status=0 http_status=500' "$fixture_root/failure.out" +grep -Fxq 'auth_probe=direct curl_status=0 http_status=200' "$fixture_root/failure.out" +grep -Fq -- '--- proxy response headers ---' "$fixture_root/failure.out" +grep -Fq -- '--- direct response body ---' "$fixture_root/failure.out" +grep -Fq -- '--- log tail:' "$fixture_root/failure.out" +grep -Fq '"result":{"uid":2}' "$fixture_root/failure.out" +grep -Fq 'Set-Cookie: [REDACTED]; HttpOnly' "$fixture_root/failure.out" +grep -Fq 'Odoo traceback containing [REDACTED]' "$fixture_root/failure.out" +! grep -Fq "$app_password" "$fixture_root/failure.out" +! grep -Fq 'fixture-session-secret' "$fixture_root/failure.out" +grep -Fq "$app_password" "$work/auth-proxy.body" +grep -Fq 'fixture-session-secret' "$work/auth-proxy.headers" +test "$(wc -l <"$fixture_root/calls")" -eq 2 +test "$(cut -f2 "$fixture_root/calls" | sort -u | wc -l)" -eq 1 + +rm -f "$fixture_root/calls" +rm -f "$work"/auth-* "$work"/cookie "$work"/direct-cookie +FIXTURE_PROXY_STATUS=200 \ +FIXTURE_PROXY_BODY='{"result":{"uid":2}}' \ + authenticate diagnose >"$fixture_root/success.out" 2>&1 +test "$(wc -l <"$fixture_root/calls")" -eq 1 +grep -Fq 'https://proxy.invalid/web/session/authenticate' "$fixture_root/calls" +! grep -Fq '127.0.0.1:8069' "$fixture_root/calls" +test ! -s "$fixture_root/success.out" + +echo 'odoo auth diagnostics fixture: PASS' diff --git a/tests/odoo-auth-diagnostics.sh b/tests/odoo-auth-diagnostics.sh new file mode 100755 index 0000000..b8783bc --- /dev/null +++ b/tests/odoo-auth-diagnostics.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# Diagnostic support for the Odoo authentication acceptance boundary. The +# caller owns `work`, `cookie`, `database`, and `app_password`. + +auth_proxy_url=${auth_proxy_url:-https://127.0.0.1/web/session/authenticate} +auth_direct_url=${auth_direct_url:-http://127.0.0.1:8069/web/session/authenticate} +if ! declare -p auth_log_files >/dev/null 2>&1; then + auth_log_files=( + /var/log/odoo/odoo-server.log + /var/log/apache2/access.log + /var/log/apache2/error.log + ) +fi + +sanitize_auth_diagnostic() { + APP_SECRET=$app_password perl -pe ' + BEGIN { $secret = $ENV{"APP_SECRET"} // ""; } + s/\Q$secret\E/[REDACTED]/g if length $secret; + s/^(Set-Cookie:\s*)[^;]*/${1}[REDACTED]/i; + s/(session_id=)[^;\s"]+/${1}[REDACTED]/ig; + ' +} + +capture_auth_response() { + local name=$1 + local url=$2 + local payload=$3 + local -n http_status_ref=$4 + local -n curl_status_ref=$5 + local response_cookie=$cookie + local prefix=$work/auth-$name + + if [[ $name == direct ]]; then + response_cookie=$work/direct-cookie + fi + + if http_status_ref=$(curl --insecure --silent --show-error \ + --max-time 30 \ + --cookie "$response_cookie" --cookie-jar "$response_cookie" \ + --header 'Content-Type: application/json' \ + --dump-header "$prefix.headers" \ + --output "$prefix.body" \ + --stderr "$prefix.curl-error" \ + --write-out '%{http_code}' \ + --data "$payload" "$url"); then + curl_status_ref=0 + else + curl_status_ref=$? + fi +} + +print_auth_file() { + local label=$1 + local file=$2 + + printf '%s\n' "--- $label ---" + if [[ -r $file ]]; then + sanitize_auth_diagnostic <"$file" + else + printf 'unavailable: %s\n' "$file" + fi +} + +print_auth_response() { + local name=$1 + local curl_status=$2 + local http_status=$3 + local prefix=$work/auth-$name + + printf 'auth_probe=%s curl_status=%s http_status=%s\n' \ + "$name" "$curl_status" "${http_status:-absent}" + print_auth_file "$name response headers" "$prefix.headers" + print_auth_file "$name response body" "$prefix.body" + print_auth_file "$name curl stderr" "$prefix.curl-error" +} + +print_auth_logs() { + local logfile + + for logfile in "${auth_log_files[@]}"; do + printf '%s\n' "--- log tail: $logfile ---" + if [[ -r $logfile ]]; then + if ! tail -n 240 "$logfile" 2>&1 | sanitize_auth_diagnostic; then + printf 'unable to read complete log tail: %s\n' "$logfile" + fi + else + printf 'unavailable: %s\n' "$logfile" + fi + done +} + +authenticate() { + local diagnostics=${1:-diagnose} + local payload proxy_http_status= proxy_curl_status= + local direct_http_status= direct_curl_status= + + payload=$(jq -cn \ + --arg db "$database" \ + --arg login admin \ + --arg password "$app_password" \ + '{jsonrpc:"2.0", method:"call", params:{db:$db, login:$login, password:$password}}') + capture_auth_response proxy "$auth_proxy_url" "$payload" \ + proxy_http_status proxy_curl_status + + if (( proxy_curl_status == 0 )) && [[ $proxy_http_status == 200 ]] && + jq -e '.result.uid == 2 and (.error | not)' \ + "$work/auth-proxy.body" >/dev/null; then + return 0 + fi + + if [[ $diagnostics == diagnose ]]; then + print_auth_response proxy "$proxy_curl_status" "$proxy_http_status" + capture_auth_response direct "$auth_direct_url" "$payload" \ + direct_http_status direct_curl_status + print_auth_response direct "$direct_curl_status" "$direct_http_status" + print_auth_logs + fi + return 1 +} diff --git a/tests/v19.sh b/tests/v19.sh index 5e6f121..88d6f97 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -16,6 +16,10 @@ cookie=$work/cookie partner_id= cron_id= +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) +# shellcheck source=odoo-auth-diagnostics.sh +. "$script_dir/odoo-auth-diagnostics.sh" + command -v jq >/dev/null ocurl() { @@ -51,19 +55,6 @@ ocall() { "https://127.0.0.1/web/dataset/call_kw/$model/$method" } -authenticate() { - local payload response - - payload=$(jq -cn \ - --arg db "$database" \ - --arg login admin \ - --arg password "$app_password" \ - '{jsonrpc:"2.0", method:"call", params:{db:$db, login:$login, password:$password}}') - response=$(ocurl --data "$payload" \ - https://127.0.0.1/web/session/authenticate) - jq -e '.result.uid == 2 and (.error | not)' <<<"$response" >/dev/null -} - cleanup() { trap - ERR set +e @@ -215,12 +206,15 @@ systemctl restart postgresql.service systemctl restart odoo.service ready= for attempt in {1..60}; do - if authenticate >/dev/null 2>&1; then + if authenticate quiet >/dev/null 2>&1; then ready=1 break fi sleep 2 done +if [[ $ready != 1 ]]; then + authenticate diagnose || true +fi test "$ready" = 1 main_pid=$(systemctl show odoo.service --property=MainPID --value) From 42d8365bb7653d0f7d8c822a7f82a4cb1c1a2e1e Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 23:57:41 +0000 Subject: [PATCH 15/25] Fix Odoo service home environment --- docs/v19.0-testing.md | 46 ++++++++++++++++--- .../system/odoo.service.d/turnkey.conf | 1 + overlay/usr/lib/inithooks/bin/odoo.py | 2 +- .../inithooks/firstboot.d/20-odoo-db-secrets | 2 +- tests/v19.sh | 7 +++ 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 117e25e..ce10b4e 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -211,13 +211,45 @@ file. Its report SHA-256 was run-log SHA-256 was `7718e99d3c831ccc94382d48596a1ceb5550d510baf58de6a66a245eba5f06d1`. -Per the execution wish, that failed validation does not consume the remaining -product loop: the ledger remains five of six. The diagnostic fixture now keeps -the proxied response headers and body, compares the same request directly with -Odoo on `127.0.0.1:8069`, and emits secret- and session-cookie-redacted copies -plus Odoo and Apache log tails on failure. This instrumentation does not retry -the initial request or change application behavior; it exists only to classify -the failure before the final product loop is used. +Per the execution wish, that failed validation did not consume the remaining +product loop: the ledger remained five of six. The diagnostic fixture keeps the +proxied response headers and body, compares the same request directly with Odoo +on `127.0.0.1:8069`, and emits secret- and session-cookie-redacted copies plus +Odoo and Apache log tails on failure. This instrumentation does not retry the +initial request or change application behavior. + +The diagnostic candidate `fe9e7fd563737295225bc51e3e964851bbbc7b9a` +then ran as `20260826t224548z-1167796-3851`, after waiting 2,199,151 ms behind +GitLab and Canvas. The proxied and direct authentication requests both returned +HTTP 500, excluding Apache. Odoo's retained traceback identified the product +boundary before JSON-RPC dispatch: its worker resolved `config.session_dir` +below `/root/.local` and could not create it as user `odoo`. The firstboot hook +had restarted the SysV service directly from a root process, so that daemon +inherited root's home instead of the service account's home. The retained +manifest validated every evidence file; report SHA-256 was +`5339281f27422065ec503b15fef8c226f4645f2553a171bdab1468c89cbaa830` and +run-log SHA-256 was +`bef2b23e5afb642d4e22c3f42de539a888ea6e8e30946687fab6b60ee99d4bc3`. +Cleanup, builder identity, and disk checks passed. + +The final product correction sets `HOME=/var/lib/odoo` on the supervised Odoo +service and makes firstboot restart that unit through systemd. All service +starts therefore derive Odoo's XDG data and session paths below the `odoo` +account's writable home. Acceptance checks both the live main process +environment and the owner/mode of the session directory created by a real +login. This service-environment correction is product loop six of six. + +A disposable `debian:trixie-slim` systemd runtime then installed the exact +`odoo` `19.0.20260825` package after validating its pinned SHA-256, Trixie's +PostgreSQL 17, and the pinned `python3-pypdf2` compatibility provider. It +initialized `TurnkeylinuxExample`, set the administrator password through the +official Odoo ORM, and proved all of the final correction's boundaries: +`systemctl show` and `/proc//environ` both reported +`HOME=/var/lib/odoo`; the main process ran as `odoo`; direct JSON-RPC +authentication returned UID 2 before and after a supervised restart; and Odoo +created `/var/lib/odoo/.local/share/Odoo/sessions` as `odoo:odoo` mode `0700`. +`apt-get check` passed, and the probe's uniquely labelled containers and image +were removed after the run. ## Deferred issues diff --git a/overlay/etc/systemd/system/odoo.service.d/turnkey.conf b/overlay/etc/systemd/system/odoo.service.d/turnkey.conf index 75cf981..70696e0 100644 --- a/overlay/etc/systemd/system/odoo.service.d/turnkey.conf +++ b/overlay/etc/systemd/system/odoo.service.d/turnkey.conf @@ -3,4 +3,5 @@ Requires=postgresql.service After=postgresql.service [Service] +Environment=HOME=/var/lib/odoo Environment=ODOO_NOTIFY_CRON_CHANGES=1 diff --git a/overlay/usr/lib/inithooks/bin/odoo.py b/overlay/usr/lib/inithooks/bin/odoo.py index abd61c6..9249d6f 100755 --- a/overlay/usr/lib/inithooks/bin/odoo.py +++ b/overlay/usr/lib/inithooks/bin/odoo.py @@ -104,7 +104,7 @@ def main(): subprocess.run(['chmod', '0640', ODOO_CONFIG], check=True) # restart odoo to apply updated password - subprocess.run(['service', 'odoo', 'restart'], check=True) + subprocess.run(['systemctl', 'restart', 'odoo.service'], check=True) if __name__ == "__main__": diff --git a/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets b/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets index 5f11b7a..5d875ec 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets +++ b/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets @@ -24,4 +24,4 @@ EOF # Restart the database before Odoo so every application connection is made # with the rotated credential. service postgresql restart -service odoo restart +systemctl restart odoo.service diff --git a/tests/v19.sh b/tests/v19.sh index 88d6f97..f9ac11a 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -108,6 +108,11 @@ PY service_environment=$(systemctl show odoo.service --property=Environment --value) [[ $service_environment == *ODOO_NOTIFY_CRON_CHANGES=1* ]] +[[ $service_environment == *HOME=/var/lib/odoo* ]] +service_main_pid=$(systemctl show odoo.service --property=MainPID --value) +[[ $service_main_pid =~ ^[1-9][0-9]*$ ]] +tr '\0' '\n' <"/proc/$service_main_pid/environ" | + grep -Fxq 'HOME=/var/lib/odoo' # shellcheck disable=SC1090 . "$source_file" @@ -186,6 +191,8 @@ admin_password_hash=$(runuser -u postgres -- psql --no-psqlrc --tuples-only \ [[ $admin_password_hash != "$app_password" ]] authenticate +test "$(stat -c '%U:%G:%a' /var/lib/odoo/.local/share/Odoo/sessions)" = \ + odoo:odoo:700 contact_args=$(jq -cn --arg name "$fixture" --arg email "$email" \ '[{name:$name, email:$email}]') From 031e4681ad7ac6f0db17004aa444866411c27d54 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 01:10:48 +0000 Subject: [PATCH 16/25] Document final Odoo v19 blocker evidence --- docs/v19.0-testing.md | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index ce10b4e..fe5b720 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -251,6 +251,50 @@ created `/var/lib/odoo/.local/share/Odoo/sessions` as `odoo:odoo` mode `0700`. `apt-get check` passed, and the probe's uniquely labelled containers and image were removed after the run. +The final exact run `20260826t235821z-1471535-357` used source +`42d8365bb7653d0f7d8c822a7f82a4cb1c1a2e1e` and pinned harness +`152bc9b876557266b90ed4ded77b611d9b817aff`. Its source commit and transport +archive SHA-256 was +`f5140cee6032ef649c6cd63635251b4f1d7927b3118660fcf3357a0455391eb2`; +the staged input tree SHA-256 was +`b4e6336379583c69ba7b8b313ac51fd22e139dfce1c30409c331e2cb4b8c370a`; +and the built tree SHA-256 was +`df796578f7afea8784d32ff63d5098821ce5f8370284db3715982df4b525bdd0`. +Build, configured-root import, supervised boot, and runtime health passed. The +acceptance checks before authentication also proved that the systemd drop-in +was loaded: both the unit environment and the live Odoo main process reported +`HOME=/var/lib/odoo`. The first proxied authentication request and the same +request sent directly to Odoo on `127.0.0.1:8069` then both returned HTTP 500, +excluding Apache from the failure boundary. + +The retained Odoo traceback shows that the HTTP worker nevertheless tried to +create its session directory below `/root/.local`. The residual cause is the +root-run `40odoo` inithook: importing and parsing Odoo's configuration derives +the default `data_dir` from the hook process's root home, and its unscoped +`config.save()` persists that root-derived value in `/etc/odoo/odoo.conf`. +After the hook restarts the correctly supervised unit, that explicit +configuration value overrides the effective service `HOME`. This is a product +failure in firstboot configuration serialization, not a systemd-drop-in, +Apache, or acceptance-fixture failure. + +The sealed retained evidence is under +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/failures/odoo/current`. +Its validating `RETAINED-SHA256SUMS` manifest has SHA-256 +`4e155f9f6dbaf7b360a3f69dee4b65b1e0341e0deef22deb4b831b18f190b696`; +the retained report has SHA-256 +`ab307a552f53b0f7c3872d83dc64064efb77d47ca2169c8725b22958f2db19b2`; +and the retained run log has SHA-256 +`3a59883f7590196d97959be452197ed93b3242291cb679d954eb25f6e47c341a`. +The retained manifest validates every retained object. Harness cleanup, +builder-identity, and disk checks all exited zero, and the report records Docker +usage falling from 50,863,747,072 to 50,862,923,776 bytes. + +This failed final product correction consumes product loop six of six. The +Odoo v19 execution lifecycle is **BLOCKED** with the task left in progress; +changing the root inithook's configuration-save behavior or running another +product candidate requires an explicit wish amendment and a renewed loop +allowance. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates From 84b004fa2c3d9d7cce652cd8c6922402d5fe3524 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 01:35:27 +0000 Subject: [PATCH 17/25] Restore Odoo's Trixie PDF dependency alternative Odoo's official 19 package collapses its source-declared python3-pypdf2 or python3-pypdf dependency to the package name absent from Trixie. Repack the exact hash-verified official artifact by changing only that control dependency while preserving the upstream data archive byte-for-byte. Use the same deterministic transformation for supervised updates, require an approved repacked hash before installation, and retain upstream, payload, and repack provenance. Positive and negative source, control, payload, signed-download, prepare, and apply fixtures pass alongside shell syntax, Python compile and AST, diff, evidence-schema, documentation, and signed-feed checks. --- README.rst | 6 +- changelog | 7 +- conf.d/main | 16 +- docs/v19.0-testing.md | 25 ++- overlay/usr/local/sbin/odoo-repack-package | 240 +++++++++++++++++++++ overlay/usr/local/sbin/odoo-update | 217 +++++++++++++++++-- plan/main | 2 + tests/repack-fixture.sh | 137 ++++++++++++ tests/v19.sh | 14 +- 9 files changed, 626 insertions(+), 38 deletions(-) create mode 100755 overlay/usr/local/sbin/odoo-repack-package create mode 100755 tests/repack-fixture.sh diff --git a/README.rst b/README.rst index 1c1828f..6494ba6 100644 --- a/README.rst +++ b/README.rst @@ -19,8 +19,10 @@ This appliance includes all the standard features in `TurnKey Core`_: - **Security note**: Odoo application updates require supervision and are not configured to install automatically. Check the official daily channel with - ``odoo-update --check``, back up the database, then apply the selected APT - update. + ``odoo-update --check`` and back up the database. Prepare a compatible, + integrity-checked package with ``odoo-update --prepare OUTPUT.deb``, inspect + its reported hashes, then install the selected update and advance its + provenance record with ``odoo-update --apply REPACKED_SHA256``. - SSL support out of the box. - `Adminer`_ administration frontend for PostgreSQL (listening on diff --git a/changelog b/changelog index 5991275..15cd34e 100644 --- a/changelog +++ b/changelog @@ -1,7 +1,9 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low * Install supported Odoo 19 Community from its official daily package - channel with an exact package digest and bound repository key. + channel with an exact package digest and bound repository key. Preserve + the upstream payload while restoring its source-declared Trixie PDF + dependency alternative in package control metadata. * Use a non-superuser PostgreSQL application role, load its generated credential from the protected Odoo config during initialization, and keep @@ -12,7 +14,8 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low its gevent worker. * Add a non-mutating update check, v19 acceptance coverage, and README - evidence crosswalk. + evidence crosswalk. Prepare supervised updates through the same verified, + payload-preserving control correction used at build time. * Upgrade the base distribution to Debian 13 Trixie. diff --git a/conf.d/main b/conf.d/main index 740e60f..d90d4c9 100755 --- a/conf.d/main +++ b/conf.d/main @@ -12,7 +12,9 @@ SOURCE_RECORD=/usr/local/share/turnkey-odoo/source key=$(mktemp) package=$(mktemp --suffix=.deb) -trap 'find "$key" "$package" -maxdepth 0 -type f -delete' EXIT +repacked_package=$(mktemp --suffix=.deb) +repack_metadata=$(mktemp) +trap 'find "$key" "$package" "$repacked_package" "$repack_metadata" -maxdepth 0 -type f -delete' EXIT curl -fsSL "$ODOO_KEY_URL" -o "$key" echo "$ODOO_KEY_SHA256 $key" | sha256sum -c - test "$(gpg --show-keys --with-colons "$key" | awk -F: '$1 == "fpr" { print $10; exit }')" = "$ODOO_KEY_FINGERPRINT" @@ -34,7 +36,12 @@ EOF apt-get update curl -fsSL "$ODOO_PACKAGE_URL" -o "$package" echo "$ODOO_PACKAGE_SHA256 $package" | sha256sum -c - -DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" +find "$repacked_package" -maxdepth 0 -type f -delete +odoo-repack-package "$package" "$repacked_package" >"$repack_metadata" +# shellcheck disable=SC1090 +. "$repack_metadata" +test "$upstream_package_sha256" = "$ODOO_PACKAGE_SHA256" +DEBIAN_FRONTEND=noninteractive apt-get install -y "$repacked_package" test "$(dpkg-query -W -f='${Version}' odoo)" = "$ODOO_VERSION" mkdir -p "$(dirname "$SOURCE_RECORD")" @@ -42,7 +49,10 @@ cat >"$SOURCE_RECORD" <\n" +DEPENDENCY = b"python3-pypdf2 | python3-pypdf" +COLLAPSED = re.compile( + rb"(?10}".encode("ascii") + if len(encoded_size) != 10: + fail("rewritten control archive is too large") + rewritten_header[48:58] = encoded_size + output.write(rewritten_header) + output.write(payload) + if len(payload) % 2: + output.write(b"\n") + continue + source.seek(offset - 60) + remaining = 60 + size + size % 2 + while remaining: + chunk = source.read(min(1024 * 1024, remaining)) + if not chunk: + fail("truncated ar archive while copying payload") + output.write(chunk) + remaining -= len(chunk) + + +def member_sha256(path, member): + digest = hashlib.sha256() + _, _, offset, size = member + with path.open("rb") as archive: + archive.seek(offset) + remaining = size + while remaining: + chunk = archive.read(min(1024 * 1024, remaining)) + if not chunk: + fail("truncated payload member") + digest.update(chunk) + remaining -= len(chunk) + return digest.hexdigest() + + +def main(): + if len(sys.argv) != 3: + fail("usage: odoo-repack-package INPUT.deb OUTPUT.deb") + source_path = Path(sys.argv[1]).resolve() + output_path = Path(sys.argv[2]).resolve() + if not source_path.is_file(): + fail("input package is not a regular file") + if source_path == output_path or output_path.exists(): + fail("output path must be new and distinct from input") + + members = ar_members(source_path) + control_members = [member for member in members if member[0].startswith("control.tar.")] + data_members = [member for member in members if member[0].startswith("data.tar.")] + if len(control_members) != 1 or len(data_members) != 1: + fail("package must contain one control archive and one data archive") + control_member = control_members[0] + original_control = decompressed_control( + control_member[0], read_member(source_path, control_member) + ) + rebuilt_control = compressed_control( + control_member[0], rebuild_control_tar(original_control) + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=output_path.parent) as temporary: + temporary_output = Path(temporary) / output_path.name + write_ar( + source_path, + temporary_output, + members, + control_member[0], + rebuilt_control, + ) + rewritten_members = ar_members(temporary_output) + rewritten_data = [ + member for member in rewritten_members if member[0].startswith("data.tar.") + ][0] + original_payload_sha256 = member_sha256(source_path, data_members[0]) + if member_sha256(temporary_output, rewritten_data) != original_payload_sha256: + fail("upstream payload changed during repack") + os.replace(temporary_output, output_path) + + print(f"upstream_package_sha256={sha256(source_path)}") + print(f"repacked_package_sha256={sha256(output_path)}") + print(f"upstream_payload_sha256={original_payload_sha256}") + print("dependency_rewrite=python3-pypdf2_to_python3-pypdf2_or_python3-pypdf") + + +if __name__ == "__main__": + main() diff --git a/overlay/usr/local/sbin/odoo-update b/overlay/usr/local/sbin/odoo-update index ded6cfe..d54aa7b 100755 --- a/overlay/usr/local/sbin/odoo-update +++ b/overlay/usr/local/sbin/odoo-update @@ -1,38 +1,209 @@ #!/bin/bash set -Eeuo pipefail -if [[ ${1:-} != --check || $# -ne 1 ]]; then - echo "usage: odoo-update --check" >&2 - exit 2 -fi - source_record=/usr/local/share/turnkey-odoo/source keyring=/usr/share/keyrings/odoo-archive-keyring.gpg +repack_command=/usr/local/sbin/odoo-repack-package -# shellcheck disable=SC1090 -. "$source_record" +usage() { + echo "usage: odoo-update --check | --prepare OUTPUT.deb | --apply EXPECTED_SHA256" >&2 + exit 2 +} -installed=$(dpkg-query -W -f='${Version}' odoo) -candidate=$(apt-cache policy odoo | awk '/Candidate:/ { print $2; exit }') -fingerprint=$(gpg --show-keys --with-colons "$keyring" | - awk -F: '$1 == "fpr" { print $10; exit }') +load_channel() { + # shellcheck disable=SC1090 + . "$source_record" -test "$installed" = "$installed_version" -test "$fingerprint" = "$repository_key_fingerprint" -test "$candidate" != "(none)" -grep -Fq 'https://nightly.odoo.com/19.0/nightly/deb/ ./' \ - /etc/apt/sources.list.d/odoo.list + installed=$(dpkg-query -W -f='${Version}' odoo) + candidate=$(apt-cache policy odoo | awk '/Candidate:/ { print $2; exit }') + fingerprint=$(gpg --show-keys --with-colons "$keyring" | + awk -F: '$1 == "fpr" { print $10; exit }') -if dpkg --compare-versions "$candidate" eq "$installed"; then - status=up-to-date -else - status=supervised-update-available -fi + test "$installed" = "$installed_version" + test "$fingerprint" = "$repository_key_fingerprint" + test "$candidate" != "(none)" + grep -Fq 'https://nightly.odoo.com/19.0/nightly/deb/ ./' \ + /etc/apt/sources.list.d/odoo.list +} + +signed_candidate_sha256() { + apt-cache show "odoo=$candidate" | + awk '/^SHA256:/ { print $2; exit }' +} -cat <&2 + return 1 + } + work=$(mktemp -d /tmp/odoo-update.XXXXXXXX) + if ! download_candidate "$work"; then + find "$work" -depth -delete + return 1 + fi + mapfile -t packages < <(find "$work" -maxdepth 1 -type f -name 'odoo_*.deb') + if [[ ${#packages[@]} -ne 1 ]]; then + echo "odoo-update: expected one downloaded Odoo package" >&2 + find "$work" -depth -delete + return 1 + fi + package=${packages[0]} + expected_sha=$(signed_candidate_sha256) + actual_sha=$(sha256sum "$package" | awk '{ print $1 }') + if [[ -z $expected_sha || $actual_sha != "$expected_sha" ]]; then + echo "odoo-update: downloaded package does not match signed metadata" >&2 + find "$work" -depth -delete + return 1 + fi + + prepared=$work/prepared.deb + metadata=$work/repack-metadata + if ! "$repack_command" "$package" "$prepared" >"$metadata"; then + find "$work" -depth -delete + return 1 + fi + # shellcheck disable=SC1090 + . "$metadata" + test "$upstream_package_sha256" = "$expected_sha" + mv "$prepared" "$output" + find "$work" -depth -delete + + cat <&2 + return 1 + } + temporary=$(mktemp "${source_record}.XXXXXXXX") + cat >"$temporary" <&2 + return 1 + } + work=$(mktemp -d /tmp/odoo-update-apply.XXXXXXXX) + package=$work/candidate.deb + metadata=$work/metadata + if ! prepare_candidate "$package" >"$metadata"; then + find "$work" -depth -delete + return 1 + fi + # shellcheck disable=SC1090 + . "$metadata" + if [[ $repacked_package_sha256 != "$expected_repacked_sha" ]]; then + echo 'odoo-update: prepared package does not match approved SHA-256' >&2 + find "$work" -depth -delete + return 1 + fi + if ! install_package "$package"; then + find "$work" -depth -delete + return 1 + fi + installed_after=$(installed_version_after_install) + test "$installed_after" = "$candidate" + record_installation "$candidate" "$upstream_package_sha256" \ + "$repacked_package_sha256" "$upstream_payload_sha256" \ + "$dependency_rewrite" + find "$work" -depth -delete + cat < python3-psycogreen +python3-pypdf +zstd adduser postgresql-client diff --git a/tests/repack-fixture.sh b/tests/repack-fixture.sh new file mode 100755 index 0000000..3bac6d6 --- /dev/null +++ b/tests/repack-fixture.sh @@ -0,0 +1,137 @@ +#!/bin/bash +set -Eeuo pipefail +umask 077 + +repo=$(cd "$(dirname "$0")/.." && pwd) +repack=$repo/overlay/usr/local/sbin/odoo-repack-package +updater=$repo/overlay/usr/local/sbin/odoo-update +work=$(mktemp -d /tmp/odoo-repack-fixture.XXXXXXXX) +trap 'find "$work" -depth -delete' EXIT + +make_package() { + local name=$1 + local dependency=$2 + local output=$3 + local root=$work/root-$name-$(basename "$output") + mkdir -p "$root/DEBIAN" "$root/usr/share/odoo" + chmod 0755 "$root" "$root/DEBIAN" "$root/usr" \ + "$root/usr/share" "$root/usr/share/odoo" + printf '%s\n' \ + "Package: $name" \ + 'Version: 19.0.20990101' \ + 'Architecture: all' \ + 'Maintainer: Odoo S.A. ' \ + "Depends: bash, $dependency" \ + 'Description: Odoo repack fixture' >"$root/DEBIAN/control" + printf '%s\n' '#!/bin/sh' 'exit 0' >"$root/DEBIAN/postinst" + chmod 0755 "$root/DEBIAN/postinst" + printf '%s\n' 'official-upstream-payload' >"$root/usr/share/odoo/payload" + dpkg-deb --build --root-owner-group -Zxz "$root" "$output" >/dev/null +} + +expect_repack_failure() { + local input=$1 + local output=$2 + if "$repack" "$input" "$output" >"$work/unexpected-output" 2>"$work/expected-error"; then + echo "fixture unexpectedly repacked $input" >&2 + exit 1 + fi + test ! -e "$output" +} + +original=$work/odoo-original.deb +repacked_one=$work/odoo-repacked-one.deb +repacked_two=$work/odoo-repacked-two.deb +make_package odoo python3-pypdf2 "$original" +"$repack" "$original" "$repacked_one" >"$work/metadata-one" +"$repack" "$original" "$repacked_two" >"$work/metadata-two" +test "$(sha256sum "$repacked_one" | awk '{ print $1 }')" = \ + "$(sha256sum "$repacked_two" | awk '{ print $1 }')" +dpkg-deb -f "$repacked_one" Depends | + grep -Fxq 'bash, python3-pypdf2 | python3-pypdf' +original_fields=$(dpkg-deb -f "$original") +repacked_fields=$(dpkg-deb -f "$repacked_one") +test "${repacked_fields/python3-pypdf2 | python3-pypdf/python3-pypdf2}" = \ + "$original_fields" +mkdir "$work/original-control" "$work/repacked-control" +dpkg-deb --control "$original" "$work/original-control" +dpkg-deb --control "$repacked_one" "$work/repacked-control" +test "$(sha256sum "$work/original-control/postinst" | awk '{ print $1 }')" = \ + "$(sha256sum "$work/repacked-control/postinst" | awk '{ print $1 }')" +test "$(dpkg-deb --fsys-tarfile "$original" | sha256sum | awk '{ print $1 }')" = \ + "$(dpkg-deb --fsys-tarfile "$repacked_one" | sha256sum | awk '{ print $1 }')" +grep -Fxq 'dependency_rewrite=python3-pypdf2_to_python3-pypdf2_or_python3-pypdf' \ + "$work/metadata-one" + +already_fixed=$work/odoo-already-fixed.deb +missing_dependency=$work/odoo-missing-dependency.deb +wrong_package=$work/not-odoo.deb +make_package odoo 'python3-pypdf2 | python3-pypdf' "$already_fixed" +make_package odoo python3-requests "$missing_dependency" +make_package other python3-pypdf2 "$wrong_package" +expect_repack_failure "$already_fixed" "$work/already-fixed-output.deb" +expect_repack_failure "$missing_dependency" "$work/missing-output.deb" +expect_repack_failure "$wrong_package" "$work/wrong-package-output.deb" + +# Exercise the updater's signed-download boundary with a deterministic fixture. +# shellcheck disable=SC1090 +. "$updater" +repack_command=$repack +candidate=19.0.20990101 +fixture_download=$original +download_candidate() { + cp "$fixture_download" "$1/odoo_${candidate}_all.deb" +} +signed_candidate_sha256() { + sha256sum "$fixture_download" | awk '{ print $1 }' +} +prepare_candidate "$work/updater-prepared.deb" >"$work/updater-result" +dpkg-deb -f "$work/updater-prepared.deb" Depends | + grep -Fxq 'bash, python3-pypdf2 | python3-pypdf' +grep -Fxq "candidate=$candidate" "$work/updater-result" + +signed_candidate_sha256() { + printf '%064d\n' 0 +} +if prepare_candidate "$work/updater-rejected.deb" >"$work/rejected-result" 2>"$work/rejected-error"; then + echo 'updater accepted a package that did not match signed metadata' >&2 + exit 1 +fi +test ! -e "$work/updater-rejected.deb" + +approved_sha=$(sha256sum "$repacked_one" | awk '{ print $1 }') +prepare_candidate() { + cp "$repacked_one" "$1" + printf '%s\n' \ + "candidate=$candidate" \ + "upstream_package_sha256=$(sha256sum "$original" | awk '{ print $1 }')" \ + "repacked_package_sha256=$approved_sha" \ + 'upstream_payload_sha256=fixture-payload-sha256' \ + 'dependency_rewrite=python3-pypdf2_to_python3-pypdf2_or_python3-pypdf' \ + "prepared=$1" +} +install_package() { + test -f "$1" + applied_package=1 +} +installed_version_after_install() { + printf '%s\n' "$candidate" +} +record_installation() { + recorded_installation="$*" +} +applied_package= +recorded_installation= +apply_candidate "$approved_sha" >"$work/apply-result" +test "$applied_package" = 1 +test -n "$recorded_installation" +grep -Fxq "installed=$candidate" "$work/apply-result" + +applied_package= +if apply_candidate "$(printf '%064d' 0)" >"$work/apply-rejected" 2>"$work/apply-error"; then + echo 'updater applied a package without the approved repacked hash' >&2 + exit 1 +fi +test -z "$applied_package" + +printf '%s\n' 'odoo-repack-fixture=PASS' diff --git a/tests/v19.sh b/tests/v19.sh index c9f6b22..a801862 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -55,7 +55,15 @@ done . "$source_file" test "$installed_version" = 19.0.20260825 test "$(dpkg-query -W -f='${Version}' odoo)" = "$installed_version" -test "$package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 +test "$upstream_package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 +test -n "$repacked_package_sha256" +test -n "$upstream_payload_sha256" +test "$dependency_rewrite" = \ + python3-pypdf2_to_python3-pypdf2_or_python3-pypdf +test "$(dpkg-query -W -f='${Status}' python3-pypdf)" = \ + 'install ok installed' +dpkg-query -W -f='${Depends}' odoo | + grep -Fq 'python3-pypdf2 | python3-pypdf' test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.gpg | awk -F: '$1 == "fpr" { print $10; exit }')" = \ "$repository_key_fingerprint" @@ -157,7 +165,7 @@ candidate=$(sed -n 's/^candidate=//p' "$work/update") status=$(sed -n 's/^status=//p' "$work/update") test -n "$candidate" grep -Fxq 'channel=official-odoo-19-community-daily' "$work/update" -grep -Fxq "integrity=APT-signed-by-$repository_key_fingerprint" "$work/update" +grep -Fxq "integrity=APT-signed-by-$repository_key_fingerprint; upstream-payload-preserved-by-control-only-repack" "$work/update" cat >"$result" < Date: Thu, 27 Aug 2026 02:01:31 +0000 Subject: [PATCH 18/25] Keep updater download chatter out of metadata The supervised apply path sources prepare output as machine-readable metadata, while apt-get download can emit progress on stdout. Redirect downloader stdout to stderr at the prepare boundary so network chatter cannot contaminate or execute from the metadata file. Exercise representative progress and assignment-shaped chatter through the real prepare and approved-hash apply paths. The focused fixture verifies the six-key metadata schema, stream separation, non-execution, deterministic repack gates, signed-hash rejection, and apply hash rejection. --- overlay/usr/local/sbin/odoo-update | 2 +- tests/repack-fixture.sh | 33 ++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/overlay/usr/local/sbin/odoo-update b/overlay/usr/local/sbin/odoo-update index d54aa7b..0de4012 100755 --- a/overlay/usr/local/sbin/odoo-update +++ b/overlay/usr/local/sbin/odoo-update @@ -53,7 +53,7 @@ prepare_candidate() { return 1 } work=$(mktemp -d /tmp/odoo-update.XXXXXXXX) - if ! download_candidate "$work"; then + if ! download_candidate "$work" >&2; then find "$work" -depth -delete return 1 fi diff --git a/tests/repack-fixture.sh b/tests/repack-fixture.sh index 3bac6d6..03c4e56 100755 --- a/tests/repack-fixture.sh +++ b/tests/repack-fixture.sh @@ -80,15 +80,31 @@ repack_command=$repack candidate=19.0.20990101 fixture_download=$original download_candidate() { + printf '%s\n' \ + 'Get:1 https://nightly.odoo.com/19.0/nightly/deb odoo fixture' \ + 'download_chatter_executed=1' cp "$fixture_download" "$1/odoo_${candidate}_all.deb" } signed_candidate_sha256() { sha256sum "$fixture_download" | awk '{ print $1 }' } -prepare_candidate "$work/updater-prepared.deb" >"$work/updater-result" +prepare_candidate "$work/updater-prepared.deb" \ + >"$work/updater-result" 2>"$work/updater-chatter" dpkg-deb -f "$work/updater-prepared.deb" Depends | grep -Fxq 'bash, python3-pypdf2 | python3-pypdf' grep -Fxq "candidate=$candidate" "$work/updater-result" +grep -Fq 'Get:1 https://nightly.odoo.com/' "$work/updater-chatter" +grep -Fxq 'download_chatter_executed=1' "$work/updater-chatter" +if grep -Eq '^(Get:|download_chatter_executed=)' "$work/updater-result"; then + echo 'download chatter contaminated updater metadata' >&2 + exit 1 +fi +test "$(wc -l <"$work/updater-result")" = 6 +cut -d= -f1 "$work/updater-result" | sort >"$work/updater-result-keys" +printf '%s\n' candidate dependency_rewrite prepared repacked_package_sha256 \ + upstream_package_sha256 upstream_payload_sha256 | sort \ + >"$work/expected-result-keys" +cmp "$work/expected-result-keys" "$work/updater-result-keys" signed_candidate_sha256() { printf '%064d\n' 0 @@ -100,15 +116,8 @@ fi test ! -e "$work/updater-rejected.deb" approved_sha=$(sha256sum "$repacked_one" | awk '{ print $1 }') -prepare_candidate() { - cp "$repacked_one" "$1" - printf '%s\n' \ - "candidate=$candidate" \ - "upstream_package_sha256=$(sha256sum "$original" | awk '{ print $1 }')" \ - "repacked_package_sha256=$approved_sha" \ - 'upstream_payload_sha256=fixture-payload-sha256' \ - 'dependency_rewrite=python3-pypdf2_to_python3-pypdf2_or_python3-pypdf' \ - "prepared=$1" +signed_candidate_sha256() { + sha256sum "$fixture_download" | awk '{ print $1 }' } install_package() { test -f "$1" @@ -122,9 +131,11 @@ record_installation() { } applied_package= recorded_installation= -apply_candidate "$approved_sha" >"$work/apply-result" +download_chatter_executed= +apply_candidate "$approved_sha" >"$work/apply-result" 2>"$work/apply-chatter" test "$applied_package" = 1 test -n "$recorded_installation" +test -z "$download_chatter_executed" grep -Fxq "installed=$candidate" "$work/apply-result" applied_package= From a501a8267d92370ce3b638dd0ccea534e71d550d Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 02:29:17 +0000 Subject: [PATCH 19/25] Activate Odoo commented secret settings Odoo 19 ships admin_passwd in its configuration as a semicolon-commented default. The build-time secret writer treated that valid slot as absent and stopped after the official package installed. Recognize semicolon-commented allowed keys, activate each replacement exactly once, and reject duplicate settings before writing. Mirror the upstream configuration in the fixture and cover successful replacement plus transactional missing, duplicate, and unknown-key failures. Verified with tests/config-secrets.sh, tests/repack-fixture.sh, Bash syntax checks, Python AST parsing, documentation punctuation checks, and git diff --check. --- .../usr/local/sbin/odoo-set-config-secrets | 7 +- tests/config-secrets.sh | 77 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/overlay/usr/local/sbin/odoo-set-config-secrets b/overlay/usr/local/sbin/odoo-set-config-secrets index 0c44469..c6e216e 100755 --- a/overlay/usr/local/sbin/odoo-set-config-secrets +++ b/overlay/usr/local/sbin/odoo-set-config-secrets @@ -18,10 +18,15 @@ lines = config_path.read_text(encoding="utf-8").splitlines(keepends=True) seen = set() updated = [] for line in lines: - key = line.partition("=")[0].strip() + setting = line.lstrip() + if setting.startswith(";"): + setting = setting[1:].lstrip() + key = setting.partition("=")[0].strip() if key not in replacements: updated.append(line) continue + if key in seen: + raise SystemExit(f"duplicate Odoo secret setting: {key}") newline = "\n" if line.endswith("\n") else "" updated.append(f"{key} = {replacements[key]}{newline}") seen.add(key) diff --git a/tests/config-secrets.sh b/tests/config-secrets.sh index 62f46fd..f673079 100755 --- a/tests/config-secrets.sh +++ b/tests/config-secrets.sh @@ -12,9 +12,14 @@ db_secret='db&secret|with\slashes=and equals' master_secret='master&secret|with\slashes=and equals' cat >"$config" <<'EOF' [options] -db_password = old database secret -admin_passwd = old master secret -db_host = localhost +; This is the password that allows database operations: +; admin_passwd = admin +db_host = False +db_port = False +db_user = odoo +db_password = False +;addons_path = /usr/lib/python3/dist-packages/odoo/addons +default_productivity_apps = True EOF mkfifo "$input" @@ -41,7 +46,71 @@ wait "$helper_pid" grep -Fxq "db_password = $db_secret" "$config" grep -Fxq "admin_passwd = $master_secret" "$config" -grep -Fxq 'db_host = localhost' "$config" +test "$(grep -Fxc "admin_passwd = $master_secret" "$config")" -eq 1 +test "$(grep -Fxc "db_password = $db_secret" "$config")" -eq 1 +! grep -Fxq '; admin_passwd = admin' "$config" +! grep -Fxq 'admin_passwd = admin' "$config" +! grep -Fxq 'db_password = False' "$config" +grep -Fxq '; This is the password that allows database operations:' "$config" +grep -Fxq 'db_host = False' "$config" +grep -Fxq 'db_port = False' "$config" +grep -Fxq 'db_user = odoo' "$config" +grep -Fxq ';addons_path = /usr/lib/python3/dist-packages/odoo/addons' "$config" +grep -Fxq 'default_productivity_apps = True' "$config" + +missing=$fixture/missing.conf +cat >"$missing" <<'EOF' +[options] +db_password = False +db_host = False +EOF +cp "$missing" "$missing.before" +if printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$db_secret" "$master_secret" | + "$helper" "$missing" >"$fixture/missing.stdout" 2>"$fixture/missing.stderr"; then + echo 'missing setting unexpectedly accepted' >&2 + exit 1 +fi +grep -Fxq 'missing Odoo secret setting: admin_passwd' "$fixture/missing.stderr" +cmp "$missing.before" "$missing" + +duplicate=$fixture/duplicate.conf +cat >"$duplicate" <<'EOF' +[options] +; admin_passwd = admin +admin_passwd = another default +db_password = False +EOF +cp "$duplicate" "$duplicate.before" +if printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$db_secret" "$master_secret" | + "$helper" "$duplicate" >"$fixture/duplicate.stdout" 2>"$fixture/duplicate.stderr"; then + echo 'duplicate setting unexpectedly accepted' >&2 + exit 1 +fi +grep -Fxq 'duplicate Odoo secret setting: admin_passwd' "$fixture/duplicate.stderr" +cmp "$duplicate.before" "$duplicate" + +cp "$config" "$config.before-invalid-input" +if printf 'db_password=%s\ndb_password=%s\nadmin_passwd=%s\n' \ + first second "$master_secret" | + "$helper" "$config" >"$fixture/input-duplicate.stdout" 2>"$fixture/input-duplicate.stderr"; then + echo 'duplicate input unexpectedly accepted' >&2 + exit 1 +fi +grep -Fxq "invalid Odoo secret setting: 'db_password'" \ + "$fixture/input-duplicate.stderr" +cmp "$config.before-invalid-input" "$config" + +if printf 'db_password=%s\nadmin_passwd=%s\nunknown=%s\n' \ + "$db_secret" "$master_secret" value | + "$helper" "$config" >"$fixture/input-unknown.stdout" 2>"$fixture/input-unknown.stderr"; then + echo 'unknown input unexpectedly accepted' >&2 + exit 1 +fi +grep -Fxq "invalid Odoo secret setting: 'unknown'" \ + "$fixture/input-unknown.stderr" +cmp "$config.before-invalid-input" "$config" python3 - "$helper" <<'PY' import ast From d3c393295b147b0f442a52b52ab717356bc8a3e2 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 02:34:00 +0000 Subject: [PATCH 20/25] Limit commented secret activation to Odoo default The prior loop-6 correction treated any semicolon-commented allowed key as a replacement slot. That could unintentionally activate a commented db_password or a near-match configuration line. Recognize only Odoo 19's exact vendor admin_passwd default while retaining ordinary active-setting replacement. Add transactional fixtures for active settings, commented db_password, and near-miss admin comments so unsupported comment forms remain absent and unchanged. Verified with tests/config-secrets.sh, Bash syntax, Python AST parsing, documentation punctuation checks, scoped diff validation, and git diff --check. --- .../usr/local/sbin/odoo-set-config-secrets | 8 +-- tests/config-secrets.sh | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/overlay/usr/local/sbin/odoo-set-config-secrets b/overlay/usr/local/sbin/odoo-set-config-secrets index c6e216e..a0fbd40 100755 --- a/overlay/usr/local/sbin/odoo-set-config-secrets +++ b/overlay/usr/local/sbin/odoo-set-config-secrets @@ -18,10 +18,10 @@ lines = config_path.read_text(encoding="utf-8").splitlines(keepends=True) seen = set() updated = [] for line in lines: - setting = line.lstrip() - if setting.startswith(";"): - setting = setting[1:].lstrip() - key = setting.partition("=")[0].strip() + if line.removesuffix("\n") == "; admin_passwd = admin": + key = "admin_passwd" + else: + key = line.partition("=")[0].strip() if key not in replacements: updated.append(line) continue diff --git a/tests/config-secrets.sh b/tests/config-secrets.sh index f673079..2fa022e 100755 --- a/tests/config-secrets.sh +++ b/tests/config-secrets.sh @@ -112,6 +112,56 @@ grep -Fxq "invalid Odoo secret setting: 'unknown'" \ "$fixture/input-unknown.stderr" cmp "$config.before-invalid-input" "$config" +active=$fixture/active.conf +cat >"$active" <<'EOF' +[options] +admin_passwd = active default +db_password = False +EOF +printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$db_secret" "$master_secret" | "$helper" "$active" +grep -Fxq "admin_passwd = $master_secret" "$active" +grep -Fxq "db_password = $db_secret" "$active" + +commented_db=$fixture/commented-db.conf +cat >"$commented_db" <<'EOF' +[options] +admin_passwd = active default +; db_password = False +EOF +cp "$commented_db" "$commented_db.before" +if printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$db_secret" "$master_secret" | + "$helper" "$commented_db" >"$fixture/commented-db.stdout" \ + 2>"$fixture/commented-db.stderr"; then + echo 'commented db_password unexpectedly accepted' >&2 + exit 1 +fi +grep -Fxq 'missing Odoo secret setting: db_password' \ + "$fixture/commented-db.stderr" +cmp "$commented_db.before" "$commented_db" + +for near_miss in \ + ';admin_passwd = admin' \ + '; admin_passwd=admin' \ + '; admin_passwd = changed' \ + ' ; admin_passwd = admin'; do + near_miss_config=$fixture/near-miss.conf + printf '[options]\n%s\ndb_password = False\n' "$near_miss" \ + >"$near_miss_config" + cp "$near_miss_config" "$near_miss_config.before" + if printf 'db_password=%s\nadmin_passwd=%s\n' \ + "$db_secret" "$master_secret" | + "$helper" "$near_miss_config" >"$fixture/near-miss.stdout" \ + 2>"$fixture/near-miss.stderr"; then + echo "near-miss admin_passwd unexpectedly accepted: $near_miss" >&2 + exit 1 + fi + grep -Fxq 'missing Odoo secret setting: admin_passwd' \ + "$fixture/near-miss.stderr" + cmp "$near_miss_config.before" "$near_miss_config" +done + python3 - "$helper" <<'PY' import ast import pathlib From fd511aed3afdf868faedae8755a1131a0e575dea Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 03:06:25 +0000 Subject: [PATCH 21/25] Import Odoo tools in runtime probes The final acceptance reached a healthy Odoo runtime, then the fixture failed before its configuration assertions because importing the top-level Odoo package does not load the tools submodule in Odoo 19. Import odoo.tools explicitly in both configuration probes. This changes only test initialization and preserves every runtime assertion and acceptance criterion. Verified with the focused embedded-Python AST checks, tests/config-secrets.sh, Bash syntax, documentation punctuation checks, and git diff --check. --- tests/v19.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/v19.sh b/tests/v19.sh index a801862..74ebd22 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -69,7 +69,7 @@ test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.g "$repository_key_fingerprint" odoo --version | grep -Fq '19.0' runuser -u odoo -- python3 - <<'PY' -import odoo +import odoo.tools odoo.tools.config.parse_config(['--config=/etc/odoo/odoo.conf']) assert odoo.tools.config['proxy_mode'] is True @@ -158,7 +158,7 @@ EOF grep -Fxq "$fixture" printf '%s\n' "$app_password" | runuser -u odoo -- python3 -c \ - 'import sys; import odoo; odoo.tools.config.parse_config(["--config=/etc/odoo/odoo.conf"]); assert odoo.tools.config.verify_admin_password(sys.stdin.readline().rstrip("\n"))' + 'import sys; import odoo.tools; odoo.tools.config.parse_config(["--config=/etc/odoo/odoo.conf"]); assert odoo.tools.config.verify_admin_password(sys.stdin.readline().rstrip("\n"))' odoo-update --check >"$work/update" candidate=$(sed -n 's/^candidate=//p' "$work/update") From 9ea9d68f2a8ff09f87294858b369cc753a42db9e Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 04:41:10 +0000 Subject: [PATCH 22/25] docs(turnkey-v19-appliance-migration): distinguish blocker from SHIP --- docs/v19.0-testing.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index fe5b720..09d85bc 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -295,6 +295,11 @@ changing the root inithook's configuration-save behavior or running another product candidate requires an explicit wish amendment and a renewed loop allowance. +Review approval of this blocker dossier does **not** constitute `SHIP` for the +Odoo v19 migration and must not be counted as a shipped migration in aggregate +reporting. It confirms only that the blocker evidence and handoff are complete; +the migration itself remains **BLOCKED**. + ## Deferred issues - The Odoo repository publishes supported branch packages daily. Core updates From 9bfc97e00328f3d78bfd1c33a2d5a5caa802dc0f Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 05:41:15 +0000 Subject: [PATCH 23/25] fix(odoo-v19-trixie): persist Odoo-owned data directory --- docs/v19.0-testing.md | 67 +++++++++++ overlay/usr/lib/inithooks/bin/odoo.py | 30 ++++- tests/odoo-firstboot-config-fixture.py | 150 +++++++++++++++++++++++++ tests/v19.sh | 2 + 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100755 tests/odoo-firstboot-config-fixture.py diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 09d85bc..5261943 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -310,3 +310,70 @@ the migration itself remains **BLOCKED**. - Public Postfix delivery and optional localization dependencies are not exercised by local acceptance. - Docker acceptance does not repeat installer, kernel, or hardware checks. + +## Epoch-two amendment + +The 2026-08-27 execution amendment preserves the complete epoch-one history +above, including its six-of-six product-loop ledger, and authorizes a separate +epoch-two ledger starting at zero of six. Before epoch-two source work, the +sealed epoch-one failure was copied atomically to +`/home/agent/.local/state/turnkey-v19-harness-wave2-2/archive/odoo/epoch1-20260826t235821z-1471535-357`. +The archive's `RETAINED-SHA256SUMS` SHA-256 is +`4e155f9f6dbaf7b360a3f69dee4b65b1e0341e0deef22deb4b831b18f190b696`, +its `report.txt` SHA-256 is +`ab307a552f53b0f7c3872d83dc64064efb77d47ca2169c8725b22958f2db19b2`, +and its `run.log` SHA-256 is +`3a59883f7590196d97959be452197ed93b3242291cb679d954eb25f6e47c341a`. +Every retained object validated against the archived manifest before the +correction. + +The epoch-one exact run reproduced one residual product defect: the root-run +firstboot hook parsed and saved Odoo configuration in root's XDG context, +persisting `/root/.local/share/Odoo` as `data_dir`. Epoch two binds the hook and +its Odoo subprocess to `HOME=/var/lib/odoo` and +`XDG_DATA_HOME=/var/lib/odoo/.local/share` before importing Odoo. After parsing +the existing configuration, it replaces `data_dir` with the exact +`/var/lib/odoo/.local/share/Odoo` path and saves only `admin_passwd` plus +`data_dir`, preserving unrelated file options. Firstboot reads the serialized +file back and fails before permission changes or service restart if the exact +data directory was not persisted. The existing supervised-service HOME, +`root:odoo` mode `0640` configuration, and `odoo:odoo` mode `0700` session +directory contracts remain unchanged. This reproduced product correction is +epoch-two product loop one of six. + +The focused firstboot fixture starts with root-derived HOME, XDG, and persisted +`data_dir` values. It proves that the hook fixes the process and subprocess +contexts, removes the persisted root path, preserves `proxy_mode`, scopes the +save, keeps the password out of argv, restarts the systemd unit only after a +successful persistence check, and fails closed when the root value remains. +Python compilation, Bash syntax, the firstboot fixture, the authentication +diagnostics fixture, the updater fixture, and `git diff --check` passed. +`shellcheck` was unavailable in the execution environment. + +A disposable Debian Trixie probe then installed the exact Odoo package +`19.0.20260825` after validating package SHA-256 +`e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69` +and the same pypdf compatibility-provider boundary used by the appliance. The +modified production hook ran against Odoo's real `configmanager`; its imported +default and serialized `data_dir` were both exactly +`/var/lib/odoo/.local/share/Odoo`, the pre-existing `proxy_mode` survived, +Odoo verified the new database-management password, no `/root` value remained, +and `apt-get check` plus `dpkg --audit` passed. The first probe invocation used +a fresh Odoo singleton after leaving its module sandbox and therefore failed +only its final password assertion; keeping that assertion inside the same +module lifetime made the corrected probe pass without a product change. Both +uniquely labelled probe containers were removed. The probe-fixture correction +does not consume an epoch-two product loop. + +Epoch-two exact acceptance uses a separate state directory while retaining the +shared builder and serialization lock: + +```sh +TKLDEV_CONTAINER=tkldev19-wave2-2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2-2-odoo-epoch2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2-2/build.lock \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=137438953472 \ +TKL_HARNESS_DOCKER_OBJECT_LABEL=org.turnkeylinux.v19-harness.wave2-2.odoo-epoch2 \ +/home/agent/.local/worktrees/turnkey/harness-wave2-2-152bc9b/tools/test-v19-appliance odoo \ + --source /home/agent/.local/worktrees/turnkey-apps/odoo/wish-odoo-v19-trixie +``` diff --git a/overlay/usr/lib/inithooks/bin/odoo.py b/overlay/usr/lib/inithooks/bin/odoo.py index 9249d6f..495e37c 100755 --- a/overlay/usr/lib/inithooks/bin/odoo.py +++ b/overlay/usr/lib/inithooks/bin/odoo.py @@ -5,6 +5,7 @@ --pass= unless provided, will ask interactively """ +import configparser import getopt import os import subprocess @@ -14,6 +15,9 @@ ODOO_DIST_PACKAGES = '/usr/lib/python3/dist-packages' ODOO_CONFIG = '/etc/odoo/odoo.conf' +ODOO_HOME = '/var/lib/odoo' +ODOO_DATA_DIR = f'{ODOO_HOME}/.local/share/Odoo' +ODOO_XDG_DATA_HOME = f'{ODOO_HOME}/.local/share' ODOO_PASSWORD_ENV = 'TURNKEY_ODOO_ADMIN_PASSWORD' @@ -59,6 +63,25 @@ def load_odoo_config(): return config +def set_odoo_config_context(): + """Resolve Odoo's user-scoped defaults below its service account home.""" + os.environ['HOME'] = ODOO_HOME + os.environ['XDG_DATA_HOME'] = ODOO_XDG_DATA_HOME + + +def assert_persisted_data_dir(): + """Fail firstboot unless the serialized data directory is Odoo-owned.""" + persisted = configparser.RawConfigParser() + if not persisted.read(ODOO_CONFIG): + raise RuntimeError(f'cannot read Odoo configuration: {ODOO_CONFIG}') + + data_dir = persisted.get('options', 'data_dir', fallback=None) + if data_dir != ODOO_DATA_DIR: + raise RuntimeError( + f'unexpected Odoo data_dir in {ODOO_CONFIG}: {data_dir!r}' + ) + + def usage(s=None): if s: print("Error:", s, file=sys.stderr) @@ -92,14 +115,19 @@ def main(): "This password will also login to 'admin' account of default/example Odoo.", blacklist=['\\', '/']) + # The hook runs as root, but Odoo must derive and persist user-scoped paths + # from the same home as its supervised service and subprocesses. + set_odoo_config_context() config = load_odoo_config() default_db = 'TurnkeylinuxExample' set_example_admin_password(default_db, password) config.parse_config([f'--config={ODOO_CONFIG}']) + config['data_dir'] = ODOO_DATA_DIR config.set_admin_password(password) - config.save() + config.save(['admin_passwd', 'data_dir']) + assert_persisted_data_dir() subprocess.run(['chown', 'root:odoo', ODOO_CONFIG], check=True) subprocess.run(['chmod', '0640', ODOO_CONFIG], check=True) diff --git a/tests/odoo-firstboot-config-fixture.py b/tests/odoo-firstboot-config-fixture.py new file mode 100755 index 0000000..9c8774f --- /dev/null +++ b/tests/odoo-firstboot-config-fixture.py @@ -0,0 +1,150 @@ +#!/usr/bin/python3 + +"""Exercise the root-run firstboot hook's Odoo configuration boundary.""" + +import configparser +import importlib.util +import os +from pathlib import Path +import sys +import tempfile +import types +from unittest import mock + + +REPO_ROOT = Path(__file__).resolve().parent.parent +HOOK_PATH = REPO_ROOT / 'overlay/usr/lib/inithooks/bin/odoo.py' +EXPECTED_HOME = '/var/lib/odoo' +EXPECTED_DATA_DIR = f'{EXPECTED_HOME}/.local/share/Odoo' +PASSWORD = 'fixture password with spaces and $pecial characters' + + +class FixtureConfig: + """Minimal configmanager contract with real config-file persistence.""" + + def __init__(self): + self.options = {'data_dir': '/root/.local/share/Odoo'} + self.config_path = None + self.save_keys = None + + def parse_config(self, arguments): + assert os.environ['HOME'] == EXPECTED_HOME + assert os.environ['XDG_DATA_HOME'] == f'{EXPECTED_HOME}/.local/share' + assert len(arguments) == 1 + self.config_path = arguments[0].removeprefix('--config=') + + persisted = configparser.RawConfigParser() + assert persisted.read(self.config_path) + self.options['data_dir'] = persisted.get('options', 'data_dir') + + def __setitem__(self, key, value): + self.options[key] = value + + def set_admin_password(self, password): + assert password == PASSWORD + self.options['admin_passwd'] = 'fixture-hash' + + def save(self, keys=None): + self.save_keys = keys + persisted = configparser.RawConfigParser() + assert persisted.read(self.config_path) + for key in keys: + persisted.set('options', key, self.options[key]) + with open(self.config_path, 'w', encoding='utf-8') as config_file: + persisted.write(config_file) + + +def load_hook(fixture_config): + dialog_wrapper = types.ModuleType('libinithooks.dialog_wrapper') + dialog_wrapper.Dialog = object + libinithooks = types.ModuleType('libinithooks') + libinithooks.dialog_wrapper = dialog_wrapper + odoo = types.ModuleType('odoo') + odoo.__file__ = '/usr/lib/python3/dist-packages/odoo/__init__.py' + odoo.__path__ = [] + tools = types.ModuleType('odoo.tools') + tools.config = fixture_config + odoo.tools = tools + + modules = { + 'libinithooks': libinithooks, + 'libinithooks.dialog_wrapper': dialog_wrapper, + 'odoo': odoo, + 'odoo.tools': tools, + } + with mock.patch.dict(sys.modules, modules): + spec = importlib.util.spec_from_file_location( + 'turnkey_odoo_inithook', HOOK_PATH + ) + hook = importlib.util.module_from_spec(spec) + spec.loader.exec_module(hook) + return hook, modules + + +def main(): + fixture_config = FixtureConfig() + hook, modules = load_hook(fixture_config) + calls = [] + + def record_run(arguments, **kwargs): + calls.append((arguments, kwargs)) + if arguments[0] == '/usr/sbin/runuser': + assert kwargs['env']['HOME'] == EXPECTED_HOME + assert kwargs['env']['XDG_DATA_HOME'] == \ + f'{EXPECTED_HOME}/.local/share' + assert PASSWORD not in arguments + return types.SimpleNamespace(returncode=0) + + with tempfile.TemporaryDirectory(prefix='odoo-firstboot-config.') as work: + config_path = Path(work) / 'odoo.conf' + config_path.write_text( + '[options]\n' + 'data_dir = /root/.local/share/Odoo\n' + 'proxy_mode = True\n', + encoding='utf-8', + ) + hook.ODOO_CONFIG = str(config_path) + + environment = { + 'APP_PASS': PASSWORD, + 'HOME': '/root', + 'XDG_DATA_HOME': '/root/.local/share', + } + with ( + mock.patch.dict(os.environ, environment, clear=True), + mock.patch.dict(sys.modules, modules), + mock.patch.object(sys, 'argv', [str(HOOK_PATH)]), + mock.patch.object(hook.subprocess, 'run', side_effect=record_run), + ): + hook.main() + assert os.environ['HOME'] == EXPECTED_HOME + assert os.environ['XDG_DATA_HOME'] == f'{EXPECTED_HOME}/.local/share' + + persisted = configparser.RawConfigParser() + assert persisted.read(config_path) + assert persisted.get('options', 'data_dir') == EXPECTED_DATA_DIR + assert persisted.getboolean('options', 'proxy_mode') is True + assert '/root' not in config_path.read_text(encoding='utf-8') + assert fixture_config.save_keys == ['admin_passwd', 'data_dir'] + + assert calls[0][0][:4] == [ + '/usr/sbin/runuser', '-u', 'odoo', '--' + ] + assert calls[-1][0] == ['systemctl', 'restart', 'odoo.service'] + + config_path.write_text( + '[options]\ndata_dir = /root/.local/share/Odoo\n', + encoding='utf-8', + ) + try: + hook.assert_persisted_data_dir() + except RuntimeError as error: + assert 'unexpected Odoo data_dir' in str(error) + else: + raise AssertionError('root-derived data_dir did not fail closed') + + print('odoo firstboot config fixture: PASS') + + +if __name__ == '__main__': + main() diff --git a/tests/v19.sh b/tests/v19.sh index f9ac11a..86a22ea 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -104,7 +104,9 @@ assert config['proxy_mode'] is True assert config['workers'] == 2 assert config['max_cron_threads'] == 1 assert config['gevent_port'] == 8072 +assert config['data_dir'] == '/var/lib/odoo/.local/share/Odoo' PY +! grep -Fq '/root' /etc/odoo/odoo.conf service_environment=$(systemctl show odoo.service --property=Environment --value) [[ $service_environment == *ODOO_NOTIFY_CRON_CHANGES=1* ]] From 747bdf3457b80a28c97c55179bbacf16c29b8e80 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Thu, 27 Aug 2026 06:09:58 +0000 Subject: [PATCH 24/25] docs(odoo-v19-trixie): record epoch-two validation --- docs/v19.0-testing.md | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 5261943..2db573c 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -377,3 +377,73 @@ TKL_HARNESS_DOCKER_OBJECT_LABEL=org.turnkeylinux.v19-harness.wave2-2.odoo-epoch2 /home/agent/.local/worktrees/turnkey/harness-wave2-2-152bc9b/tools/test-v19-appliance odoo \ --source /home/agent/.local/worktrees/turnkey-apps/odoo/wish-odoo-v19-trixie ``` + +The first epoch-two harness attempt +`20260827t054148z-2622542-27908` overlapped a separately launched Core +self-test that did not use the required shared lock. While Odoo's build process +was still active, that test's cleanup deleted Odoo's staged source directory. +The retained log records repeated missing-working-directory errors, absent +source-local release inputs, and cleanup's failure to enter the deleted staging +path. This attempt is classified **INFRA-ORCHESTRATION**, not a product result, +and consumes no product loop. Its evidence was copied atomically to +`/home/agent/.local/state/turnkey-v19-harness-wave2-2-odoo-epoch2/archive/odoo/infra-orchestration-20260827t054148z-2622542-27908`. +The retained-manifest SHA-256 is +`c1cb80f55a815a3ab06160f7412dd5b76f1506407d4b344c193a379e78aa5120`, +the report SHA-256 is +`5d656ac450322b01a9c2c2178652cbcf2d441e6d8bcca24cf2585af26c8e8b1d`, +and the run-log SHA-256 is +`a56eec4056b7a59273adcce3c451124be13eeb9fdbc829b8fd2cfd07a50e5ee5`. +Every retained object validates. The unchanged retry waited until a correctly +serialized Core validation passed and released the shared lock. + +The unchanged exact retry `20260827t055146z-2730655-29726` used source commit +`9bfc97e00328f3d78bfd1c33a2d5a5caa802dc0f` and pinned harness +`152bc9b876557266b90ed4ded77b611d9b817aff`. Its commit and transport archive +SHA-256 was +`a2019907e0df204ca2d0b112eb05de9f7164bb0bdcf5a32cad20aefe64ef6c6b`, +its input tree SHA-256 was +`1704aba6124f7dd7aaa8106fc039328b0dac7ae529996bdcf2174e748a312733`, +and its built tree SHA-256 was +`030b872932cef0fd9fbfd1b8c8b74a65df3220742c126106bb378a57ff9a34ae`. +Build, configured-root import, supervised boot, multi-user health, inithooks, +and Odoo service health all passed. The strict runtime acceptance returned +zero, proving the exact persisted `data_dir` with no `/root` residue, the +service HOME and session owner/mode, proxied administrator authentication as +UID 2, contact create/read plus PostgreSQL readback, persistence through +supervised PostgreSQL and Odoo restarts, two HTTP workers and a real cron +update, the least-privileged PostgreSQL role, the pinned PDF renderer and pypdf +provider, the database-management password, Adminer, Webmin modules, Postfix, +and the supervised update check. The report records Odoo +`19.0.20260825` as up to date and repeats all source-integrity identifiers. +Scoped runtime cleanup and disk checks passed with no retained container or +image. + +The PASS artifact manifest SHA-256 is +`3c514ee1bfd8fa6fd042c9aef5052ee568a1829bd4c57d28baabeb74b2433943`, +the PASS report SHA-256 is +`463df35bd34575d29b89c5818c6da26c74d7a2bc1b2047147efe4a9d1f8cafd4`, +and its run-log SHA-256 is +`72a1065d834b0a676d57519b4684991f24e5b81b40f994ae5b5e26c6a4338ac8`. +Every PASS artifact validates against its manifest. + +On successful authentication, the diagnostic helper returns after the Apache +request and reserves its separate direct request for failure diagnosis. A +supplemental disposable Trixie runtime therefore installed the same exact Odoo +package, initialized a real PostgreSQL database, and ran the production hook +with SHA-256 +`ae64f27d1f5e46c81ac87109bfeaa05ebf9e76113dd49211b3689555b02de856` +against a deliberately root-derived configuration. The hook persisted the +exact Odoo-owned path with `root:odoo` mode `0640`; a request sent directly to +`127.0.0.1:8069` authenticated as UID 2 and created the session directory as +`odoo:odoo` mode `0700`. Package and dependency audits passed, and the uniquely +labelled container was removed. This supplemental validation changed no +product source and consumes no product loop. Its evidence manifest SHA-256 is +`ac885b0152a41334b12a849ed8bbb2a742cb9b074940ceec12bab9aeafb3fa98`, +and its run-log SHA-256 is +`940a9867ee30b77e3a9f1e8cb7629cae4f21a8ac94615dabe16d65e0b22aad02`. + +The epoch-one archive was validated again after all epoch-two execution and +retains its original manifest, report, and run-log hashes. Epoch two finishes +with one reproduced product correction out of the six authorized loops; the +probe-fixture correction, orchestration-invalid attempt, unchanged exact +retry, and supplemental direct-login validation consume no product loops. From 213ee412360abdb486b2489474d842052ca39b04 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Fri, 28 Aug 2026 11:55:50 +0000 Subject: [PATCH 25/25] Restore the exact-tested Odoo release tree The post-PASS merge introduced an untested package repacker and updater changes that left undefined variables and broke the updater metadata contract. Remove that optional machinery and restore all product, packaging, and test content byte-for-byte to the accepted epoch-two candidate at 9bfc97e. Retain the later epoch-two validation record because it truthfully documents the accepted exact run. Verified with Bash syntax checks, Python AST and compile checks, all source-local Odoo fixtures, documentation punctuation checks, and an exact non-document tree comparison against 9bfc97e. --- README.rst | 49 ++-- changelog | 7 +- conf.d/main | 22 +- docs/v19.0-testing.md | 14 +- .../inithooks/firstboot.d/20-odoo-db-secrets | 4 +- overlay/usr/local/sbin/odoo-repack-package | 240 ------------------ .../usr/local/sbin/odoo-set-config-secrets | 37 --- overlay/usr/local/sbin/odoo-update | 193 +------------- plan/main | 2 - tests/config-secrets.sh | 179 ------------- tests/repack-fixture.sh | 148 ----------- tests/v19.sh | 13 +- 12 files changed, 55 insertions(+), 853 deletions(-) delete mode 100755 overlay/usr/local/sbin/odoo-repack-package delete mode 100755 overlay/usr/local/sbin/odoo-set-config-secrets delete mode 100755 tests/config-secrets.sh delete mode 100755 tests/repack-fixture.sh diff --git a/README.rst b/README.rst index f49eb5a..7efc805 100644 --- a/README.rst +++ b/README.rst @@ -30,23 +30,38 @@ This appliance includes all the standard features in `TurnKey Core`_: **To create a new Odoo Database (i.e. site)** -Choose a non-default password for the new database administrator, then use -Odoo 19's database command to create and initialize the database:: - - read -rsp 'New Odoo database administrator password: ' ODOO_DB_ADMIN_PASSWORD - printf '\n' - runuser -u odoo -- odoo db -c /etc/odoo/odoo.conf init example \ - --password "$ODOO_DB_ADMIN_PASSWORD" - unset ODOO_DB_ADMIN_PASSWORD - -Do not use Odoo's default ``admin`` password. - -Add the new name to the comma-separated ``db_name`` setting in -``/etc/odoo/odoo.conf``, then restart Odoo:: - - systemctl restart odoo - -The database command also provides ``drop``, ``dump``, and ``load`` operations. +To create a new Odoo DB, the Odoo config file needs to be edited and the +Odoo service restarted. + +Overview of process: +1. Edit /etc/odoo/odoo.conf + - change value of "db_name" from "TurnkeylinuxExample" to your desired DB + name +2. Restart odoo.service +3. Reload Odoo login screen in your browser and you should be good to go + +Webmin steps: +1. + - Browse to 'Tools' >> 'FileManager' + - Navigate to '/etc/odoo' and edit the 'odoo.conf' file + - Edit as per step 1 above + - Save changes +2. + - Navigate to 'System' >> 'Bootup and Shutdown' + - Find the "odoo.service" in the list and select it via the checkbox + - Scroll to the bottom and select 'Restart' +3. + - As per step 3 above + +CLI steps: +1. + - Edit /etc/odoo/odoo.conf as per step 1 +2. + - Restart odoo.service:: + + systemctl restart odoo +3. + - As per step 3 above **Notice for special Odoo Localization** diff --git a/changelog b/changelog index b8ccd78..5ba83bc 100644 --- a/changelog +++ b/changelog @@ -1,9 +1,7 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low * Install supported Odoo 19 Community from its official daily package - channel with an exact package digest and bound repository key. Preserve - the upstream payload while restoring its source-declared Trixie PDF - dependency alternative in package control metadata. + channel with an exact package digest and bound repository key. * Install Odoo's required patched wkhtmltopdf 0.12.6 series from an exact upstream package digest for PDF report rendering. @@ -15,8 +13,7 @@ turnkey-odoo-19.0 (1) turnkey; urgency=low database and firstboot credentials out of process arguments and traces. * Add a non-mutating update check, v19 acceptance coverage, and README - evidence crosswalk. Prepare supervised updates through the same verified, - payload-preserving control correction used at build time. + evidence crosswalk. * Upgrade the base distribution to Debian 13 Trixie. diff --git a/conf.d/main b/conf.d/main index 8118e8f..3127e7a 100755 --- a/conf.d/main +++ b/conf.d/main @@ -88,12 +88,7 @@ python3 -c 'import pypdf; assert pypdf.__version__' curl -fsSL "$ODOO_PACKAGE_URL" -o "$package" echo "$ODOO_PACKAGE_SHA256 $package" | sha256sum -c - -find "$repacked_package" -maxdepth 0 -type f -delete -odoo-repack-package "$package" "$repacked_package" >"$repack_metadata" -# shellcheck disable=SC1090 -. "$repack_metadata" -test "$upstream_package_sha256" = "$ODOO_PACKAGE_SHA256" -DEBIAN_FRONTEND=noninteractive apt-get install -y "$repacked_package" +DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" test "$(dpkg-query -W -f='${Version}' odoo)" = "$ODOO_VERSION" apt-get check runuser -u odoo -- python3 -c \ @@ -105,10 +100,7 @@ cat >"$SOURCE_RECORD" <> $CONF cat >>"$CONF" <\n" -DEPENDENCY = b"python3-pypdf2 | python3-pypdf" -COLLAPSED = re.compile( - rb"(?10}".encode("ascii") - if len(encoded_size) != 10: - fail("rewritten control archive is too large") - rewritten_header[48:58] = encoded_size - output.write(rewritten_header) - output.write(payload) - if len(payload) % 2: - output.write(b"\n") - continue - source.seek(offset - 60) - remaining = 60 + size + size % 2 - while remaining: - chunk = source.read(min(1024 * 1024, remaining)) - if not chunk: - fail("truncated ar archive while copying payload") - output.write(chunk) - remaining -= len(chunk) - - -def member_sha256(path, member): - digest = hashlib.sha256() - _, _, offset, size = member - with path.open("rb") as archive: - archive.seek(offset) - remaining = size - while remaining: - chunk = archive.read(min(1024 * 1024, remaining)) - if not chunk: - fail("truncated payload member") - digest.update(chunk) - remaining -= len(chunk) - return digest.hexdigest() - - -def main(): - if len(sys.argv) != 3: - fail("usage: odoo-repack-package INPUT.deb OUTPUT.deb") - source_path = Path(sys.argv[1]).resolve() - output_path = Path(sys.argv[2]).resolve() - if not source_path.is_file(): - fail("input package is not a regular file") - if source_path == output_path or output_path.exists(): - fail("output path must be new and distinct from input") - - members = ar_members(source_path) - control_members = [member for member in members if member[0].startswith("control.tar.")] - data_members = [member for member in members if member[0].startswith("data.tar.")] - if len(control_members) != 1 or len(data_members) != 1: - fail("package must contain one control archive and one data archive") - control_member = control_members[0] - original_control = decompressed_control( - control_member[0], read_member(source_path, control_member) - ) - rebuilt_control = compressed_control( - control_member[0], rebuild_control_tar(original_control) - ) - - output_path.parent.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=output_path.parent) as temporary: - temporary_output = Path(temporary) / output_path.name - write_ar( - source_path, - temporary_output, - members, - control_member[0], - rebuilt_control, - ) - rewritten_members = ar_members(temporary_output) - rewritten_data = [ - member for member in rewritten_members if member[0].startswith("data.tar.") - ][0] - original_payload_sha256 = member_sha256(source_path, data_members[0]) - if member_sha256(temporary_output, rewritten_data) != original_payload_sha256: - fail("upstream payload changed during repack") - os.replace(temporary_output, output_path) - - print(f"upstream_package_sha256={sha256(source_path)}") - print(f"repacked_package_sha256={sha256(output_path)}") - print(f"upstream_payload_sha256={original_payload_sha256}") - print("dependency_rewrite=python3-pypdf2_to_python3-pypdf2_or_python3-pypdf") - - -if __name__ == "__main__": - main() diff --git a/overlay/usr/local/sbin/odoo-set-config-secrets b/overlay/usr/local/sbin/odoo-set-config-secrets deleted file mode 100755 index a0fbd40..0000000 --- a/overlay/usr/local/sbin/odoo-set-config-secrets +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/python3 -"""Replace Odoo secret settings using key=value records from stdin.""" - -import pathlib -import sys - - -config_path = pathlib.Path(sys.argv[1]) -allowed_keys = {"admin_passwd", "db_password"} -replacements = {} -for record in sys.stdin: - key, separator, value = record.rstrip("\n").partition("=") - if not separator or key not in allowed_keys or key in replacements: - raise SystemExit(f"invalid Odoo secret setting: {key!r}") - replacements[key] = value - -lines = config_path.read_text(encoding="utf-8").splitlines(keepends=True) -seen = set() -updated = [] -for line in lines: - if line.removesuffix("\n") == "; admin_passwd = admin": - key = "admin_passwd" - else: - key = line.partition("=")[0].strip() - if key not in replacements: - updated.append(line) - continue - if key in seen: - raise SystemExit(f"duplicate Odoo secret setting: {key}") - newline = "\n" if line.endswith("\n") else "" - updated.append(f"{key} = {replacements[key]}{newline}") - seen.add(key) - -missing = replacements.keys() - seen -if missing: - raise SystemExit(f"missing Odoo secret setting: {', '.join(sorted(missing))}") -config_path.write_text("".join(updated), encoding="utf-8") diff --git a/overlay/usr/local/sbin/odoo-update b/overlay/usr/local/sbin/odoo-update index c4bad65..5087e73 100755 --- a/overlay/usr/local/sbin/odoo-update +++ b/overlay/usr/local/sbin/odoo-update @@ -1,9 +1,13 @@ #!/bin/bash set -Eeuo pipefail +if [[ ${1:-} != --check || $# -ne 1 ]]; then + echo "usage: odoo-update --check" >&2 + exit 2 +fi + source_record=/usr/local/share/turnkey-odoo/source keyring=/usr/share/keyrings/odoo-archive-keyring.gpg -repack_command=/usr/local/sbin/odoo-repack-package # shellcheck disable=SC1090 . "$source_record" @@ -44,170 +48,13 @@ grep -Fq 'https://nightly.odoo.com/19.0/nightly/deb/ ./' \ DEBIAN_FRONTEND=noninteractive apt-get --simulate --quiet=2 \ install "odoo=$candidate" >/dev/null - test "$installed" = "$installed_version" - test "$fingerprint" = "$repository_key_fingerprint" - test "$candidate" != "(none)" - grep -Fq 'https://nightly.odoo.com/19.0/nightly/deb/ ./' \ - /etc/apt/sources.list.d/odoo.list -} - -signed_candidate_sha256() { - apt-cache show "odoo=$candidate" | - awk '/^SHA256:/ { print $2; exit }' -} - -signed_candidate_filename() { - apt-cache show "odoo=$candidate" | - awk '/^Filename:/ { print $2; exit }' -} - -download_candidate() { - local directory=$1 - ( - cd "$directory" - apt-get download "odoo=$candidate" - ) -} - -prepare_candidate() { - local output=$1 - local work package expected_sha actual_sha metadata prepared - local -a packages - [[ ! -e $output ]] || { - echo "odoo-update: output already exists: $output" >&2 - return 1 - } - work=$(mktemp -d /tmp/odoo-update.XXXXXXXX) - if ! download_candidate "$work" >&2; then - find "$work" -depth -delete - return 1 - fi - mapfile -t packages < <(find "$work" -maxdepth 1 -type f -name 'odoo_*.deb') - if [[ ${#packages[@]} -ne 1 ]]; then - echo "odoo-update: expected one downloaded Odoo package" >&2 - find "$work" -depth -delete - return 1 - fi - package=${packages[0]} - expected_sha=$(signed_candidate_sha256) - actual_sha=$(sha256sum "$package" | awk '{ print $1 }') - if [[ -z $expected_sha || $actual_sha != "$expected_sha" ]]; then - echo "odoo-update: downloaded package does not match signed metadata" >&2 - find "$work" -depth -delete - return 1 - fi - - prepared=$work/prepared.deb - metadata=$work/repack-metadata - if ! "$repack_command" "$package" "$prepared" >"$metadata"; then - find "$work" -depth -delete - return 1 - fi - # shellcheck disable=SC1090 - . "$metadata" - test "$upstream_package_sha256" = "$expected_sha" - mv "$prepared" "$output" - find "$work" -depth -delete - - cat <&2 - return 1 - } - temporary=$(mktemp "${source_record}.XXXXXXXX") - cat >"$temporary" <&2 - return 1 - } - work=$(mktemp -d /tmp/odoo-update-apply.XXXXXXXX) - package=$work/candidate.deb - metadata=$work/metadata - if ! prepare_candidate "$package" >"$metadata"; then - find "$work" -depth -delete - return 1 - fi - # shellcheck disable=SC1090 - . "$metadata" - if [[ $repacked_package_sha256 != "$expected_repacked_sha" ]]; then - echo 'odoo-update: prepared package does not match approved SHA-256' >&2 - find "$work" -depth -delete - return 1 - fi - if ! install_package "$package"; then - find "$work" -depth -delete - return 1 - fi - installed_after=$(installed_version_after_install) - test "$installed_after" = "$candidate" - record_installation "$candidate" "$upstream_package_sha256" \ - "$repacked_package_sha256" "$upstream_payload_sha256" \ - "$dependency_rewrite" - find "$work" -depth -delete - cat <"$config" <<'EOF' -[options] -; This is the password that allows database operations: -; admin_passwd = admin -db_host = False -db_port = False -db_user = odoo -db_password = False -;addons_path = /usr/lib/python3/dist-packages/odoo/addons -default_productivity_apps = True -EOF -mkfifo "$input" - -# Hold the FIFO's write end open so the process can be inspected while it -# waits for records. -"$helper" "$config" <"$input" & -helper_pid=$! -exec 3>"$input" -for _ in {1..50}; do - if tr '\0' '\n' <"/proc/$helper_pid/cmdline" | grep -Fxq "$helper"; then - break - fi - sleep 0.01 -done -argv=$(tr '\0' '\n' <"/proc/$helper_pid/cmdline") -grep -Fxq "$helper" <<<"$argv" -! grep -Fq "$db_secret" <<<"$argv" -! grep -Fq "$master_secret" <<<"$argv" - -printf 'db_password=%s\nadmin_passwd=%s\n' \ - "$db_secret" "$master_secret" >&3 -exec 3>&- -wait "$helper_pid" - -grep -Fxq "db_password = $db_secret" "$config" -grep -Fxq "admin_passwd = $master_secret" "$config" -test "$(grep -Fxc "admin_passwd = $master_secret" "$config")" -eq 1 -test "$(grep -Fxc "db_password = $db_secret" "$config")" -eq 1 -! grep -Fxq '; admin_passwd = admin' "$config" -! grep -Fxq 'admin_passwd = admin' "$config" -! grep -Fxq 'db_password = False' "$config" -grep -Fxq '; This is the password that allows database operations:' "$config" -grep -Fxq 'db_host = False' "$config" -grep -Fxq 'db_port = False' "$config" -grep -Fxq 'db_user = odoo' "$config" -grep -Fxq ';addons_path = /usr/lib/python3/dist-packages/odoo/addons' "$config" -grep -Fxq 'default_productivity_apps = True' "$config" - -missing=$fixture/missing.conf -cat >"$missing" <<'EOF' -[options] -db_password = False -db_host = False -EOF -cp "$missing" "$missing.before" -if printf 'db_password=%s\nadmin_passwd=%s\n' \ - "$db_secret" "$master_secret" | - "$helper" "$missing" >"$fixture/missing.stdout" 2>"$fixture/missing.stderr"; then - echo 'missing setting unexpectedly accepted' >&2 - exit 1 -fi -grep -Fxq 'missing Odoo secret setting: admin_passwd' "$fixture/missing.stderr" -cmp "$missing.before" "$missing" - -duplicate=$fixture/duplicate.conf -cat >"$duplicate" <<'EOF' -[options] -; admin_passwd = admin -admin_passwd = another default -db_password = False -EOF -cp "$duplicate" "$duplicate.before" -if printf 'db_password=%s\nadmin_passwd=%s\n' \ - "$db_secret" "$master_secret" | - "$helper" "$duplicate" >"$fixture/duplicate.stdout" 2>"$fixture/duplicate.stderr"; then - echo 'duplicate setting unexpectedly accepted' >&2 - exit 1 -fi -grep -Fxq 'duplicate Odoo secret setting: admin_passwd' "$fixture/duplicate.stderr" -cmp "$duplicate.before" "$duplicate" - -cp "$config" "$config.before-invalid-input" -if printf 'db_password=%s\ndb_password=%s\nadmin_passwd=%s\n' \ - first second "$master_secret" | - "$helper" "$config" >"$fixture/input-duplicate.stdout" 2>"$fixture/input-duplicate.stderr"; then - echo 'duplicate input unexpectedly accepted' >&2 - exit 1 -fi -grep -Fxq "invalid Odoo secret setting: 'db_password'" \ - "$fixture/input-duplicate.stderr" -cmp "$config.before-invalid-input" "$config" - -if printf 'db_password=%s\nadmin_passwd=%s\nunknown=%s\n' \ - "$db_secret" "$master_secret" value | - "$helper" "$config" >"$fixture/input-unknown.stdout" 2>"$fixture/input-unknown.stderr"; then - echo 'unknown input unexpectedly accepted' >&2 - exit 1 -fi -grep -Fxq "invalid Odoo secret setting: 'unknown'" \ - "$fixture/input-unknown.stderr" -cmp "$config.before-invalid-input" "$config" - -active=$fixture/active.conf -cat >"$active" <<'EOF' -[options] -admin_passwd = active default -db_password = False -EOF -printf 'db_password=%s\nadmin_passwd=%s\n' \ - "$db_secret" "$master_secret" | "$helper" "$active" -grep -Fxq "admin_passwd = $master_secret" "$active" -grep -Fxq "db_password = $db_secret" "$active" - -commented_db=$fixture/commented-db.conf -cat >"$commented_db" <<'EOF' -[options] -admin_passwd = active default -; db_password = False -EOF -cp "$commented_db" "$commented_db.before" -if printf 'db_password=%s\nadmin_passwd=%s\n' \ - "$db_secret" "$master_secret" | - "$helper" "$commented_db" >"$fixture/commented-db.stdout" \ - 2>"$fixture/commented-db.stderr"; then - echo 'commented db_password unexpectedly accepted' >&2 - exit 1 -fi -grep -Fxq 'missing Odoo secret setting: db_password' \ - "$fixture/commented-db.stderr" -cmp "$commented_db.before" "$commented_db" - -for near_miss in \ - ';admin_passwd = admin' \ - '; admin_passwd=admin' \ - '; admin_passwd = changed' \ - ' ; admin_passwd = admin'; do - near_miss_config=$fixture/near-miss.conf - printf '[options]\n%s\ndb_password = False\n' "$near_miss" \ - >"$near_miss_config" - cp "$near_miss_config" "$near_miss_config.before" - if printf 'db_password=%s\nadmin_passwd=%s\n' \ - "$db_secret" "$master_secret" | - "$helper" "$near_miss_config" >"$fixture/near-miss.stdout" \ - 2>"$fixture/near-miss.stderr"; then - echo "near-miss admin_passwd unexpectedly accepted: $near_miss" >&2 - exit 1 - fi - grep -Fxq 'missing Odoo secret setting: admin_passwd' \ - "$fixture/near-miss.stderr" - cmp "$near_miss_config.before" "$near_miss_config" -done - -python3 - "$helper" <<'PY' -import ast -import pathlib -import sys - -ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) -PY - -bash -n "$root/conf.d/main" -bash -n "$root/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets" -bash -n "$root/tests/v19.sh" -! grep -E 'sed .*\$(DB_PASS|ODOO_ADMIN_PASSWORD)' \ - "$root/conf.d/main" \ - "$root/overlay/usr/lib/inithooks/firstboot.d/20-odoo-db-secrets" -git -C "$root" diff --check diff --git a/tests/repack-fixture.sh b/tests/repack-fixture.sh deleted file mode 100755 index 03c4e56..0000000 --- a/tests/repack-fixture.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/bin/bash -set -Eeuo pipefail -umask 077 - -repo=$(cd "$(dirname "$0")/.." && pwd) -repack=$repo/overlay/usr/local/sbin/odoo-repack-package -updater=$repo/overlay/usr/local/sbin/odoo-update -work=$(mktemp -d /tmp/odoo-repack-fixture.XXXXXXXX) -trap 'find "$work" -depth -delete' EXIT - -make_package() { - local name=$1 - local dependency=$2 - local output=$3 - local root=$work/root-$name-$(basename "$output") - mkdir -p "$root/DEBIAN" "$root/usr/share/odoo" - chmod 0755 "$root" "$root/DEBIAN" "$root/usr" \ - "$root/usr/share" "$root/usr/share/odoo" - printf '%s\n' \ - "Package: $name" \ - 'Version: 19.0.20990101' \ - 'Architecture: all' \ - 'Maintainer: Odoo S.A. ' \ - "Depends: bash, $dependency" \ - 'Description: Odoo repack fixture' >"$root/DEBIAN/control" - printf '%s\n' '#!/bin/sh' 'exit 0' >"$root/DEBIAN/postinst" - chmod 0755 "$root/DEBIAN/postinst" - printf '%s\n' 'official-upstream-payload' >"$root/usr/share/odoo/payload" - dpkg-deb --build --root-owner-group -Zxz "$root" "$output" >/dev/null -} - -expect_repack_failure() { - local input=$1 - local output=$2 - if "$repack" "$input" "$output" >"$work/unexpected-output" 2>"$work/expected-error"; then - echo "fixture unexpectedly repacked $input" >&2 - exit 1 - fi - test ! -e "$output" -} - -original=$work/odoo-original.deb -repacked_one=$work/odoo-repacked-one.deb -repacked_two=$work/odoo-repacked-two.deb -make_package odoo python3-pypdf2 "$original" -"$repack" "$original" "$repacked_one" >"$work/metadata-one" -"$repack" "$original" "$repacked_two" >"$work/metadata-two" -test "$(sha256sum "$repacked_one" | awk '{ print $1 }')" = \ - "$(sha256sum "$repacked_two" | awk '{ print $1 }')" -dpkg-deb -f "$repacked_one" Depends | - grep -Fxq 'bash, python3-pypdf2 | python3-pypdf' -original_fields=$(dpkg-deb -f "$original") -repacked_fields=$(dpkg-deb -f "$repacked_one") -test "${repacked_fields/python3-pypdf2 | python3-pypdf/python3-pypdf2}" = \ - "$original_fields" -mkdir "$work/original-control" "$work/repacked-control" -dpkg-deb --control "$original" "$work/original-control" -dpkg-deb --control "$repacked_one" "$work/repacked-control" -test "$(sha256sum "$work/original-control/postinst" | awk '{ print $1 }')" = \ - "$(sha256sum "$work/repacked-control/postinst" | awk '{ print $1 }')" -test "$(dpkg-deb --fsys-tarfile "$original" | sha256sum | awk '{ print $1 }')" = \ - "$(dpkg-deb --fsys-tarfile "$repacked_one" | sha256sum | awk '{ print $1 }')" -grep -Fxq 'dependency_rewrite=python3-pypdf2_to_python3-pypdf2_or_python3-pypdf' \ - "$work/metadata-one" - -already_fixed=$work/odoo-already-fixed.deb -missing_dependency=$work/odoo-missing-dependency.deb -wrong_package=$work/not-odoo.deb -make_package odoo 'python3-pypdf2 | python3-pypdf' "$already_fixed" -make_package odoo python3-requests "$missing_dependency" -make_package other python3-pypdf2 "$wrong_package" -expect_repack_failure "$already_fixed" "$work/already-fixed-output.deb" -expect_repack_failure "$missing_dependency" "$work/missing-output.deb" -expect_repack_failure "$wrong_package" "$work/wrong-package-output.deb" - -# Exercise the updater's signed-download boundary with a deterministic fixture. -# shellcheck disable=SC1090 -. "$updater" -repack_command=$repack -candidate=19.0.20990101 -fixture_download=$original -download_candidate() { - printf '%s\n' \ - 'Get:1 https://nightly.odoo.com/19.0/nightly/deb odoo fixture' \ - 'download_chatter_executed=1' - cp "$fixture_download" "$1/odoo_${candidate}_all.deb" -} -signed_candidate_sha256() { - sha256sum "$fixture_download" | awk '{ print $1 }' -} -prepare_candidate "$work/updater-prepared.deb" \ - >"$work/updater-result" 2>"$work/updater-chatter" -dpkg-deb -f "$work/updater-prepared.deb" Depends | - grep -Fxq 'bash, python3-pypdf2 | python3-pypdf' -grep -Fxq "candidate=$candidate" "$work/updater-result" -grep -Fq 'Get:1 https://nightly.odoo.com/' "$work/updater-chatter" -grep -Fxq 'download_chatter_executed=1' "$work/updater-chatter" -if grep -Eq '^(Get:|download_chatter_executed=)' "$work/updater-result"; then - echo 'download chatter contaminated updater metadata' >&2 - exit 1 -fi -test "$(wc -l <"$work/updater-result")" = 6 -cut -d= -f1 "$work/updater-result" | sort >"$work/updater-result-keys" -printf '%s\n' candidate dependency_rewrite prepared repacked_package_sha256 \ - upstream_package_sha256 upstream_payload_sha256 | sort \ - >"$work/expected-result-keys" -cmp "$work/expected-result-keys" "$work/updater-result-keys" - -signed_candidate_sha256() { - printf '%064d\n' 0 -} -if prepare_candidate "$work/updater-rejected.deb" >"$work/rejected-result" 2>"$work/rejected-error"; then - echo 'updater accepted a package that did not match signed metadata' >&2 - exit 1 -fi -test ! -e "$work/updater-rejected.deb" - -approved_sha=$(sha256sum "$repacked_one" | awk '{ print $1 }') -signed_candidate_sha256() { - sha256sum "$fixture_download" | awk '{ print $1 }' -} -install_package() { - test -f "$1" - applied_package=1 -} -installed_version_after_install() { - printf '%s\n' "$candidate" -} -record_installation() { - recorded_installation="$*" -} -applied_package= -recorded_installation= -download_chatter_executed= -apply_candidate "$approved_sha" >"$work/apply-result" 2>"$work/apply-chatter" -test "$applied_package" = 1 -test -n "$recorded_installation" -test -z "$download_chatter_executed" -grep -Fxq "installed=$candidate" "$work/apply-result" - -applied_package= -if apply_candidate "$(printf '%064d' 0)" >"$work/apply-rejected" 2>"$work/apply-error"; then - echo 'updater applied a package without the approved repacked hash' >&2 - exit 1 -fi -test -z "$applied_package" - -printf '%s\n' 'odoo-repack-fixture=PASS' diff --git a/tests/v19.sh b/tests/v19.sh index a4fab69..86a22ea 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -8,8 +8,6 @@ result=${TKL_TEST_RESULT:?TKL_TEST_RESULT is required} app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} source_file=/usr/local/share/turnkey-odoo/source database=TurnkeylinuxExample -test_database=turnkey_v19_acceptance_$$ -test_admin_password=$(mcookie) fixture="TurnKey v19 contact $(date +%s)-$$" email="odoo-v19-$$@example.invalid" cron_marker="TKL-v19-cron-$(date +%s)-$$" @@ -26,7 +24,6 @@ command -v jq >/dev/null ocurl() { curl --insecure --fail --silent --show-error \ - --connect-timeout 5 --max-time 15 \ --cookie "$cookie" --cookie-jar "$cookie" \ --header 'Content-Type: application/json' "$@" } @@ -134,15 +131,7 @@ tr '\0' '\n' <"/proc/$service_main_pid/environ" | : "${pypdf_compat_version:?pypdf_compat_version is missing from $source_file}" test "$installed_version" = 19.0.20260825 test "$(dpkg-query -W -f='${Version}' odoo)" = "$installed_version" -test "$upstream_package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 -test -n "$repacked_package_sha256" -test -n "$upstream_payload_sha256" -test "$dependency_rewrite" = \ - python3-pypdf2_to_python3-pypdf2_or_python3-pypdf -test "$(dpkg-query -W -f='${Status}' python3-pypdf)" = \ - 'install ok installed' -dpkg-query -W -f='${Depends}' odoo | - grep -Fq 'python3-pypdf2 | python3-pypdf' +test "$package_sha256" = e9d89da0fc94cd752b08b1e5501d97f464b834229ff8d68c7fecf24304e1da69 test "$(gpg --show-keys --with-colons /usr/share/keyrings/odoo-archive-keyring.gpg | awk -F: '$1 == "fpr" && !fingerprint { fingerprint=$10 } END { print fingerprint }')" = \ "$repository_key_fingerprint"