fix: support Debian 13 offline bootstrap

This commit is contained in:
Haitao Pan 2026-06-18 18:08:16 +08:00
parent 2cb26128fb
commit e950eb18b8
2 changed files with 233 additions and 1 deletions

View File

@ -169,7 +169,7 @@ dynamic_parallel_job_limit() {
fi
load_average="$(one_minute_load_average)"
load_ceiling="$(awk -v load="$load_average" 'BEGIN { value=int(load); if (load > value) value++; print value }')"
load_ceiling="$(awk -v average="$load_average" 'BEGIN { value=int(average); if (average > value) value++; print value }')"
dynamic_limit=$((hard_limit - load_ceiling))
if [ "$dynamic_limit" -lt 1 ]; then
dynamic_limit=1
@ -711,6 +711,40 @@ validate_offline_package_target() {
fi
}
validate_offline_package_requirements() {
local root=$1
local target=$2
case "$target" in
"ubuntu 26.04 "*)
if ! compgen -G "$root/packages/apt/npm_*.deb" >/dev/null; then
warn "Ubuntu 26.04 offline package is missing the required standalone npm package."
return 1
fi
;;
esac
}
refresh_offline_package_repositories() {
local root=$1
local repo_dir branch
offline_mode_is_force && return
command -v git >/dev/null 2>&1 || return
curl -m 3 -sI https://github.com >/dev/null 2>&1 || return
for repo_dir in "$root/repos/xworkspace-console" "$root/repos/playbooks"; do
[ -d "$repo_dir/.git" ] || continue
branch="$(git -C "$repo_dir" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
[ -n "$branch" ] || continue
info "Refreshing packaged $(basename "$repo_dir") checkout from origin/$branch..."
if ! git -C "$repo_dir" fetch origin "$branch" >/dev/null 2>&1 ||
! git -C "$repo_dir" reset --hard "origin/$branch" >/dev/null 2>&1; then
warn "Unable to refresh packaged $(basename "$repo_dir") checkout; using bundled revision."
fi
done
}
run_offline_installer() {
local root=$1
local target=$2
@ -718,11 +752,17 @@ run_offline_installer() {
local env_args=(
"AI_WORKSPACE_OFFLINE_ACTIVE=true"
"AI_WORKSPACE_DEPLOYMENT_LOCK_HELD=true"
# The bundled repositories can retain the release builder's uid. Keep
# this exception scoped to the offline installer process and children.
"GIT_CONFIG_COUNT=1"
"GIT_CONFIG_KEY_0=safe.directory"
"GIT_CONFIG_VALUE_0=*"
)
local env_name
[ -f "$installer" ] || return 1
validate_offline_package_target "$root" "$target"
refresh_offline_package_repositories "$root"
chmod +x "$installer"
for env_name in \
@ -746,6 +786,8 @@ run_offline_installer() {
ANSIBLE_VAULT_PASSWORD \
AI_WORKSPACE_AUTH_TOKEN_FILE \
AI_WORKSPACE_VAULT_PASSWORD_FILE \
XWORKSPACE_CONSOLE_USER \
XWORKSPACE_CONSOLE_HOME \
XWORKSPACE_CONSOLE_SOURCE_REPO \
XWORKSPACE_CONSOLE_SOURCE_VERSION \
AI_WORKSPACE_APT_LOCK_TIMEOUT; do
@ -792,6 +834,10 @@ try_bootstrap_from_offline_package() {
offline_fail_or_fallback "Unable to prepare offline package from $source."
return 1
}
if ! validate_offline_package_requirements "$root" "$target"; then
offline_fail_or_fallback "Offline package requirements are incomplete for this host."
return 1
fi
if run_offline_installer "$root" "$target"; then
return 0
fi
@ -799,6 +845,41 @@ try_bootstrap_from_offline_package() {
error "Offline package installer failed after making deployment changes; online fallback was not started."
}
linux_default_console_user() {
if [ -n "${XWORKSPACE_CONSOLE_USER:-}" ]; then
printf '%s\n' "$XWORKSPACE_CONSOLE_USER"
elif [ "$(id -u)" -eq 0 ]; then
printf 'ubuntu\n'
else
id -un
fi
}
linux_default_console_home() {
local user=$1
if [ -n "${XWORKSPACE_CONSOLE_HOME:-}" ]; then
printf '%s\n' "$XWORKSPACE_CONSOLE_HOME"
elif command -v getent >/dev/null 2>&1 && getent passwd "$user" >/dev/null 2>&1; then
getent passwd "$user" | cut -d: -f6
elif [ "$user" = "root" ]; then
printf '/root\n'
else
printf '/home/%s\n' "$user"
fi
}
append_linux_console_identity_vars() {
local console_user=$1
local console_home=$2
ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_user=$console_user")
ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_home=$console_home")
ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_root=$console_home/.local/state/ai-workspace")
ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_config_dir=$console_home/.config/xworkspace")
ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_scripts_dir=$console_home/.local/state/ai-workspace/scripts")
ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_repo_dir=$console_home/xworkspace-console")
}
resolve_unified_auth_token() {
local token="${AI_WORKSPACE_AUTH_TOKEN:-}"
if [ -z "$token" ]; then token="${XWORKSPACE_CONSOLE_AUTH_TOKEN:-}"; fi
@ -1630,6 +1711,9 @@ if ! command -v ansible-playbook >/dev/null 2>&1 || ! command -v git >/dev/null
install_prerequisites "$OS_NAME"
fi
# Git may have been installed after the early best-effort configuration above.
git config --global --add safe.directory '*' || true
export XWORKSPACE_CONSOLE_PUBLIC_ACCESS="${XWORKSPACE_CONSOLE_PUBLIC_ACCESS:-false}"
export XWORKMATE_BRIDGE_PUBLIC_ACCESS="${XWORKMATE_BRIDGE_PUBLIC_ACCESS:-true}"
export GATEWAY_OPENCLAW_PUBLIC_ACCESS="${GATEWAY_OPENCLAW_PUBLIC_ACCESS:-false}"
@ -1897,6 +1981,11 @@ if [ "$(detect_os)" = "darwin" ]; then
ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_home=$HOME")
ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_compile_cache_dir=$HOME/.cache/openclaw-compile-cache")
ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_service_path=$DARWIN_SERVICE_PATH")
else
LINUX_CONSOLE_USER="$(linux_default_console_user)"
LINUX_CONSOLE_HOME="$(linux_default_console_home "$LINUX_CONSOLE_USER")"
info "Deploying AI Workspace runtime as $LINUX_CONSOLE_USER under $LINUX_CONSOLE_HOME..."
append_linux_console_identity_vars "$LINUX_CONSOLE_USER" "$LINUX_CONSOLE_HOME"
fi
# Export environment fallbacks for roles/scripts that read environment directly.

View File

@ -70,6 +70,133 @@ test_non_root_without_sudo_fails_cleanly() (
run_as_root apt-get update -y
)
test_forced_offline_mode_does_not_refresh_repositories() (
# shellcheck disable=SC2329
git() {
fail "forced offline mode invoked git"
}
AI_WORKSPACE_OFFLINE_MODE=force refresh_offline_package_repositories /nonexistent
)
test_auto_offline_mode_refreshes_packaged_repositories() (
package_root="$(mktemp -d)"
mkdir -p "$package_root/repos/xworkspace-console/.git" "$package_root/repos/playbooks/.git"
git_log="$(mktemp)"
# shellcheck disable=SC2329
curl() {
return 0
}
# shellcheck disable=SC2329
git() {
printf '%s\n' "$*" >> "$git_log"
if [ "${3:-}" = "symbolic-ref" ]; then
printf 'main\n'
fi
}
AI_WORKSPACE_OFFLINE_MODE=auto refresh_offline_package_repositories "$package_root"
[ "$(grep -c 'fetch origin main' "$git_log")" -eq 2 ] || fail "packaged repositories were not fetched"
[ "$(grep -c 'reset --hard origin/main' "$git_log")" -eq 2 ] || fail "packaged repositories were not updated"
rm -rf "$package_root" "$git_log"
)
test_ubuntu_2604_offline_package_requires_npm() (
package_root="$(mktemp -d)"
mkdir -p "$package_root/packages/apt"
if validate_offline_package_requirements "$package_root" "ubuntu 26.04 amd64" 2>/dev/null; then
fail "Ubuntu 26.04 package without npm was accepted"
fi
touch "$package_root/packages/apt/npm_9.2.0_all.deb"
validate_offline_package_requirements "$package_root" "ubuntu 26.04 amd64"
validate_offline_package_requirements "$package_root" "debian 12 amd64"
rm -rf "$package_root"
)
test_dynamic_parallel_limit_avoids_awk_reserved_names() (
# shellcheck disable=SC2329
online_cpu_count() { printf '4\n'; }
# shellcheck disable=SC2329
one_minute_load_average() { printf '1.2\n'; }
[ "$(AI_WORKSPACE_MAX_PARALLEL_JOBS=auto dynamic_parallel_job_limit)" = "6" ] || fail "dynamic parallel limit was calculated incorrectly"
)
test_offline_installer_gets_scoped_git_config() (
installer_root="$(mktemp -d)"
mkdir -p "$installer_root/scripts"
cat > "$installer_root/scripts/ai-workspace-offline-install.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
[ "${GIT_CONFIG_COUNT:-}" = "1" ]
[ "${GIT_CONFIG_KEY_0:-}" = "safe.directory" ]
[ "${GIT_CONFIG_VALUE_0:-}" = "*" ]
EOF
chmod +x "$installer_root/scripts/ai-workspace-offline-install.sh"
# shellcheck disable=SC2329
validate_offline_package_target() { :; }
# shellcheck disable=SC2329
id() {
[ "${1:-}" = "-u" ] && printf '0\n'
}
run_offline_installer "$installer_root" "debian 13 amd64"
rm -rf "$installer_root"
)
test_linux_root_defaults_to_ubuntu_home() (
# shellcheck disable=SC2329
id() {
if [ "${1:-}" = "-u" ]; then
printf '0\n'
elif [ "${1:-}" = "-un" ]; then
printf 'root\n'
fi
}
# shellcheck disable=SC2329
getent() {
return 1
}
user="$(linux_default_console_user)"
home="$(linux_default_console_home "$user")"
[ "$user" = "ubuntu" ] || fail "root Linux default user was not ubuntu"
[ "$home" = "/home/ubuntu" ] || fail "root Linux default home was not /home/ubuntu"
)
test_linux_non_root_uses_current_user_home() (
# shellcheck disable=SC2329
id() {
if [ "${1:-}" = "-u" ]; then
printf '501\n'
elif [ "${1:-}" = "-un" ]; then
printf 'shenlan\n'
fi
}
# shellcheck disable=SC2329
getent() {
[ "${1:-}" = "passwd" ] && [ "${2:-}" = "shenlan" ] || return 1
printf 'shenlan:x:501:20::/Users/shenlan:/bin/zsh\n'
}
user="$(linux_default_console_user)"
home="$(linux_default_console_home "$user")"
[ "$user" = "shenlan" ] || fail "non-root Linux default user was not current user"
[ "$home" = "/Users/shenlan" ] || fail "non-root Linux default home did not come from passwd"
)
test_linux_identity_vars_can_be_overridden() (
export XWORKSPACE_CONSOLE_USER=deploy
export XWORKSPACE_CONSOLE_HOME=/srv/deploy
user="$(linux_default_console_user)"
home="$(linux_default_console_home "$user")"
[ "$user" = "deploy" ] || fail "explicit console user was ignored"
[ "$home" = "/srv/deploy" ] || fail "explicit console home was ignored"
ANSIBLE_EXTRA_VARS=()
append_linux_console_identity_vars "$user" "$home"
printf '%s\n' "${ANSIBLE_EXTRA_VARS[@]}" | grep -q '^xworkspace_console_user=deploy$' || fail "console user extra var missing"
printf '%s\n' "${ANSIBLE_EXTRA_VARS[@]}" | grep -q '^xworkspace_console_home=/srv/deploy$' || fail "console home extra var missing"
printf '%s\n' "${ANSIBLE_EXTRA_VARS[@]}" | grep -q '^xworkspace_console_repo_dir=/srv/deploy/xworkspace-console$' || fail "console repo extra var missing"
)
test_root_does_not_require_sudo
printf 'ok - root execution does not require sudo\n'
test_non_root_uses_sudo
@ -81,3 +208,19 @@ set -e
[ "$privilege_status" -ne 0 ] || fail "non-root execution without sudo unexpectedly succeeded"
printf '%s' "$privilege_error" | grep -q "Root privileges are required" || fail "missing sudo error was not actionable"
printf 'ok - missing sudo reports a privilege error\n'
test_forced_offline_mode_does_not_refresh_repositories
printf 'ok - forced offline mode does not refresh packaged repositories\n'
test_auto_offline_mode_refreshes_packaged_repositories
printf 'ok - auto offline mode refreshes packaged repositories\n'
test_ubuntu_2604_offline_package_requires_npm
printf 'ok - Ubuntu 26.04 offline package requires npm\n'
test_dynamic_parallel_limit_avoids_awk_reserved_names
printf 'ok - dynamic parallel limit is compatible with modern gawk\n'
test_offline_installer_gets_scoped_git_config
printf 'ok - offline installer receives scoped Git ownership compatibility\n'
test_linux_root_defaults_to_ubuntu_home
printf 'ok - Linux root deployment defaults to ubuntu home\n'
test_linux_non_root_uses_current_user_home
printf 'ok - Linux non-root deployment uses passwd home\n'
test_linux_identity_vars_can_be_overridden
printf 'ok - Linux deployment identity can be overridden\n'