diff --git a/Makefile b/Makefile index bffaec5..637f2bb 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,8 @@ include $(FAB_PATH)/common/mk/turnkey/web2py.mk + +# The shared Web2py configuration follows an unpinned latest-release lookup. +# Keep its Trixie packages and overlays, but install the pinned supported source +# from this appliance instead. +COMMON_CONF := $(filter-out web2py,$(COMMON_CONF)) + include $(FAB_PATH)/common/mk/turnkey.mk diff --git a/README.rst b/README.rst index c56c27c..a9bbf34 100644 --- a/README.rst +++ b/README.rst @@ -12,16 +12,18 @@ and on top of that: - web2py configurations: - - Installed from upstream source code to /var/www/web2py + - Web2py 3 is installed from a pinned official upstream release in + ``/var/www/web2py``. **Security note**: Updates to web2py may require supervision so - they **ARE NOT** configured to install automatically. Using the "upgrade - now" button (within the webUI admin area) is the easiest way. Otherwise, - please see the `web2py documentation`_ for further info on - upgrading. + they **ARE NOT** configured to install automatically. Run + ``web2py-update --check`` to inspect the supported Web2py 3 channel. + Back up the appliance and review the upstream changes before running + ``web2py-update --apply``. - Serve web2py applications with WSGI on Apache. - Force admin console to be served via SSL. + - Include a MariaDB connection for database-driven Web2py applications. - SSL support out of the box. - Postfix MTA (bound to localhost) to allow sending of email (e.g., diff --git a/changelog b/changelog index bc620d1..12bf817 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,17 @@ +turnkey-web2py-19.0 (1) turnkey; urgency=low + + * Install the supported Web2py 3.3.3 release from its pinned official Git + tag and recorded submodule commits. + + * Add a supervised Web2py 3.x update command. + + * Preserve the Apache WSGI, HTTPS administration, and MariaDB application + boundaries on Debian 13 Trixie. + + * See turnkey-core's 19.0 changelog for common platform changes. + + -- TurnKey Linux maintainers Tue, 25 Aug 2026 00:00:00 +0000 + turnkey-web2py-18.0 (1) turnkey; urgency=low * Install web2py from upstream git: v2.27.1. diff --git a/conf.d/main b/conf.d/main index 1454ab3..4499295 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,11 +1,104 @@ -#!/bin/bash -ex +#!/bin/bash -e +set -x + +VERSION=3.3.3 +TAG=v3.3.3 +COMMIT=a729b848ff6b6471a2792cae8156bf087afc2456 +PYDAL_COMMIT=b515c362e83006e79f681ff6491e4a4ab8566acb +ROCKET_COMMIT=4154030489ebab15b96d1f90a6f288cf4f64dd23 +YATL_COMMIT=c6983a51909f76c48f4a40c93eea7f485321cd3f +REMOTE=https://github.com/web2py/web2py.git W2PROOT=/var/www/web2py +DB_NAME=web2py +DB_USER=web2py + +install -d -m 0755 "$W2PROOT" +git -C "$W2PROOT" init +git -C "$W2PROOT" remote add origin "$REMOTE" +git -C "$W2PROOT" fetch --depth 1 origin "refs/tags/$TAG:refs/tags/$TAG" +git -C "$W2PROOT" checkout --detach "$TAG" +test "$(git -C "$W2PROOT" rev-parse HEAD)" = "$COMMIT" +git -C "$W2PROOT" submodule update --init --recursive --depth 1 +test "$(git -C "$W2PROOT/gluon/packages/pydal" rev-parse HEAD)" = \ + "$PYDAL_COMMIT" +test "$(git -C "$W2PROOT/gluon/packages/rocket3" rev-parse HEAD)" = \ + "$ROCKET_COMMIT" +test "$(git -C "$W2PROOT/gluon/packages/yatl" rev-parse HEAD)" = \ + "$YATL_COMMIT" +git -C "$W2PROOT" fsck --no-dangling + +# Expose the official WSGI entry point through the shared Apache overlay. +cp "$W2PROOT/handlers/wsgihandler.py" "$W2PROOT/wsgihandler.py" # set welcome as web2py default application -cat >$W2PROOT/routes.py<"$W2PROOT/routes.py" <<'EOF' #!/usr/bin/env python default_application = 'welcome' EOF -chown www-data:www-data $W2PROOT/routes.py +service mysql start +mysqladmin create "$DB_NAME" + +# Keep the persistent application credentials out of the build trace. +set +x +DB_PASS=$(mcookie) +mysql --batch <"$W2PROOT/.turnkey-db" </usr/local/share/turnkey-web2py/source <&2 + exit 2 +} + +if [[ $# -ne 1 && ! ($# -eq 2 && $1 == --apply && $2 == --dry-run) ]]; then + usage +fi +case $1 in + --check|--apply) ;; + *) usage ;; +esac + +[[ -r $SOURCE ]] || { + echo "Web2py source provenance is unavailable" >&2 + exit 1 +} +# shellcheck disable=SC1090 +. "$SOURCE" +git_safe=(git -c safe.directory="$WEBROOT" -C "$WEBROOT") + +installed_commit=$("${git_safe[@]}" rev-parse HEAD) +installed_version=$(python3 -c \ + 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ + "$WEBROOT") +test "$installed_commit" = "$commit" +test "$installed_version" = "$version" +test "$("${git_safe[@]}" remote get-url origin)" = "$REMOTE" +"${git_safe[@]}" diff --quiet +"${git_safe[@]}" diff --cached --quiet + +tag_refs=$(git ls-remote --refs "$REMOTE" 'refs/tags/v3.*') +latest_tag=$(awk '$2 ~ /^refs\/tags\/v3\.[0-9]+\.[0-9]+$/ { + sub("refs/tags/", "", $2); print $2 + }' <<<"$tag_refs" | sort -V | tail -n 1) +[[ $latest_tag =~ ^v3\.[0-9]+\.[0-9]+$ ]] +tag_object=$(awk -v ref="refs/tags/$latest_tag" '$2 == ref {print $1}' \ + <<<"$tag_refs") +peeled=$(git ls-remote "$REMOTE" "refs/tags/$latest_tag^{}" | + awk 'NR == 1 {print $1}') +latest_commit=${peeled:-$tag_object} +[[ $latest_commit =~ ^[0-9a-f]{40}$ ]] +latest_version=${latest_tag#v} + +status=update-available +if [[ $installed_version == "$latest_version" && \ + $installed_commit == "$latest_commit" ]]; then + status=up-to-date +elif dpkg --compare-versions "$installed_version" gt "$latest_version"; then + status=newer-than-channel +fi + +if [[ $1 == --check ]]; then + printf 'installed=%s\ninstalled_commit=%s\nlatest=%s\n' \ + "$installed_version" "$installed_commit" "$latest_version" + printf 'candidate=%s\ntag=%s\nchannel=official-web2py-3.x\nstatus=%s\n' \ + "$latest_commit" "$latest_tag" "$status" + exit 0 +fi + +if [[ ${2:-} == --dry-run ]]; then + printf 'mode=apply-dry-run\ntarget=%s\ncandidate=%s\n' \ + "$latest_version" "$latest_commit" + printf 'tag=%s\nverified=official-web2py-tag\nstatus=%s\n' \ + "$latest_tag" "$status" + exit 0 +fi + +test "$(id -u)" -eq 0 +if [[ $status != update-available ]]; then + echo "Web2py $installed_version is $status on the supported 3.x channel." + exit 0 +fi + +systemctl stop apache2.service +trap 'systemctl start apache2.service' EXIT +target_ref="refs/tags/$latest_tag" +runuser -u www-data -- git -C "$WEBROOT" fetch --force --depth 1 \ + origin "$target_ref:$target_ref" +fetched_commit=$("${git_safe[@]}" rev-parse "$target_ref^{commit}") +test "$fetched_commit" = "$latest_commit" +runuser -u www-data -- git -C "$WEBROOT" checkout --detach "$target_ref" +runuser -u www-data -- git -C "$WEBROOT" submodule sync --recursive +runuser -u www-data -- git -C "$WEBROOT" submodule update --init --recursive \ + --depth 1 +test "$("${git_safe[@]}" rev-parse HEAD)" = "$latest_commit" +"${git_safe[@]}" diff --quiet +"${git_safe[@]}" diff --cached --quiet +cp "$WEBROOT/handlers/wsgihandler.py" "$WEBROOT/wsgihandler.py" + +updated_version=$(python3 -c \ + 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ + "$WEBROOT") +test "$updated_version" = "$latest_version" +source_tmp=$(mktemp "$SOURCE.XXXXXX") +{ + printf 'version=%s\n' "$latest_version" + printf 'tag=%s\n' "$latest_tag" + printf 'commit=%s\n' "$latest_commit" + printf 'pydal_commit=%s\n' \ + "$(runuser -u www-data -- git \ + -C "$WEBROOT/gluon/packages/pydal" rev-parse HEAD)" + printf 'rocket_commit=%s\n' \ + "$(runuser -u www-data -- git \ + -C "$WEBROOT/gluon/packages/rocket3" rev-parse HEAD)" + printf 'yatl_commit=%s\n' \ + "$(runuser -u www-data -- git \ + -C "$WEBROOT/gluon/packages/yatl" rev-parse HEAD)" + printf 'remote=%s\n' "$REMOTE" +} >"$source_tmp" +chmod 0644 "$source_tmp" +mv "$source_tmp" "$SOURCE" + +chown -R www-data:www-data "$WEBROOT" +chown root:www-data "$WEBROOT/.turnkey-db" +chmod 0640 "$WEBROOT/.turnkey-db" +systemctl start apache2.service +trap - EXIT +printf 'updated=%s\ncommit=%s\ntag=%s\n' \ + "$latest_version" "$latest_commit" "$latest_tag" diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..b115d7e --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,126 @@ +#!/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} +db_password=${TKL_TEST_DB_PASS:?TKL_TEST_DB_PASS is required} +webroot=/var/www/web2py +source_file=/usr/local/share/turnkey-web2py/source +cookie=$(mktemp /tmp/web2py-cookie.XXXXXXXX) +page=$(mktemp /tmp/web2py-page.XXXXXXXX) +headers=$(mktemp /tmp/web2py-headers.XXXXXXXX) +update=$(mktemp /tmp/web2py-update.XXXXXXXX) + +cleanup() { + find "$cookie" "$page" "$headers" "$update" -maxdepth 0 -delete +} +trap cleanup EXIT + +for unit in apache2.service mariadb.service cron.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-web2py-19\.0' /etc/turnkey_version + +# shellcheck disable=SC1090 +. "$source_file" +test "$version" = 3.3.3 +test "$tag" = v3.3.3 +test "$commit" = a729b848ff6b6471a2792cae8156bf087afc2456 +test "$pydal_commit" = b515c362e83006e79f681ff6491e4a4ab8566acb +test "$rocket_commit" = 4154030489ebab15b96d1f90a6f288cf4f64dd23 +test "$yatl_commit" = c6983a51909f76c48f4a40c93eea7f485321cd3f +git_safe=(git -c safe.directory="$webroot" -C "$webroot") +test "$("${git_safe[@]}" rev-parse HEAD)" = "$commit" +"${git_safe[@]}" diff --quiet +"${git_safe[@]}" diff --cached --quiet +test "$(git -c safe.directory="$webroot/gluon/packages/pydal" \ + -C "$webroot/gluon/packages/pydal" rev-parse HEAD)" = \ + "$pydal_commit" +test "$(git -c safe.directory="$webroot/gluon/packages/rocket3" \ + -C "$webroot/gluon/packages/rocket3" rev-parse HEAD)" = \ + "$rocket_commit" +test "$(git -c safe.directory="$webroot/gluon/packages/yatl" \ + -C "$webroot/gluon/packages/yatl" rev-parse HEAD)" = \ + "$yatl_commit" +runtime_version=$(python3 -c \ + 'import sys; sys.path.insert(0, sys.argv[1]); from gluon.version import VERSION; print(VERSION.split("-")[0])' \ + "$webroot") +test "$runtime_version" = "$version" +test "$(stat -c '%U:%G:%a' "$webroot/parameters_443.py")" = \ + 'www-data:www-data:640' +test "$(stat -c '%U:%G:%a' "$webroot/.turnkey-db")" = \ + 'root:www-data:640' + +curl --fail --silent --show-error http://127.0.0.1/ >"$page" +grep -Fq 'You are successfully running web2py' "$page" +curl --insecure --fail --silent --show-error https://127.0.0.1/ >"$page" +grep -Fq 'You are successfully running web2py' "$page" + +redirect=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}|%{redirect_url}' \ + http://127.0.0.1/admin) +[[ $redirect == 307\|https://*/admin ]] +curl --insecure --fail --silent --show-error --cookie-jar "$cookie" \ + https://127.0.0.1/admin/default/index >"$page" +grep -Fq 'Login to the Administrative Interface' "$page" +curl --insecure --fail --silent --show-error --location \ + --cookie "$cookie" --cookie-jar "$cookie" \ + --data-urlencode "password=$app_password" \ + --data-urlencode 'send=/admin/default/site' \ + https://127.0.0.1/admin/default/index >"$page" +grep -Fq 'Installed applications' "$page" +grep -Fq 'welcome' "$page" + +dal_result=$(runuser -u www-data -- python3 - "$webroot" <<'PY' +import sys + +root = sys.argv[1] +sys.path.insert(0, root) +from gluon import DAL + +with open(f"{root}/.turnkey-db", encoding="utf-8") as stream: + uri = stream.read().strip().split("=", 1)[1] +db = DAL(uri, migrate=False) +rows = db.executesql("SELECT message FROM turnkey_status WHERE id = 1") +db.close() +assert len(rows) == 1 +assert rows[0][0] == "Database connectivity verified" +print(rows[0][0]) +PY +) +test "$dal_result" = 'Database connectivity verified' +MYSQL_PWD=$db_password mariadb --user=root --batch --skip-column-names \ + web2py --execute='SELECT message FROM turnkey_status WHERE id = 1' | + grep -Fxq 'Database connectivity verified' + +web2py-update --check >"$update" +latest=$(sed -n 's/^latest=//p' "$update") +candidate=$(sed -n 's/^candidate=//p' "$update") +latest_tag=$(sed -n 's/^tag=//p' "$update") +status=$(sed -n 's/^status=//p' "$update") +[[ $latest =~ ^3\.[0-9]+\.[0-9]+$ ]] +[[ $candidate =~ ^[0-9a-f]{40}$ ]] +test "$latest_tag" = "v$latest" +grep -Fxq 'channel=official-web2py-3.x' "$update" +before=$("${git_safe[@]}" rev-parse HEAD) +apply_plan=$(web2py-update --apply --dry-run) +grep -Fq 'mode=apply-dry-run' <<<"$apply_plan" +grep -Fq "target=$latest" <<<"$apply_plan" +grep -Fq "candidate=$candidate" <<<"$apply_plan" +grep -Fq "tag=$latest_tag" <<<"$apply_plan" +grep -Fq 'verified=official-web2py-tag' <<<"$apply_plan" +test "$("${git_safe[@]}" rev-parse HEAD)" = "$before" +"${git_safe[@]}" diff --quiet + +cat >"$result" <"$work/revision" + git -C "$work" add revision + git "${git_config[@]}" -C "$work" commit --quiet -m initial + git -C "$work" rev-parse HEAD >"$scratch/${name}-initial" + printf 'updated\n' >"$work/revision" + git -C "$work" add revision + git "${git_config[@]}" -C "$work" commit --quiet -m updated + git -C "$work" rev-parse HEAD >"$scratch/${name}-updated" + git clone --quiet --bare "$work" "$remote" +} + +for module in pydal rocket3 yatl; do + make_submodule "$module" +done + +upstream_work="$scratch/web2py-work" +upstream="$scratch/web2py.git" +git init --quiet "$upstream_work" +install -d "$upstream_work/gluon/packages" "$upstream_work/handlers" +printf 'VERSION = "3.3.3"\n' >"$upstream_work/gluon/version.py" +printf 'fixture\n' >"$upstream_work/handlers/wsgihandler.py" +printf '/.turnkey-db\n/wsgihandler.py\n' >"$upstream_work/.gitignore" +for module in pydal rocket3 yatl; do + git -c protocol.file.allow=always -C "$upstream_work" submodule add \ + --quiet "$scratch/${module}.git" "gluon/packages/$module" + git -C "$upstream_work/gluon/packages/$module" checkout --quiet \ + "$(<"$scratch/${module}-initial")" +done +git -C "$upstream_work" add . +git "${git_config[@]}" -C "$upstream_work" commit --quiet -m initial +git -C "$upstream_work" tag v3.3.3 +initial_commit=$(git -C "$upstream_work" rev-parse HEAD) + +printf 'VERSION = "3.3.4"\n' >"$upstream_work/gluon/version.py" +for module in pydal rocket3 yatl; do + git -C "$upstream_work/gluon/packages/$module" checkout --quiet \ + "$(<"$scratch/${module}-updated")" +done +git -C "$upstream_work" add . +git "${git_config[@]}" -C "$upstream_work" commit --quiet -m updated +git -C "$upstream_work" tag v3.3.4 +updated_commit=$(git -C "$upstream_work" rev-parse HEAD) +git clone --quiet --bare "$upstream_work" "$upstream" + +safe_remote_env=( + GIT_CONFIG_COUNT=4 + GIT_CONFIG_KEY_0=safe.directory + GIT_CONFIG_VALUE_0="$upstream" + GIT_CONFIG_KEY_1=safe.directory + GIT_CONFIG_VALUE_1="$scratch/pydal.git" + GIT_CONFIG_KEY_2=safe.directory + GIT_CONFIG_VALUE_2="$scratch/rocket3.git" + GIT_CONFIG_KEY_3=safe.directory + GIT_CONFIG_VALUE_3="$scratch/yatl.git" +) +webroot="$scratch/installed" +install -d -o www-data -g www-data "$webroot" +runuser -u www-data -- env GIT_ALLOW_PROTOCOL=file "${safe_remote_env[@]}" \ + git clone --quiet --branch v3.3.3 --recurse-submodules \ + "$upstream" "$webroot" +install -o root -g www-data -m 0640 /dev/null "$webroot/.turnkey-db" +source_file="$scratch/source" +cat >"$source_file" <"$shim_dir/systemctl" <>'$service_log' +case "\$1" in + stop) printf 'stopped\n' >'$service_state' ;; + start) printf 'active\n' >'$service_state' ;; + *) exit 2 ;; +esac +EOF +chmod 0755 "$shim_dir/systemctl" +printf 'active\n' >"$service_state" + +updater="$scratch/web2py-update" +sed -e "s|^WEBROOT=.*|WEBROOT=$webroot|" \ + -e "s|^SOURCE=.*|SOURCE=$source_file|" \ + -e "s|^REMOTE=.*|REMOTE=$upstream|" \ + overlay/usr/local/sbin/web2py-update >"$updater" +chmod 0755 "$updater" +env PATH="$shim_dir:$PATH" GIT_ALLOW_PROTOCOL=file "${safe_remote_env[@]}" \ + "$updater" --apply + +# shellcheck disable=SC1090 +. "$source_file" +test "$version" = 3.3.4 +test "$tag" = v3.3.4 +test "$commit" = "$updated_commit" +for module in pydal rocket3 yatl; do + expected=$(<"$scratch/${module}-updated") + case $module in + pydal) recorded=$pydal_commit ;; + rocket3) recorded=$rocket_commit ;; + yatl) recorded=$yatl_commit ;; + esac + test "$recorded" = "$expected" + test "$(runuser -u www-data -- git -C \ + "$webroot/gluon/packages/$module" rev-parse HEAD)" = "$expected" + runuser -u www-data -- git -C "$webroot/gluon/packages/$module" \ + diff --quiet + runuser -u www-data -- git -C "$webroot/gluon/packages/$module" \ + diff --cached --quiet +done +test "$(runuser -u www-data -- git -C "$webroot" rev-parse HEAD)" = \ + "$updated_commit" +runuser -u www-data -- git -C "$webroot" diff --quiet +runuser -u www-data -- git -C "$webroot" diff --cached --quiet +test "$(<"$service_state")" = active +test "$(<"$service_log")" = $'stop apache2.service\nstart apache2.service'