feat: prefer idempotent offline runtime installs
This commit is contained in:
parent
18d2c69271
commit
3b6b03da95
@ -2,6 +2,8 @@ name: Build Offline AI Workspace All-in-One Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'scripts/create-ai-workspace-offline-package.sh'
|
||||
- 'scripts/ai-workspace-offline-install.sh'
|
||||
@ -97,7 +99,7 @@ jobs:
|
||||
DISTRO_VERSION: ${{ matrix.version }}
|
||||
ARCH: ${{ matrix.arch }}
|
||||
PLAYBOOKS_REF: ${{ github.event.inputs.playbooks_ref || 'main' }}
|
||||
CONSOLE_REF: ${{ github.event.inputs.console_ref || 'main' }}
|
||||
CONSOLE_REF: ${{ github.event_name == 'push' && github.sha || github.event.inputs.console_ref || 'main' }}
|
||||
CORE_SKILLS_REF: ${{ github.event.inputs.core_skills_ref || 'main' }}
|
||||
PACKAGE_VERSION: ${{ github.run_number }}
|
||||
run: |
|
||||
@ -126,6 +128,35 @@ jobs:
|
||||
version: "24.04"
|
||||
arch: amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify online bootstrap hands off to offline installer
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fixture="${RUNNER_TEMP}/offline-handoff"
|
||||
mkdir -p "${fixture}/scripts" "${fixture}/metadata"
|
||||
cat > "${fixture}/metadata/target.env" <<EOF
|
||||
DISTRO_ID=${{ matrix.distro }}
|
||||
DISTRO_VERSION=${{ matrix.version }}
|
||||
ARCH=${{ matrix.arch }}
|
||||
EOF
|
||||
cat > "${fixture}/scripts/ai-workspace-offline-install.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
test "${AI_WORKSPACE_OFFLINE_ACTIVE:-}" = "true"
|
||||
printf 'offline-handoff-ok\n'
|
||||
EOF
|
||||
chmod +x "${fixture}/scripts/ai-workspace-offline-install.sh"
|
||||
output="$(
|
||||
AI_WORKSPACE_OFFLINE_MODE=force \
|
||||
AI_WORKSPACE_OFFLINE_PACKAGE="${fixture}" \
|
||||
AI_WORKSPACE_OFFLINE_DISTRO_ID=${{ matrix.distro }} \
|
||||
AI_WORKSPACE_OFFLINE_DISTRO_VERSION=${{ matrix.version }} \
|
||||
AI_WORKSPACE_OFFLINE_ARCH=${{ matrix.arch }} \
|
||||
bash scripts/setup-ai-workspace-all-in-one.sh
|
||||
)"
|
||||
grep -q 'offline-handoff-ok' <<<"${output}"
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@ -139,6 +170,7 @@ jobs:
|
||||
tar -tzf ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}.tar.gz > contents.txt
|
||||
grep -q 'scripts/ai-workspace-offline-install.sh' contents.txt
|
||||
grep -q 'metadata/manifest.json' contents.txt
|
||||
grep -q 'metadata/target.env' contents.txt
|
||||
grep -q 'repos/playbooks' contents.txt
|
||||
grep -q 'repos/xworkspace-console' contents.txt
|
||||
grep -q 'packages/apt/Packages.gz' contents.txt
|
||||
|
||||
@ -32,12 +32,50 @@ Ubuntu Pro/ESM.
|
||||
|
||||
## Runtime Usage
|
||||
|
||||
Extract the target package on the host and run:
|
||||
The online bootstrap prefers the matching offline package from the
|
||||
`ai-workspace-lab/xworkspace-console` GitHub releases when it is available:
|
||||
|
||||
```bash
|
||||
curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | bash -
|
||||
```
|
||||
|
||||
Set `AI_WORKSPACE_OFFLINE_MODE=off` to force the legacy online-only path, or
|
||||
`AI_WORKSPACE_OFFLINE_MODE=force` to fail when no matching offline package can be
|
||||
prepared.
|
||||
|
||||
The default package source is:
|
||||
|
||||
```text
|
||||
https://github.com/ai-workspace-lab/xworkspace-console/releases/latest/download/ai-workspace-all-in-one-offline-<distro>-<version>-<arch>.tar.gz
|
||||
```
|
||||
|
||||
The latest release tag follows the `offline-ai-workspace-*` pattern.
|
||||
|
||||
For private mirrors or pinned releases, use:
|
||||
|
||||
```bash
|
||||
AI_WORKSPACE_OFFLINE_PACKAGE_BASE_URL=https://mirror.example/offline-package/ai-workspace/offline-ai-workspace-<run_number> \
|
||||
bash scripts/setup-ai-workspace-all-in-one.sh
|
||||
|
||||
AI_WORKSPACE_OFFLINE_RELEASE_TAG=offline-ai-workspace-<run_number> \
|
||||
bash scripts/setup-ai-workspace-all-in-one.sh
|
||||
```
|
||||
|
||||
You can also extract the target package on the host and run:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/ai-workspace-offline-install.sh
|
||||
```
|
||||
|
||||
Pass deployment settings explicitly through `sudo env` when needed:
|
||||
|
||||
```bash
|
||||
sudo env \
|
||||
XWORKMATE_BRIDGE_DOMAIN=acp-bridge.onwalk.net \
|
||||
AI_WORKSPACE_SECURITY_LEVEL=strict \
|
||||
./scripts/ai-workspace-offline-install.sh
|
||||
```
|
||||
|
||||
The script configures a local APT repository, installs bundled binaries, loads
|
||||
packaged container images when Docker is available, and runs the packaged
|
||||
all-in-one bootstrap with local source directories.
|
||||
|
||||
@ -11,9 +11,10 @@
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] 等待并核对 `xworkspace-console` 的离线包 GitHub Actions 发布链路,确认 `publish-release` 完整结束且 release 产物上传成功。
|
||||
- [x] 等待并核对 `xworkspace-console` 的离线包 GitHub Actions 发布链路,确认 `publish-release` 完整结束且 release 产物上传成功。
|
||||
- [ ] 继续核对 `root@acp-bridge.onwalk.net` 的远程部署进度,确认 `setup-ai-workspace-all-in-one.sh` 最终完成并输出统一摘要。
|
||||
- [ ] `setup-ai-workspace-all-in-one.sh` 在目标主机上优先使用离线安装包加速部署,减少在线拉取与安装耗时。
|
||||
- [x] `setup-ai-workspace-all-in-one.sh` 在目标主机上优先使用离线安装包加速部署,减少在线拉取与安装耗时。
|
||||
- [ ] 验证 `setup-ai-workspace-all-in-one.sh` 幂等性:同一主机连续执行两次均成功,复用凭据、离线包缓存与已导入镜像,并安全等待部署/APT 锁。
|
||||
- [ ] 完成最终验收核对:Bridge 对外可达、其余服务默认仅本地监听、`acp-codex` / `opencode` / `gemini` / `hermes` / `qmd` / `litellm` 状态正常。
|
||||
- [ ] 记录最终提交哈希与远端验证结果,回填到本计划的交付结果部分。
|
||||
|
||||
@ -38,6 +39,7 @@
|
||||
- [ ] Bridge 对外使用 `acp-bridge.onwalk.net`,其余服务默认不公开。
|
||||
- [ ] 脚本结束输出统一部署摘要:访问入口、一次性凭据、各服务运行状态、可用 Agent CLI。
|
||||
- [ ] `xfce_desktop / NodeJS / Playwright` 版本均可在单一来源(role defaults)查到并被固定。
|
||||
- [ ] 同一主机连续执行两次安装均成功,第二次执行不生成新凭据、不重复下载同一 release 包,并等待而非破坏并发 APT/dpkg 操作。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -7,6 +7,15 @@ BIN_DIR="${ROOT}/packages/bin"
|
||||
IMAGE_DIR="${ROOT}/packages/images"
|
||||
NPM_CACHE_DIR="${ROOT}/packages/npm-cache"
|
||||
PIP_WHEEL_DIR="${ROOT}/packages/pip"
|
||||
STATE_DIR="${AI_WORKSPACE_OFFLINE_STATE_DIR:-/var/lib/ai-workspace/offline}"
|
||||
AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT="${AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT:-1800}"
|
||||
AI_WORKSPACE_APT_LOCK_TIMEOUT="${AI_WORKSPACE_APT_LOCK_TIMEOUT:-900}"
|
||||
APT_SOURCE_FILE="/etc/apt/sources.list.d/ai-workspace-offline.list"
|
||||
APT_LOCAL_OPTIONS=(
|
||||
-o "Dir::Etc::sourcelist=sources.list.d/ai-workspace-offline.list"
|
||||
-o "Dir::Etc::sourceparts=-"
|
||||
-o "APT::Get::List-Cleanup=0"
|
||||
)
|
||||
|
||||
info() {
|
||||
printf '\033[1;34m[INFO]\033[0m %s\n' "$*" >&2
|
||||
@ -23,6 +32,72 @@ require_root() {
|
||||
fi
|
||||
}
|
||||
|
||||
acquire_deployment_lock() {
|
||||
if [ "${AI_WORKSPACE_DEPLOYMENT_LOCK_HELD:-false}" = "true" ]; then
|
||||
return
|
||||
fi
|
||||
command -v flock >/dev/null 2>&1 || {
|
||||
warn "flock is unavailable; continuing without the deployment serialization lock."
|
||||
return
|
||||
}
|
||||
|
||||
local lock_file="${AI_WORKSPACE_DEPLOYMENT_LOCK_FILE:-/var/lock/ai-workspace-all-in-one.lock}"
|
||||
mkdir -p "$(dirname "${lock_file}")"
|
||||
exec 9>"${lock_file}"
|
||||
info "Waiting for the AI Workspace deployment lock: ${lock_file}"
|
||||
flock -w "${AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT}" 9 || {
|
||||
echo "Timed out waiting for another AI Workspace deployment to finish." >&2
|
||||
exit 1
|
||||
}
|
||||
export AI_WORKSPACE_DEPLOYMENT_LOCK_HELD=true
|
||||
}
|
||||
|
||||
wait_for_apt_locks() {
|
||||
local waited=0
|
||||
local busy
|
||||
|
||||
while true; do
|
||||
busy=false
|
||||
if pgrep -x apt-get >/dev/null 2>&1 ||
|
||||
pgrep -x apt >/dev/null 2>&1 ||
|
||||
pgrep -x dpkg >/dev/null 2>&1 ||
|
||||
pgrep -x unattended-upgrade >/dev/null 2>&1; then
|
||||
busy=true
|
||||
fi
|
||||
local lock
|
||||
for lock in \
|
||||
/var/lib/dpkg/lock-frontend \
|
||||
/var/lib/dpkg/lock \
|
||||
/var/lib/apt/lists/lock \
|
||||
/var/cache/apt/archives/lock; do
|
||||
if [ "${busy}" = "false" ] &&
|
||||
command -v fuser >/dev/null 2>&1 &&
|
||||
[ -e "${lock}" ] &&
|
||||
fuser "${lock}" >/dev/null 2>&1; then
|
||||
busy=true
|
||||
fi
|
||||
if [ "${busy}" = "false" ] &&
|
||||
command -v lslocks >/dev/null 2>&1 &&
|
||||
lslocks -rn -o PATH 2>/dev/null | grep -Fxq "${lock}"; then
|
||||
busy=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${busy}" = "false" ]; then
|
||||
return
|
||||
fi
|
||||
if [ "${waited}" -ge "${AI_WORKSPACE_APT_LOCK_TIMEOUT}" ]; then
|
||||
echo "Timed out after ${AI_WORKSPACE_APT_LOCK_TIMEOUT}s waiting for APT/dpkg locks." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $((waited % 30)) -eq 0 ]; then
|
||||
info "Another package manager is active; waiting for APT/dpkg locks (${waited}s/${AI_WORKSPACE_APT_LOCK_TIMEOUT}s)..."
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
done
|
||||
}
|
||||
|
||||
configure_local_apt_repo() {
|
||||
if [ ! -d "${APT_DIR}" ] || ! compgen -G "${APT_DIR}/*.deb" >/dev/null; then
|
||||
warn "No offline .deb cache found at ${APT_DIR}; skipping local APT repo setup."
|
||||
@ -30,18 +105,44 @@ configure_local_apt_repo() {
|
||||
fi
|
||||
|
||||
info "Configuring local APT repository from ${APT_DIR}"
|
||||
cat > /etc/apt/sources.list.d/ai-workspace-offline.list <<EOF
|
||||
cat > "${APT_SOURCE_FILE}" <<EOF
|
||||
deb [trusted=yes] file:${APT_DIR} ./
|
||||
EOF
|
||||
apt-get update -o Dir::Etc::sourcelist="sources.list.d/ai-workspace-offline.list" \
|
||||
-o Dir::Etc::sourceparts="-" \
|
||||
-o APT::Get::List-Cleanup="0"
|
||||
trap 'rm -f "${APT_SOURCE_FILE}"' EXIT
|
||||
apt-get "${APT_LOCAL_OPTIONS[@]}" update
|
||||
}
|
||||
|
||||
append_available_package() {
|
||||
local -n package_list=$1
|
||||
local package=$2
|
||||
if apt-cache "${APT_LOCAL_OPTIONS[@]}" show "${package}" >/dev/null 2>&1; then
|
||||
package_list+=("${package}")
|
||||
fi
|
||||
}
|
||||
|
||||
install_offline_prerequisites() {
|
||||
local packages=(git curl ansible ca-certificates unzip)
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
if apt-cache "${APT_LOCAL_OPTIONS[@]}" show docker-ce >/dev/null 2>&1; then
|
||||
packages+=(docker-ce docker-ce-cli containerd.io)
|
||||
append_available_package packages docker-buildx-plugin
|
||||
append_available_package packages docker-compose-plugin
|
||||
elif apt-cache "${APT_LOCAL_OPTIONS[@]}" show docker.io >/dev/null 2>&1; then
|
||||
packages+=(docker.io)
|
||||
append_available_package packages docker-compose-plugin
|
||||
fi
|
||||
fi
|
||||
|
||||
wait_for_apt_locks
|
||||
info "Installing bootstrap prerequisites from the bundled APT repository"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get "${APT_LOCAL_OPTIONS[@]}" install \
|
||||
-y --no-install-recommends "${packages[@]}"
|
||||
}
|
||||
|
||||
install_bundled_binaries() {
|
||||
if compgen -G "${BIN_DIR}/vault_*_linux_*.zip" >/dev/null; then
|
||||
info "Installing bundled Vault binary"
|
||||
apt-get install -y unzip || true
|
||||
tmp="$(mktemp -d)"
|
||||
unzip -o "${BIN_DIR}"/vault_*_linux_*.zip -d "${tmp}"
|
||||
install -m 0755 "${tmp}/vault" /usr/local/bin/vault
|
||||
@ -64,9 +165,19 @@ load_container_images() {
|
||||
return
|
||||
fi
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
mkdir -p "${STATE_DIR}/images"
|
||||
systemctl start docker >/dev/null 2>&1 || true
|
||||
for image_tar in "${IMAGE_DIR}"/*.tar; do
|
||||
local checksum marker
|
||||
checksum="$(sha256sum "${image_tar}" | awk '{print $1}')"
|
||||
marker="${STATE_DIR}/images/$(basename "${image_tar}").sha256"
|
||||
if [ "$(cat "${marker}" 2>/dev/null || true)" = "${checksum}" ]; then
|
||||
info "Container image already loaded from ${image_tar}; skipping."
|
||||
continue
|
||||
fi
|
||||
info "Loading container image ${image_tar}"
|
||||
docker load -i "${image_tar}" || true
|
||||
docker load -i "${image_tar}"
|
||||
printf '%s\n' "${checksum}" > "${marker}"
|
||||
done
|
||||
else
|
||||
warn "Docker is not installed yet; bundled image tarballs remain in ${IMAGE_DIR}."
|
||||
@ -76,18 +187,28 @@ load_container_images() {
|
||||
configure_language_package_caches() {
|
||||
if [ -d "${NPM_CACHE_DIR}" ]; then
|
||||
info "Configuring npm to prefer bundled cache"
|
||||
npm config set cache "${NPM_CACHE_DIR}" --global || true
|
||||
npm config set prefer-offline true --global || true
|
||||
touch /etc/npmrc
|
||||
local npmrc_tmp
|
||||
npmrc_tmp="$(mktemp)"
|
||||
awk -F= '$1 != "cache" && $1 != "prefer-offline" { print }' /etc/npmrc > "${npmrc_tmp}"
|
||||
printf 'cache=%s\nprefer-offline=true\n' "${NPM_CACHE_DIR}" >> "${npmrc_tmp}"
|
||||
install -m 0644 "${npmrc_tmp}" /etc/npmrc
|
||||
rm -f "${npmrc_tmp}"
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
npm config set cache "${NPM_CACHE_DIR}" --global || true
|
||||
npm config set prefer-offline true --global || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d "${PIP_WHEEL_DIR}" ]; then
|
||||
info "Configuring pip to prefer bundled wheelhouse"
|
||||
mkdir -p /etc/pip.conf.d
|
||||
cat > /etc/pip.conf <<EOF
|
||||
[global]
|
||||
find-links = ${PIP_WHEEL_DIR}
|
||||
prefer-binary = true
|
||||
EOF
|
||||
export PIP_FIND_LINKS="${PIP_WHEEL_DIR}"
|
||||
export PIP_PREFER_BINARY=true
|
||||
if command -v python3 >/dev/null 2>&1 &&
|
||||
python3 -m pip --version >/dev/null 2>&1; then
|
||||
python3 -m pip config --global set global.find-links "${PIP_WHEEL_DIR}"
|
||||
python3 -m pip config --global set global.prefer-binary true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@ -107,6 +228,9 @@ run_bootstrap() {
|
||||
export XWORKMATE_BRIDGE_PUBLIC_ACCESS="${XWORKMATE_BRIDGE_PUBLIC_ACCESS:-true}"
|
||||
export GATEWAY_OPENCLAW_PUBLIC_ACCESS="${GATEWAY_OPENCLAW_PUBLIC_ACCESS:-false}"
|
||||
export VAULT_PUBLIC_ACCESS="${VAULT_PUBLIC_ACCESS:-false}"
|
||||
export AI_WORKSPACE_OFFLINE_ACTIVE=true
|
||||
export AI_WORKSPACE_DEPLOYMENT_LOCK_HELD=true
|
||||
export AI_WORKSPACE_APT_LOCK_TIMEOUT
|
||||
|
||||
info "Running packaged AI Workspace all-in-one bootstrap"
|
||||
bash "${setup_script}"
|
||||
@ -114,7 +238,10 @@ run_bootstrap() {
|
||||
|
||||
main() {
|
||||
require_root
|
||||
acquire_deployment_lock
|
||||
wait_for_apt_locks
|
||||
configure_local_apt_repo
|
||||
install_offline_prerequisites
|
||||
install_bundled_binaries
|
||||
load_container_images
|
||||
configure_language_package_caches
|
||||
|
||||
@ -117,18 +117,45 @@ fi
|
||||
|
||||
apt-get update -y || true
|
||||
|
||||
packages=(
|
||||
required_packages=(
|
||||
ansible git curl ca-certificates gnupg jq rsync unzip wget
|
||||
)
|
||||
optional_packages=(
|
||||
caddy xfce4 python3 python3-pip python3-venv python3-dev python3-setuptools
|
||||
build-essential pkg-config python-is-python3 pandoc fonts-noto-cjk
|
||||
fonts-noto-cjk-extra fonts-wqy-zenhei fonts-wqy-microhei
|
||||
google-chrome-stable nodejs yarn
|
||||
)
|
||||
packages=()
|
||||
|
||||
for package in "${required_packages[@]}"; do
|
||||
if ! apt-cache show "${package}" >/dev/null 2>&1; then
|
||||
echo "Required APT package is unavailable for this target: ${package}" >&2
|
||||
exit 1
|
||||
fi
|
||||
packages+=("${package}")
|
||||
done
|
||||
|
||||
for package in "${optional_packages[@]}"; do
|
||||
if apt-cache show "${package}" >/dev/null 2>&1; then
|
||||
packages+=("${package}")
|
||||
else
|
||||
echo "Skipping unavailable optional APT package: ${package}" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
if apt-cache show docker-ce >/dev/null 2>&1; then
|
||||
packages+=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
|
||||
packages+=(docker-ce docker-ce-cli containerd.io)
|
||||
for package in docker-buildx-plugin docker-compose-plugin; do
|
||||
if apt-cache show "${package}" >/dev/null 2>&1; then
|
||||
packages+=("${package}")
|
||||
fi
|
||||
done
|
||||
elif apt-cache show docker.io >/dev/null 2>&1; then
|
||||
packages+=(docker.io docker-compose-plugin)
|
||||
packages+=(docker.io)
|
||||
if apt-cache show docker-compose-plugin >/dev/null 2>&1; then
|
||||
packages+=(docker-compose-plugin)
|
||||
fi
|
||||
fi
|
||||
|
||||
if apt-cache show golang-go >/dev/null 2>&1; then
|
||||
@ -138,7 +165,7 @@ if apt-cache show texlive-xetex >/dev/null 2>&1; then
|
||||
packages+=(texlive-xetex texlive-latex-extra texlive-fonts-recommended texlive-lang-chinese latexmk)
|
||||
fi
|
||||
|
||||
apt-get install --download-only -y --no-install-recommends "${packages[@]}" || true
|
||||
apt-get install --download-only -y --no-install-recommends "${packages[@]}"
|
||||
apt-get download "nodejs=${NODEJS_22_VERSION}-1nodesource1" || true
|
||||
apt-get download "nodejs=${NODEJS_24_VERSION}-1nodesource1" || true
|
||||
cp -n ./*.deb /offline-apt/ 2>/dev/null || true
|
||||
@ -215,6 +242,11 @@ export_container_images() {
|
||||
write_manifest() {
|
||||
local manifest="${WORKDIR}/metadata/manifest.json"
|
||||
mkdir -p "$(dirname "${manifest}")"
|
||||
cat > "${WORKDIR}/metadata/target.env" <<EOF
|
||||
DISTRO_ID=${DISTRO_ID}
|
||||
DISTRO_VERSION=${DISTRO_VERSION}
|
||||
ARCH=${ARCH}
|
||||
EOF
|
||||
cat > "${manifest}" <<JSON
|
||||
{
|
||||
"name": "ai-workspace-all-in-one-offline",
|
||||
@ -272,6 +304,13 @@ main() {
|
||||
cat > "${WORKDIR}/README.md" <<'README'
|
||||
# AI Workspace All-in-One Offline Package
|
||||
|
||||
The standard online bootstrap can use matching packages published from
|
||||
`https://github.com/ai-workspace-lab/xworkspace-console/releases` automatically:
|
||||
|
||||
```bash
|
||||
curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | bash -
|
||||
```
|
||||
|
||||
Extract this archive on the target host, then run:
|
||||
|
||||
```bash
|
||||
@ -284,8 +323,14 @@ container images when Docker is available, and runs the packaged
|
||||
directories for playbooks, console, core skills, and bridge code.
|
||||
|
||||
Set the same environment variables supported by the online installer before
|
||||
running the script, for example `TOKEN`, `XWORKMATE_BRIDGE_DOMAIN`, or
|
||||
`AI_WORKSPACE_SECURITY_LEVEL`.
|
||||
running the script, for example:
|
||||
|
||||
```bash
|
||||
sudo env \
|
||||
XWORKMATE_BRIDGE_DOMAIN=acp-bridge.onwalk.net \
|
||||
AI_WORKSPACE_SECURITY_LEVEL=strict \
|
||||
./scripts/ai-workspace-offline-install.sh
|
||||
```
|
||||
README
|
||||
|
||||
tar -czf "${OUT}" -C "$(dirname "${WORKDIR}")" "$(basename "${WORKDIR}")"
|
||||
|
||||
@ -24,6 +24,16 @@ set -euo pipefail
|
||||
# PLAYBOOK_DIR (optional local playbooks checkout; useful for macOS validation)
|
||||
# XWORKSPACE_CONSOLE_DIR (optional local xworkspace-console checkout for macOS)
|
||||
# QMD_SOURCE_REPO / LITELLM_SOURCE_REPO (optional local git sources for offline installs)
|
||||
# AI_WORKSPACE_OFFLINE_MODE=auto (default) | force | off
|
||||
# AI_WORKSPACE_OFFLINE_PACKAGE (local tarball/directory or URL)
|
||||
# AI_WORKSPACE_OFFLINE_PACKAGE_URL (direct tarball URL)
|
||||
# AI_WORKSPACE_OFFLINE_PACKAGE_BASE_URL (mirror directory containing target tarball)
|
||||
# AI_WORKSPACE_OFFLINE_RELEASE_TAG=latest (GitHub release tag or latest)
|
||||
# AI_WORKSPACE_OFFLINE_REPO=ai-workspace-lab/xworkspace-console
|
||||
# AI_WORKSPACE_OFFLINE_AUTO_DOWNLOAD=true (download matching GitHub release package in auto mode)
|
||||
# AI_WORKSPACE_OFFLINE_WORK_DIR=/tmp/ai-workspace-offline
|
||||
# AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT=1800
|
||||
# AI_WORKSPACE_APT_LOCK_TIMEOUT=900
|
||||
# AI_WORKSPACE_DARWIN_MODE=local (default on macOS) | ansible
|
||||
# ==============================================================================
|
||||
|
||||
@ -40,6 +50,12 @@ XWORKMATE_BRIDGE_BRANCH=${XWORKMATE_BRIDGE_BRANCH:-"release/v1.1.4"}
|
||||
XWORKMATE_BRIDGE_SOURCE_DIR=${XWORKMATE_BRIDGE_SOURCE_DIR:-"/tmp/xworkmate-bridge"}
|
||||
AUTH_TOKEN_FILE=${AI_WORKSPACE_AUTH_TOKEN_FILE:-"$HOME/.ai_workspace_auth_token"}
|
||||
VAULT_FILE=${AI_WORKSPACE_VAULT_PASSWORD_FILE:-"$HOME/.vault_password"}
|
||||
AI_WORKSPACE_OFFLINE_MODE=${AI_WORKSPACE_OFFLINE_MODE:-"auto"}
|
||||
AI_WORKSPACE_OFFLINE_REPO=${AI_WORKSPACE_OFFLINE_REPO:-"ai-workspace-lab/xworkspace-console"}
|
||||
AI_WORKSPACE_OFFLINE_RELEASE_TAG=${AI_WORKSPACE_OFFLINE_RELEASE_TAG:-"latest"}
|
||||
AI_WORKSPACE_OFFLINE_WORK_DIR=${AI_WORKSPACE_OFFLINE_WORK_DIR:-"/tmp/ai-workspace-offline"}
|
||||
AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT=${AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT:-"1800"}
|
||||
AI_WORKSPACE_APT_LOCK_TIMEOUT=${AI_WORKSPACE_APT_LOCK_TIMEOUT:-"900"}
|
||||
|
||||
# Function: Output messages
|
||||
info() {
|
||||
@ -48,6 +64,9 @@ info() {
|
||||
success() {
|
||||
echo -e "\033[1;32m[SUCCESS]\033[0m $*" >&2
|
||||
}
|
||||
warn() {
|
||||
echo -e "\033[1;33m[WARN]\033[0m $*" >&2
|
||||
}
|
||||
error() {
|
||||
echo -e "\033[1;31m[ERROR]\033[0m $*" >&2
|
||||
exit 1
|
||||
@ -72,6 +91,77 @@ detect_os() {
|
||||
esac
|
||||
}
|
||||
|
||||
acquire_deployment_lock() {
|
||||
if [ "${AI_WORKSPACE_DEPLOYMENT_LOCK_HELD:-false}" = "true" ]; then
|
||||
return
|
||||
fi
|
||||
if ! command -v flock >/dev/null 2>&1; then
|
||||
warn "flock is unavailable; continuing without the deployment serialization lock."
|
||||
return
|
||||
fi
|
||||
|
||||
local lock_file="${AI_WORKSPACE_DEPLOYMENT_LOCK_FILE:-/var/lock/ai-workspace-all-in-one.lock}"
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
lock_file="${AI_WORKSPACE_DEPLOYMENT_LOCK_FILE:-${TMPDIR:-/tmp}/ai-workspace-all-in-one-${UID}.lock}"
|
||||
fi
|
||||
mkdir -p "$(dirname "$lock_file")"
|
||||
exec 9>"$lock_file"
|
||||
info "Waiting for the AI Workspace deployment lock: $lock_file"
|
||||
if ! flock -w "$AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT" 9; then
|
||||
error "Timed out waiting for another AI Workspace deployment to finish."
|
||||
fi
|
||||
export AI_WORKSPACE_DEPLOYMENT_LOCK_HELD=true
|
||||
}
|
||||
|
||||
wait_for_apt_locks() {
|
||||
if [ ! -f /etc/debian_version ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local timeout="${AI_WORKSPACE_APT_LOCK_TIMEOUT:-900}"
|
||||
local waited=0
|
||||
local busy
|
||||
while true; do
|
||||
busy=false
|
||||
if pgrep -x apt-get >/dev/null 2>&1 ||
|
||||
pgrep -x apt >/dev/null 2>&1 ||
|
||||
pgrep -x dpkg >/dev/null 2>&1 ||
|
||||
pgrep -x unattended-upgrade >/dev/null 2>&1; then
|
||||
busy=true
|
||||
fi
|
||||
local lock
|
||||
for lock in \
|
||||
/var/lib/dpkg/lock-frontend \
|
||||
/var/lib/dpkg/lock \
|
||||
/var/lib/apt/lists/lock \
|
||||
/var/cache/apt/archives/lock; do
|
||||
if [ "$busy" = "false" ] &&
|
||||
command -v fuser >/dev/null 2>&1 &&
|
||||
[ -e "$lock" ] &&
|
||||
fuser "$lock" >/dev/null 2>&1; then
|
||||
busy=true
|
||||
fi
|
||||
if [ "$busy" = "false" ] &&
|
||||
command -v lslocks >/dev/null 2>&1 &&
|
||||
lslocks -rn -o PATH 2>/dev/null | grep -Fxq "$lock"; then
|
||||
busy=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$busy" = "false" ]; then
|
||||
return
|
||||
fi
|
||||
if [ "$waited" -ge "$timeout" ]; then
|
||||
error "Timed out after ${timeout}s waiting for APT/dpkg locks."
|
||||
fi
|
||||
if [ $((waited % 30)) -eq 0 ]; then
|
||||
info "Another package manager is active; waiting for APT/dpkg locks (${waited}s/${timeout}s)..."
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
done
|
||||
}
|
||||
|
||||
install_prerequisites() {
|
||||
local os="$1"
|
||||
info "Installing required dependencies (git, ansible)..."
|
||||
@ -103,6 +193,369 @@ install_prerequisites() {
|
||||
success "Dependencies installed."
|
||||
}
|
||||
|
||||
offline_mode_is_force() {
|
||||
case "$(printf '%s' "${AI_WORKSPACE_OFFLINE_MODE:-auto}" | tr '[:upper:]' '[:lower:]')" in
|
||||
force|required|true|1|yes) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
offline_mode_is_off() {
|
||||
case "$(printf '%s' "${AI_WORKSPACE_OFFLINE_MODE:-auto}" | tr '[:upper:]' '[:lower:]')" in
|
||||
off|disabled|false|0|no) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
offline_fail_or_fallback() {
|
||||
local message=$1
|
||||
if offline_mode_is_force; then
|
||||
error "$message"
|
||||
fi
|
||||
warn "$message Falling back to online bootstrap."
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_offline_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) echo "amd64" ;;
|
||||
aarch64|arm64) echo "arm64" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_offline_target() {
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
local distro="${AI_WORKSPACE_OFFLINE_DISTRO_ID:-${ID:-}}"
|
||||
local version="${AI_WORKSPACE_OFFLINE_DISTRO_VERSION:-${VERSION_ID:-}}"
|
||||
local arch="${AI_WORKSPACE_OFFLINE_ARCH:-}"
|
||||
|
||||
if [ -z "$arch" ]; then
|
||||
arch="$(detect_offline_arch)" || return 1
|
||||
fi
|
||||
|
||||
case "${distro}:${version}" in
|
||||
debian:11|debian:12|debian:13|ubuntu:22.04|ubuntu:24.04|ubuntu:26.04) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
printf '%s %s %s\n' "$distro" "$version" "$arch"
|
||||
}
|
||||
|
||||
offline_package_filename() {
|
||||
local target=$1
|
||||
# shellcheck disable=SC2086
|
||||
set -- $target
|
||||
printf 'ai-workspace-all-in-one-offline-%s-%s-%s.tar.gz\n' "$1" "$2" "$3"
|
||||
}
|
||||
|
||||
offline_release_url() {
|
||||
local filename=$1
|
||||
local tag="${AI_WORKSPACE_OFFLINE_RELEASE_TAG:-latest}"
|
||||
if [ "$tag" = "latest" ]; then
|
||||
printf 'https://github.com/%s/releases/latest/download/%s\n' "$AI_WORKSPACE_OFFLINE_REPO" "$filename"
|
||||
else
|
||||
printf 'https://github.com/%s/releases/download/%s/%s\n' "$AI_WORKSPACE_OFFLINE_REPO" "$tag" "$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
offline_package_source() {
|
||||
local filename=$1
|
||||
if [ -n "${AI_WORKSPACE_OFFLINE_PACKAGE:-}" ]; then
|
||||
printf '%s\n' "$AI_WORKSPACE_OFFLINE_PACKAGE"
|
||||
return
|
||||
fi
|
||||
if [ -n "${AI_WORKSPACE_OFFLINE_PACKAGE_URL:-}" ]; then
|
||||
printf '%s\n' "$AI_WORKSPACE_OFFLINE_PACKAGE_URL"
|
||||
return
|
||||
fi
|
||||
if [ -n "${AI_WORKSPACE_OFFLINE_PACKAGE_BASE_URL:-}" ]; then
|
||||
printf '%s/%s\n' "${AI_WORKSPACE_OFFLINE_PACKAGE_BASE_URL%/}" "$filename"
|
||||
return
|
||||
fi
|
||||
if [ "${AI_WORKSPACE_OFFLINE_AUTO_DOWNLOAD:-true}" = "true" ]; then
|
||||
offline_release_url "$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_offline_source_url() {
|
||||
local source=$1
|
||||
local location
|
||||
|
||||
case "$source" in
|
||||
http://*|https://*)
|
||||
location="$(
|
||||
curl -sSI --connect-timeout 15 --max-time 30 "$source" 2>/dev/null |
|
||||
tr -d '\r' |
|
||||
awk 'tolower($1) == "location:" { print $2; exit }'
|
||||
)"
|
||||
if [ -n "$location" ]; then
|
||||
printf '%s\n' "$location"
|
||||
else
|
||||
printf '%s\n' "$source"
|
||||
fi
|
||||
;;
|
||||
*) printf '%s\n' "$source" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
source_cache_key() {
|
||||
local source=$1
|
||||
printf '%s' "$source" | sha256_stream | cut -c1-16
|
||||
}
|
||||
|
||||
sha256_stream() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum | awk '{print $1}'
|
||||
return
|
||||
fi
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 | awk '{print $1}'
|
||||
return
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
offline_root_from_dir() {
|
||||
local dir=$1
|
||||
if [ -f "$dir/scripts/ai-workspace-offline-install.sh" ]; then
|
||||
cd "$dir"
|
||||
pwd
|
||||
return
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_offline_package() {
|
||||
local package=$1
|
||||
local extract_dir="$AI_WORKSPACE_OFFLINE_WORK_DIR/extracted"
|
||||
local installer archive_checksum cached_checksum cached_root
|
||||
|
||||
validate_offline_archive "$package" || return 1
|
||||
archive_checksum="$(sha256_file "$package")" || return 1
|
||||
cached_checksum="$(cat "$extract_dir/.archive-sha256" 2>/dev/null || true)"
|
||||
cached_root="$(cat "$extract_dir/.package-root" 2>/dev/null || true)"
|
||||
if [ "$cached_checksum" = "$archive_checksum" ] &&
|
||||
[ -n "$cached_root" ] &&
|
||||
[ -f "$cached_root/scripts/ai-workspace-offline-install.sh" ]; then
|
||||
info "Reusing extracted AI Workspace offline package: $cached_root"
|
||||
printf '%s\n' "$cached_root"
|
||||
return
|
||||
fi
|
||||
|
||||
rm -rf "$extract_dir"
|
||||
mkdir -p "$extract_dir"
|
||||
tar -xzf "$package" -C "$extract_dir"
|
||||
installer="$(find "$extract_dir" -mindepth 2 -maxdepth 3 -type f -path '*/scripts/ai-workspace-offline-install.sh' -print -quit)"
|
||||
if [ -z "$installer" ]; then
|
||||
return 1
|
||||
fi
|
||||
cached_root="$(cd "$(dirname "$installer")/.." && pwd)"
|
||||
printf '%s\n' "$archive_checksum" > "$extract_dir/.archive-sha256"
|
||||
printf '%s\n' "$cached_root" > "$extract_dir/.package-root"
|
||||
printf '%s\n' "$cached_root"
|
||||
}
|
||||
|
||||
sha256_file() {
|
||||
local file=$1
|
||||
sha256_stream < "$file"
|
||||
}
|
||||
|
||||
validate_offline_archive() {
|
||||
local package=$1
|
||||
local contents="$AI_WORKSPACE_OFFLINE_WORK_DIR/archive-contents.txt"
|
||||
|
||||
[ -f "$package" ] || return 1
|
||||
tar -tzf "$package" > "$contents" || return 1
|
||||
if awk '
|
||||
/^\/+/ { exit 1 }
|
||||
/(^|\/)\.\.(\/|$)/ { exit 1 }
|
||||
' "$contents"; then
|
||||
return 0
|
||||
fi
|
||||
warn "Offline package contains an unsafe archive path: $package"
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_offline_package_root() {
|
||||
local source=$1
|
||||
local filename=$2
|
||||
local resolved_source cache_key package
|
||||
resolved_source="$(resolve_offline_source_url "$source")"
|
||||
cache_key="$(source_cache_key "$resolved_source")" || return 1
|
||||
package="$AI_WORKSPACE_OFFLINE_WORK_DIR/${cache_key}-${filename}"
|
||||
local partial="${package}.part"
|
||||
|
||||
mkdir -p "$AI_WORKSPACE_OFFLINE_WORK_DIR"
|
||||
|
||||
case "$source" in
|
||||
http://*|https://*)
|
||||
command -v curl >/dev/null 2>&1 || return 1
|
||||
if validate_offline_archive "$package" 2>/dev/null; then
|
||||
info "Reusing cached AI Workspace offline package: $package"
|
||||
else
|
||||
info "Downloading AI Workspace offline package: $resolved_source"
|
||||
if ! curl -fL --retry 3 --retry-delay 5 --continue-at - -o "$partial" "$resolved_source"; then
|
||||
rm -f "$partial"
|
||||
curl -fL --retry 3 --retry-delay 5 -o "$partial" "$resolved_source" || return 1
|
||||
fi
|
||||
validate_offline_archive "$partial" || return 1
|
||||
mv "$partial" "$package"
|
||||
fi
|
||||
extract_offline_package "$package"
|
||||
;;
|
||||
*)
|
||||
if offline_root_from_dir "$source" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$source" ]; then
|
||||
return 1
|
||||
fi
|
||||
extract_offline_package "$source"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
manifest_target_value() {
|
||||
local manifest=$1
|
||||
local key=$2
|
||||
awk -v key="$key" '
|
||||
/"target"[[:space:]]*:/ { in_target=1; next }
|
||||
in_target && /^[[:space:]]*}/ { exit }
|
||||
in_target && $0 ~ "\"" key "\"[[:space:]]*:" {
|
||||
value=$0
|
||||
sub(/^.*:[[:space:]]*"/, "", value)
|
||||
sub(/".*$/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
' "$manifest"
|
||||
}
|
||||
|
||||
validate_offline_package_target() {
|
||||
local root=$1
|
||||
local target=$2
|
||||
local expected_distro expected_version expected_arch
|
||||
local package_distro package_version package_arch
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
set -- $target
|
||||
expected_distro=$1
|
||||
expected_version=$2
|
||||
expected_arch=$3
|
||||
|
||||
if [ -f "$root/metadata/target.env" ]; then
|
||||
package_distro="$(sed -n 's/^DISTRO_ID=//p' "$root/metadata/target.env" | head -n 1)"
|
||||
package_version="$(sed -n 's/^DISTRO_VERSION=//p' "$root/metadata/target.env" | head -n 1)"
|
||||
package_arch="$(sed -n 's/^ARCH=//p' "$root/metadata/target.env" | head -n 1)"
|
||||
elif [ -f "$root/metadata/manifest.json" ]; then
|
||||
package_distro="$(manifest_target_value "$root/metadata/manifest.json" distro)"
|
||||
package_version="$(manifest_target_value "$root/metadata/manifest.json" version)"
|
||||
package_arch="$(manifest_target_value "$root/metadata/manifest.json" arch)"
|
||||
elif [ "${AI_WORKSPACE_OFFLINE_ALLOW_UNVERIFIED_TARGET:-false}" = "true" ]; then
|
||||
warn "Offline package has no target metadata; proceeding by explicit override."
|
||||
return
|
||||
else
|
||||
error "Offline package is missing metadata/target.env and metadata/manifest.json."
|
||||
fi
|
||||
|
||||
if [ "$package_distro" != "$expected_distro" ] ||
|
||||
[ "$package_version" != "$expected_version" ] ||
|
||||
[ "$package_arch" != "$expected_arch" ]; then
|
||||
error "Offline package target ${package_distro}:${package_version}:${package_arch} does not match host ${expected_distro}:${expected_version}:${expected_arch}."
|
||||
fi
|
||||
}
|
||||
|
||||
run_offline_installer() {
|
||||
local root=$1
|
||||
local target=$2
|
||||
local installer="$root/scripts/ai-workspace-offline-install.sh"
|
||||
local env_args=(
|
||||
"AI_WORKSPACE_OFFLINE_ACTIVE=true"
|
||||
"AI_WORKSPACE_DEPLOYMENT_LOCK_HELD=true"
|
||||
)
|
||||
local env_name
|
||||
|
||||
[ -f "$installer" ] || return 1
|
||||
validate_offline_package_target "$root" "$target"
|
||||
chmod +x "$installer"
|
||||
|
||||
for env_name in \
|
||||
AI_WORKSPACE_SECURITY_LEVEL \
|
||||
LITELLM_API_CADDY_STRICT_WHITELIST \
|
||||
XWORKSPACE_CONSOLE_PUBLIC_ACCESS \
|
||||
XWORKMATE_BRIDGE_PUBLIC_ACCESS \
|
||||
XWORKMATE_BRIDGE_DOMAIN \
|
||||
GATEWAY_OPENCLAW_PUBLIC_ACCESS \
|
||||
VAULT_PUBLIC_ACCESS \
|
||||
XWORKSPACE_CONSOLE_ENABLE_XRDP \
|
||||
AI_WORKSPACE_RUNTIME_MODES \
|
||||
POSTGRESQL_DEPLOY_MODE \
|
||||
AI_WORKSPACE_AUTH_TOKEN \
|
||||
XWORKSPACE_CONSOLE_AUTH_TOKEN \
|
||||
XWORKMATE_BRIDGE_AUTH_TOKEN \
|
||||
BRIDGE_AUTH_TOKEN \
|
||||
INTERNAL_SERVICE_TOKEN \
|
||||
DEPLOY_TOKEN \
|
||||
ANSIBLE_VAULT_PASSWORD \
|
||||
AI_WORKSPACE_AUTH_TOKEN_FILE \
|
||||
AI_WORKSPACE_VAULT_PASSWORD_FILE \
|
||||
AI_WORKSPACE_APT_LOCK_TIMEOUT; do
|
||||
if [ -n "${!env_name+x}" ]; then
|
||||
env_args+=("$env_name=${!env_name}")
|
||||
fi
|
||||
done
|
||||
|
||||
info "Using offline AI Workspace package at $root"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
env "${env_args[@]}" bash "$installer"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo env "${env_args[@]}" bash "$installer"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
try_bootstrap_from_offline_package() {
|
||||
if [ "${AI_WORKSPACE_OFFLINE_ACTIVE:-false}" = "true" ]; then
|
||||
return 1
|
||||
fi
|
||||
if offline_mode_is_off; then
|
||||
info "Offline package bootstrap disabled by AI_WORKSPACE_OFFLINE_MODE=$AI_WORKSPACE_OFFLINE_MODE"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(detect_os)" != "linux" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local target filename source root
|
||||
target="$(detect_offline_target)" || {
|
||||
offline_fail_or_fallback "No supported offline package target detected for this host."
|
||||
return 1
|
||||
}
|
||||
filename="$(offline_package_filename "$target")"
|
||||
source="$(offline_package_source "$filename")"
|
||||
if [ -z "$source" ]; then
|
||||
offline_fail_or_fallback "No offline package source is configured."
|
||||
return 1
|
||||
fi
|
||||
|
||||
root="$(prepare_offline_package_root "$source" "$filename")" || {
|
||||
offline_fail_or_fallback "Unable to prepare offline package from $source."
|
||||
return 1
|
||||
}
|
||||
if run_offline_installer "$root" "$target"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
error "Offline package installer failed after making deployment changes; online fallback was not started."
|
||||
}
|
||||
|
||||
resolve_unified_auth_token() {
|
||||
local token="${AI_WORKSPACE_AUTH_TOKEN:-}"
|
||||
if [ -z "$token" ]; then token="${XWORKSPACE_CONSOLE_AUTH_TOKEN:-}"; fi
|
||||
@ -686,7 +1139,8 @@ deploy_launch_agent() {
|
||||
local stderr_log=$5
|
||||
local plist_dir="$HOME/Library/LaunchAgents"
|
||||
local plist="$plist_dir/$label.plist"
|
||||
local domain="gui/$(id -u)"
|
||||
local domain
|
||||
domain="gui/$(id -u)"
|
||||
|
||||
mkdir -p "$plist_dir" "$(dirname "$stdout_log")" "$(dirname "$stderr_log")"
|
||||
cat > "$plist" <<EOF
|
||||
@ -930,6 +1384,14 @@ if [ "$OS_NAME" = "darwin" ] && [ "${AI_WORKSPACE_DARWIN_MODE:-local}" = "local"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$OS_NAME" = "linux" ]; then
|
||||
acquire_deployment_lock
|
||||
wait_for_apt_locks
|
||||
if try_bootstrap_from_offline_package; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v ansible-playbook >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then
|
||||
install_prerequisites "$OS_NAME"
|
||||
fi
|
||||
@ -996,6 +1458,7 @@ append_var "VAULT_DEPLOY_MODE" "vault_deploy_mode"
|
||||
append_var "XWORKSPACE_CONSOLE_ENABLE_XRDP" "xworkspace_console_enable_xrdp"
|
||||
append_var "AI_WORKSPACE_RUNTIME_MODES" "ai_workspace_runtime_modes"
|
||||
append_var "POSTGRESQL_DEPLOY_MODE" "postgresql_deploy_mode"
|
||||
append_var "AI_WORKSPACE_APT_LOCK_TIMEOUT" "ai_workspace_apt_lock_timeout"
|
||||
append_var "QMD_SOURCE_REPO" "qmd_source_repo"
|
||||
append_var "QMD_VERSION" "qmd_version"
|
||||
append_var "LITELLM_SOURCE_REPO" "litellm_source_repo"
|
||||
@ -1048,6 +1511,7 @@ fi
|
||||
chmod 600 "$VAULT_FILE"
|
||||
|
||||
# 6. Run Ansible Playbook locally
|
||||
wait_for_apt_locks
|
||||
info "Running Ansible Playbook locally..."
|
||||
ansible-playbook -i '127.0.0.1,' -c local setup-ai-workspace-all-in-one.yml \
|
||||
--vault-password-file "$VAULT_FILE" \
|
||||
|
||||
Loading…
Reference in New Issue
Block a user