mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
db328efd21
- R-a12 create_bytes:硬链接不了的文件系统(exFAT、一部分 SMB / FUSE,EPERM / ENOTSUP / EXDEV)不再首次启动就崩,退回 O_CREAT | O_EXCL 直接建(仍然只建一次,说明里写了写的那一会儿读的一方可能读到不完整);建成后 fsync 目录(同 raw_store) - R-a13 file_write 的追加改回原地追加写(O_APPEND,core/atomic.append_bytes):不换文件(inode、权限、开着的句柄不变),原有内容不动,文件大了也不用整份读出来重写;取舍写在函数说明里——读的一方最多看到半截新内容。覆盖照旧先写临时文件再改名,并沿用原文件的权限 - R-a11 deploy.sh 改属主前认数据目录收紧:只认 agentlab.db、.secret_key、.agentlab-data 三样之一(不再认 uploads/ 这种普通目录名);.agentlab-data 是服务启动时(Settings.ensure_dirs)建的标记,只建一次不覆盖,库在 PostgreSQL、密钥由环境变量给的部署也认得出;老部署有 agentlab.db 或 .secret_key。deploy/README 写明这个文件别删 - 测试:模拟不能硬链接仍只建一次、权限 0600、不留临时文件;覆盖沿用原权限;追加不换 inode、权限不变;file_write 追加不换文件、覆盖照常;数据目录里有标记、重复启动不覆盖
628 lines
24 KiB
Bash
Executable File
628 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# AgentLab 一键部署(离线包自带)。只依赖 docker,不需要 docker compose、不需要联网。
|
||
# 配置在同目录的 agentlab.env 里,用法见 ./deploy.sh --help。
|
||
set -euo pipefail
|
||
|
||
# 下面两个值由 scripts/package-docker.sh 打包时填进来
|
||
VERSION="__AGENTLAB_VERSION__"
|
||
IMAGE_ARCH="__AGENTLAB_ARCH__"
|
||
IMAGE="agentlab:$VERSION"
|
||
|
||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ENV_FILE="$HERE/agentlab.env"
|
||
IMAGE_TAR="$HERE/image.tar"
|
||
|
||
# 给沙箱用 bubblewrap 时容器要加的权限,说明见 agentlab.env 的沙箱一节
|
||
BWRAP_OPTS=(--security-opt seccomp=unconfined --security-opt systempaths=unconfined --security-opt apparmor=unconfined)
|
||
|
||
usage() {
|
||
cat <<USAGE
|
||
用法:./deploy.sh [子命令] [选项]
|
||
|
||
install 部署(默认):导入镜像、创建数据目录、启动容器,等健康检查通过后打印访问地址。
|
||
已经有同名容器的,先停掉再换成新的,数据保留。改了 agentlab.env 也用它让配置生效。
|
||
本机有 KVM 时会先试着启动一台 microVM,起得来才让代码在 microVM 里执行(见 agentlab.env)。
|
||
upgrade 用这个包里的镜像替换正在用的容器,数据保留。旧镜像留着,方便回退。
|
||
start | stop | restart
|
||
启动 / 停止 / 重启容器。
|
||
status 查看容器状态、健康检查、数据目录、代码沙箱和访问地址。
|
||
logs [-f] [--tail N]
|
||
查看日志(默认最后 200 行),-f 持续跟随。
|
||
uninstall [--purge-image]
|
||
删除容器。默认保留数据和镜像,--purge-image 连镜像一起删。数据永远不删。
|
||
-h, --help 显示这段说明。
|
||
|
||
这个包:AgentLab ${VERSION}(linux/${IMAGE_ARCH})
|
||
USAGE
|
||
}
|
||
|
||
say() { printf '==> %s\n' "$*"; }
|
||
warn() { printf '警告:%s\n' "$*" >&2; }
|
||
die() { printf '错误:%s\n' "$*" >&2; exit 1; }
|
||
bad_arg() { printf '认不出的参数:%s\n\n' "$1" >&2; usage >&2; exit 2; }
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 参数:先认完再干活,认不出的直接报错
|
||
# ---------------------------------------------------------------------------
|
||
CMD="install"
|
||
FOLLOW=""
|
||
TAIL="200"
|
||
PURGE_IMAGE=""
|
||
if [ $# -gt 0 ]; then
|
||
case "$1" in
|
||
-h|--help) usage; exit 0 ;;
|
||
-*) ;;
|
||
*) CMD="$1"; shift ;;
|
||
esac
|
||
fi
|
||
case "$CMD" in
|
||
install|upgrade|start|stop|restart|status|logs|uninstall) ;;
|
||
*) printf '认不出的子命令:%s\n\n' "$CMD" >&2; usage >&2; exit 2 ;;
|
||
esac
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
-h|--help) usage; exit 0 ;;
|
||
-f|--follow) [ "$CMD" = logs ] || bad_arg "$1"; FOLLOW=1 ;;
|
||
--tail)
|
||
[ "$CMD" = logs ] && [ $# -ge 2 ] || bad_arg "$1"
|
||
TAIL="$2"; shift ;;
|
||
--tail=*) [ "$CMD" = logs ] || bad_arg "$1"; TAIL="${1#--tail=}" ;;
|
||
--purge-image) [ "$CMD" = uninstall ] || bad_arg "$1"; PURGE_IMAGE=1 ;;
|
||
*) bad_arg "$1" ;;
|
||
esac
|
||
shift
|
||
done
|
||
case "$TAIL" in
|
||
all) ;;
|
||
''|*[!0-9]*) die "--tail 要跟行数或 all:$TAIL" ;;
|
||
esac
|
||
|
||
case "$VERSION$IMAGE_ARCH" in
|
||
*"__AGENTLAB"_*) die "这是仓库里的模板,要用 scripts/package-docker.sh 打出来的离线包里的 deploy.sh" ;;
|
||
esac
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 读 agentlab.env。不 source 它:docker --env-file 不处理引号和变量,这里按同样的规矩逐行读,
|
||
# 两边看到的值才一致;source 还会把值当命令执行
|
||
# ---------------------------------------------------------------------------
|
||
trim() {
|
||
local s="$1"
|
||
s="${s#"${s%%[![:space:]]*}"}"
|
||
s="${s%"${s##*[![:space:]]}"}"
|
||
printf '%s' "$s"
|
||
}
|
||
|
||
unquote() {
|
||
local s="$1"
|
||
case "$s" in
|
||
\"*\"|\'*\') s="${s:1:${#s}-2}" ;;
|
||
esac
|
||
printf '%s' "$s"
|
||
}
|
||
|
||
HOST_PORT="8000"
|
||
BIND_ADDR="0.0.0.0"
|
||
DATA_DIR="./data"
|
||
CONTAINER_NAME="agentlab"
|
||
ENABLE_BWRAP="0"
|
||
ENABLE_MICROVM="auto"
|
||
|
||
read_env() {
|
||
[ -f "$ENV_FILE" ] || die "找不到 ${ENV_FILE},它应该和 deploy.sh 在同一个目录"
|
||
if grep -q "$(printf '\r')" "$ENV_FILE"; then
|
||
die "agentlab.env 是 Windows 换行(CRLF),docker 会把行尾的 \\r 当成值的一部分。先转换:sed -i 's/\\r\$//' agentlab.env"
|
||
fi
|
||
local line key val n=0
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
n=$((n + 1))
|
||
case "$(trim "$line")" in ''|'#'*) continue ;; esac
|
||
case "$line" in *=*) ;; *) continue ;; esac
|
||
key="$(trim "${line%%=*}")"
|
||
val="${line#*=}"
|
||
case "$key" in
|
||
HOST_PORT|BIND_ADDR|DATA_DIR|CONTAINER_NAME|ENABLE_BWRAP|ENABLE_MICROVM)
|
||
printf -v "$key" '%s' "$(unquote "$(trim "$val")")" ;;
|
||
AGENTLAB_DATA_DIR|AGENTLAB_WEB_DIST|AGENTLAB_PORT|AGENTLAB_HOST)
|
||
die "agentlab.env 第 $n 行:不要设 ${key}。容器里的路径和端口是固定的,改宿主机这边用 DATA_DIR、HOST_PORT、BIND_ADDR" ;;
|
||
*)
|
||
case "$val" in
|
||
\"*\"|\'*\') warn "agentlab.env 第 $n 行:$key 的值带了引号。docker 不去引号,引号会原样传进容器,一般要去掉" ;;
|
||
esac ;;
|
||
esac
|
||
done < "$ENV_FILE"
|
||
|
||
case "$HOST_PORT" in ''|*[!0-9]*) die "HOST_PORT 必须是数字:$HOST_PORT" ;; esac
|
||
if [ "$HOST_PORT" -lt 1 ] || [ "$HOST_PORT" -gt 65535 ]; then die "HOST_PORT 超出范围:$HOST_PORT"; fi
|
||
[ -n "$BIND_ADDR" ] || die "BIND_ADDR 不能为空(所有网卡写 0.0.0.0,只给本机用写 127.0.0.1)"
|
||
[ -n "$DATA_DIR" ] || die "DATA_DIR 不能为空"
|
||
case "$CONTAINER_NAME" in
|
||
''|[!a-zA-Z0-9]*|*[!a-zA-Z0-9_.-]*) die "CONTAINER_NAME 只能用字母、数字和 _ . -,并以字母或数字开头:$CONTAINER_NAME" ;;
|
||
esac
|
||
case "$ENABLE_BWRAP" in 0|1) ;; *) die "ENABLE_BWRAP 只能是 0 或 1:$ENABLE_BWRAP" ;; esac
|
||
case "$ENABLE_MICROVM" in auto|0|1) ;; *) die "ENABLE_MICROVM 只能是 auto、0 或 1:$ENABLE_MICROVM" ;; esac
|
||
|
||
# 相对路径以 deploy.sh 所在的目录为准,和从哪里运行无关
|
||
# 下面比较的就是字面上的 ~,不需要展开
|
||
# shellcheck disable=SC2088
|
||
case "$DATA_DIR" in
|
||
/*) DATA_PATH="$DATA_DIR" ;;
|
||
"~"|"~/"*) DATA_PATH="$HOME${DATA_DIR#\~}" ;;
|
||
*) DATA_PATH="$HERE/${DATA_DIR#./}" ;;
|
||
esac
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# docker 与镜像
|
||
# ---------------------------------------------------------------------------
|
||
check_docker() {
|
||
command -v docker >/dev/null 2>&1 || die "没找到 docker。先装好 Docker Engine(离线机器要提前下载好安装包),再运行这个脚本"
|
||
local out
|
||
if ! out="$(docker version --format '{{.Server.Version}}' 2>&1)"; then
|
||
case "$out" in
|
||
*"ermission denied"*)
|
||
die "当前用户没有权限访问 docker。二选一:
|
||
- 用 sudo 运行:sudo ./deploy.sh $CMD
|
||
- 把当前用户加进 docker 组(之后重新登录才生效):sudo usermod -aG docker \$USER" ;;
|
||
*) die "连不上 docker 服务,确认它在运行:sudo systemctl start docker
|
||
docker 的原话:$out" ;;
|
||
esac
|
||
fi
|
||
}
|
||
|
||
host_arch() {
|
||
case "$(uname -m)" in
|
||
x86_64|amd64) echo amd64 ;;
|
||
aarch64|arm64) echo arm64 ;;
|
||
armv7l|armv6l|armhf) echo arm ;;
|
||
*) uname -m ;;
|
||
esac
|
||
}
|
||
|
||
check_arch() {
|
||
local host
|
||
host="$(host_arch)"
|
||
[ "$host" = "$IMAGE_ARCH" ] && return 0
|
||
local hint="在打包的机器上用 ./scripts/package-docker.sh --platform linux/$host 重新打一份。"
|
||
[ "$host" = arm ] && hint="这台机器跑的是 32 位系统,AgentLab 只提供 64 位镜像。ARM 单板机请装 64 位系统(arm64)。"
|
||
die "架构不一致:这个包里的镜像是 linux/${IMAGE_ARCH},本机是 $(uname -m)。
|
||
不同架构的镜像跑不起来(没有模拟层时直接报 exec format error)。$hint"
|
||
}
|
||
|
||
image_present() { docker image inspect "$IMAGE" >/dev/null 2>&1; }
|
||
|
||
verify_tar() {
|
||
# 拷贝中途断了、U 盘出错,docker load 的报错不好懂:先对一下校验和
|
||
[ -f "$HERE/SHA256SUMS" ] || return 0
|
||
command -v sha256sum >/dev/null 2>&1 || return 0
|
||
local line
|
||
line="$(grep ' image.tar$' "$HERE/SHA256SUMS" || true)"
|
||
[ -n "$line" ] || return 0
|
||
say "校验 image.tar"
|
||
(cd "$HERE" && printf '%s\n' "$line" | sha256sum -c --quiet -) \
|
||
|| die "image.tar 校验和对不上,文件不完整或损坏。重新拷一份离线包再试"
|
||
}
|
||
|
||
load_image() {
|
||
[ -f "$IMAGE_TAR" ] || die "本地没有镜像 ${IMAGE},包里也找不到 image.tar"
|
||
verify_tar
|
||
say "导入镜像:docker load -i image.tar(性能较弱的 ARM 单板机上可能要一两分钟)"
|
||
docker load -i "$IMAGE_TAR"
|
||
image_present || die "导入后仍找不到 ${IMAGE},image.tar 可能不是这个版本的"
|
||
}
|
||
|
||
ensure_image() {
|
||
if image_present; then
|
||
say "镜像 $IMAGE 已在本地,跳过导入"
|
||
else
|
||
load_image
|
||
fi
|
||
local arch
|
||
arch="$(docker image inspect -f '{{.Architecture}}' "$IMAGE")"
|
||
[ "$arch" = "$(host_arch)" ] || die "镜像 $IMAGE 的架构是 ${arch},本机是 $(uname -m),跑不起来"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 数据目录
|
||
# ---------------------------------------------------------------------------
|
||
prepare_data() {
|
||
DATA_CREATED=""
|
||
[ -d "$DATA_PATH" ] || DATA_CREATED=1
|
||
mkdir -p "$DATA_PATH" 2>/dev/null \
|
||
|| die "无法创建数据目录 ${DATA_PATH}。权限不够的话用 sudo 运行,或者把 DATA_DIR 改到有写权限的位置"
|
||
DATA_ABS="$(cd "$DATA_PATH" && pwd -P)"
|
||
case "$DATA_ABS" in
|
||
/|/bin|/boot|/dev|/etc|/home|/lib|/opt|/proc|/root|/sbin|/srv|/sys|/usr|/var)
|
||
die "DATA_DIR 不能是系统目录:$DATA_ABS" ;;
|
||
esac
|
||
[ "$DATA_ABS" != "$HOME" ] || die "DATA_DIR 不能是家目录本身:$DATA_ABS"
|
||
}
|
||
|
||
# 已经有同名容器时:它得是 AgentLab 的,数据目录也得和配置的是同一个。
|
||
# 升级解到新目录、DATA_DIR 还是相对路径,是最容易踩的坑:新容器会从一个空库开始,看着像数据丢了
|
||
check_existing() {
|
||
container_exists "$CONTAINER_NAME" || return 0
|
||
check_is_agentlab
|
||
local old_src
|
||
old_src="$(data_source_of "$CONTAINER_NAME")"
|
||
[ -n "$old_src" ] && [ "$old_src" != "$DATA_ABS" ] || return 0
|
||
# 刚才为这次部署新建的空目录收回去,别留一个误导人的空 data
|
||
[ -z "$DATA_CREATED" ] || rmdir "$DATA_ABS" 2>/dev/null || true
|
||
die "现有容器 ${CONTAINER_NAME} 用的数据目录是 ${old_src},而 agentlab.env 配置的是 ${DATA_ABS}。
|
||
直接换过去,新容器会从一个空库开始(旧数据还在原处,不会丢)。
|
||
- 要沿用原来的数据:把 agentlab.env 里的 DATA_DIR 改成 ${old_src} 再运行;
|
||
- 确实要换新目录:先 ./deploy.sh uninstall,再 ./deploy.sh install。"
|
||
}
|
||
|
||
fix_data_owner() {
|
||
# 容器里的服务以固定的非 root 用户运行,数据目录得归它,否则启动时写不进数据库
|
||
local user uid gid owner
|
||
user="$(docker image inspect -f '{{.Config.User}}' "$IMAGE")"
|
||
uid="${user%%:*}"
|
||
gid="${user#*:}"
|
||
[ "$gid" != "$user" ] || gid="$uid"
|
||
case "$uid$gid" in ''|*[!0-9]*) return 0 ;; esac
|
||
# 只看一个目录自己的属主;stat 的参数 GNU 和 BSD 不一样,ls -nd 两边通用
|
||
# shellcheck disable=SC2012
|
||
owner="$(ls -nd "$DATA_ABS" | awk '{print $3}')"
|
||
[ "$owner" = "$uid" ] && return 0
|
||
# 改属主前确认这确实是一个新目录或者 AgentLab 的数据目录,免得 DATA_DIR 写错改了别的东西
|
||
# 认这三样里的一样:agentlab.db(库在 SQLite)、.secret_key(自动生成的加密密钥)、.agentlab-data(服务启动时建的标记,
|
||
# 库放在 PostgreSQL、密钥又由 AGENTLAB_SECRET_KEY 给时只有它)。只有 uploads/ 这类普通目录名的不认,免得 chown 错了别人的目录
|
||
if [ -n "$(ls -A "$DATA_ABS" 2>/dev/null)" ] && [ ! -e "$DATA_ABS/agentlab.db" ] \
|
||
&& [ ! -e "$DATA_ABS/.secret_key" ] && [ ! -e "$DATA_ABS/.agentlab-data" ]; then
|
||
die "DATA_DIR 指向的 $DATA_ABS 不是空目录,也不像 AgentLab 的数据目录(里面没有 agentlab.db、.secret_key、.agentlab-data)。
|
||
为免改错别的文件的属主,请指向一个新目录,或者原来的 AgentLab 数据目录"
|
||
fi
|
||
say "把数据目录的属主改成容器里的用户(uid ${uid}),容器里的服务才写得进去"
|
||
docker run --rm --user 0:0 --network none --entrypoint chown \
|
||
-v "$DATA_ABS:/data" "$IMAGE" -R "$uid:$gid" /data \
|
||
|| die "无法修改 $DATA_ABS 的属主"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# microVM:本机有 KVM、镜像里带了运行时,就先用一个临时容器真起一台 VM,起得来才把 /dev/kvm
|
||
# 交给正式容器。只看设备在不在不够:系统调用过滤、嵌套虚拟化没开,都可能让 VM 起不来。
|
||
# 起不来还把设备交出去,服务端会选中 microVM,之后每次执行代码都失败
|
||
# ---------------------------------------------------------------------------
|
||
KVM_OPTS=()
|
||
MICROVM_OPTS=()
|
||
|
||
kvm_opts() {
|
||
# 容器里的服务以 uid 10001 运行,要靠 /dev/kvm 所属的组才能读写它;其他用户本来就能读写的不用加组
|
||
local perm gid
|
||
perm="$(ls -lL /dev/kvm | cut -c1-10)"
|
||
gid="$(ls -nL /dev/kvm | awk '{print $4}')"
|
||
KVM_OPTS=(--device /dev/kvm)
|
||
case "$perm" in
|
||
???????rw*) ;;
|
||
*) KVM_OPTS+=(--group-add "$gid") ;;
|
||
esac
|
||
}
|
||
|
||
microvm_selftest() {
|
||
# 和正式容器一样的配置和权限,只是不挂数据目录、不开端口。只取最后一行:结论,或者报错的最后一句
|
||
local extra=()
|
||
[ "$ENABLE_BWRAP" = 1 ] && extra=("${BWRAP_OPTS[@]}")
|
||
docker run --rm \
|
||
--env-file "$ENV_FILE" \
|
||
-e AGENTLAB_MICROVM_BOOT_TIMEOUT=120 \
|
||
"${KVM_OPTS[@]}" ${extra[@]+"${extra[@]}"} \
|
||
--entrypoint python "$IMAGE" -m app.sandbox.selftest 2>&1 | tail -n 1
|
||
}
|
||
|
||
decide_microvm() {
|
||
MICROVM_OPTS=()
|
||
[ "$ENABLE_MICROVM" != 0 ] || return 0
|
||
local baked out
|
||
baked="$(docker image inspect -f '{{index .Config.Labels "agentlab.microvm"}}' "$IMAGE" 2>/dev/null || true)"
|
||
if [ "$baked" != 1 ]; then
|
||
[ "$ENABLE_MICROVM" = auto ] || die "ENABLE_MICROVM=1,但这个包打的时候没有带 microVM 运行时(用了 --no-microvm)。
|
||
换一个默认方式打出来的包,或者把 ENABLE_MICROVM 改成 auto"
|
||
return 0
|
||
fi
|
||
if [ ! -e /dev/kvm ]; then
|
||
if [ "$ENABLE_MICROVM" = 1 ]; then
|
||
die "ENABLE_MICROVM=1,但这台机器没有 /dev/kvm,启动不了 microVM。
|
||
- 物理机:确认 CPU 支持虚拟化、BIOS 里已打开,并已加载 kvm 模块(ls -l /dev/kvm 能看到它);
|
||
- 云主机:要选支持嵌套虚拟化的机型并打开这项功能;
|
||
- 不需要 microVM 的话,把 ENABLE_MICROVM 改成 auto 或 0。"
|
||
fi
|
||
say "这台机器没有 KVM(/dev/kvm),不启用 microVM"
|
||
return 0
|
||
fi
|
||
kvm_opts
|
||
say "检测到 /dev/kvm,试着启动一台 microVM(第一次要十几秒到一分钟)"
|
||
if out="$(microvm_selftest)"; then
|
||
MICROVM_OPTS=("${KVM_OPTS[@]}")
|
||
say "$out"
|
||
return 0
|
||
fi
|
||
[ "$ENABLE_MICROVM" = auto ] || die "ENABLE_MICROVM=1,但 microVM 启动不了:${out:-没有输出}
|
||
常见原因见 README 的常见问题。不需要 microVM 的话,把 ENABLE_MICROVM 改成 auto 或 0"
|
||
warn "microVM 启动不了,这次不启用,代码仍按原来的方式执行。原因:${out:-没有输出}"
|
||
}
|
||
|
||
# 服务端实际选中的沙箱。镜像里没有 curl,用 python 从容器里问
|
||
sandbox_summary() {
|
||
docker exec "$CONTAINER_NAME" python -c '
|
||
import json, urllib.request as u
|
||
r = u.build_opener(u.ProxyHandler({})).open("http://127.0.0.1:8000/api/sandbox/health", timeout=5)
|
||
d = json.load(r)
|
||
print(d.get("selected_because") or d.get("backend") or "未知")
|
||
' 2>/dev/null || echo "查不到"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 容器
|
||
# ---------------------------------------------------------------------------
|
||
container_exists() { docker container inspect "$1" >/dev/null 2>&1; }
|
||
|
||
container_state() { docker inspect -f '{{.State.Status}}' "$1" 2>/dev/null || echo missing; }
|
||
|
||
data_source_of() {
|
||
docker inspect -f '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Source}}{{end}}{{end}}' "$1" 2>/dev/null || true
|
||
}
|
||
|
||
require_container() {
|
||
container_exists "$CONTAINER_NAME" || die "没有名为 $CONTAINER_NAME 的容器。还没部署的话运行 ./deploy.sh install"
|
||
}
|
||
|
||
check_is_agentlab() {
|
||
local image
|
||
image="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME")"
|
||
case "$image" in
|
||
agentlab:*) ;;
|
||
*) die "已经有一个叫 $CONTAINER_NAME 的容器,但它用的镜像是 ${image},不是 AgentLab。改一下 agentlab.env 里的 CONTAINER_NAME" ;;
|
||
esac
|
||
}
|
||
|
||
port_in_use() {
|
||
# 查不了(没有 ss)就当没占,交给 docker run 报错
|
||
command -v ss >/dev/null 2>&1 || return 1
|
||
ss -ltnH "sport = :$1" 2>/dev/null | grep -q .
|
||
}
|
||
|
||
container_publishes() {
|
||
docker port "$1" 8000/tcp 2>/dev/null | grep -q ":$2\$"
|
||
}
|
||
|
||
run_container() {
|
||
local extra=()
|
||
[ "$ENABLE_BWRAP" = 1 ] && extra=("${BWRAP_OPTS[@]}")
|
||
docker run -d \
|
||
--name "$CONTAINER_NAME" \
|
||
--restart unless-stopped \
|
||
--env-file "$ENV_FILE" \
|
||
-p "$BIND_ADDR:$HOST_PORT:8000" \
|
||
-v "$DATA_ABS:/data" \
|
||
--log-opt max-size=10m --log-opt max-file=3 \
|
||
${extra[@]+"${extra[@]}"} \
|
||
${MICROVM_OPTS[@]+"${MICROVM_OPTS[@]}"} \
|
||
"$IMAGE" >/dev/null
|
||
}
|
||
|
||
wait_healthy() {
|
||
local name="$1" state health i
|
||
say "等健康检查通过"
|
||
for i in $(seq 1 90); do
|
||
state="$(container_state "$name")"
|
||
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$name" 2>/dev/null || true)"
|
||
case "$state" in
|
||
exited|dead|missing) return 1 ;;
|
||
restarting) [ "$i" -gt 3 ] && return 1 ;;
|
||
esac
|
||
case "$health" in
|
||
healthy) return 0 ;;
|
||
unhealthy) return 1 ;;
|
||
esac
|
||
sleep 2
|
||
done
|
||
return 1
|
||
}
|
||
|
||
print_access() {
|
||
echo
|
||
case "$BIND_ADDR" in
|
||
0.0.0.0)
|
||
local ip
|
||
ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
||
echo " 本机 http://127.0.0.1:$HOST_PORT"
|
||
echo " 局域网 http://${ip:-<本机 IP>}:$HOST_PORT"
|
||
echo
|
||
echo " 注意:AgentLab 没有登录,同一网络里打得开页面的人都能用它(跑工作流、花模型额度、"
|
||
echo " 读数据源、执行代码)。只在信得过的网络里开,并用防火墙限制来源。"
|
||
;;
|
||
127.0.0.1|localhost) echo " http://127.0.0.1:$HOST_PORT (只有本机能打开)" ;;
|
||
*) echo " http://$BIND_ADDR:$HOST_PORT" ;;
|
||
esac
|
||
echo
|
||
}
|
||
|
||
show_failure() {
|
||
echo >&2
|
||
echo "---- 容器最后 60 行日志 ----" >&2
|
||
docker logs --tail 60 "$CONTAINER_NAME" >&2 2>&1 || true
|
||
echo "----" >&2
|
||
}
|
||
|
||
# 起一个新容器替换掉同名的旧容器(没有旧的就直接起)。数据目录不动。
|
||
# 旧容器先改名留着:新容器起不来就换回去;起来了但健康检查没过,告诉用户怎么回退
|
||
deploy_container() {
|
||
local prev="$CONTAINER_NAME-previous" had_old=""
|
||
container_exists "$CONTAINER_NAME" && had_old=1
|
||
|
||
if port_in_use "$HOST_PORT"; then
|
||
if [ -z "$had_old" ] || ! container_publishes "$CONTAINER_NAME" "$HOST_PORT"; then
|
||
die "端口 $HOST_PORT 已被别的程序占用。改 agentlab.env 里的 HOST_PORT,或停掉占用方(sudo ss -ltnp 'sport = :$HOST_PORT' 可以看是谁)"
|
||
fi
|
||
fi
|
||
|
||
if [ -n "$had_old" ]; then
|
||
docker rm -f "$prev" >/dev/null 2>&1 || true
|
||
say "停止现有容器 ${CONTAINER_NAME}(数据保留)"
|
||
docker stop "$CONTAINER_NAME" >/dev/null
|
||
docker rename "$CONTAINER_NAME" "$prev"
|
||
fi
|
||
|
||
say "启动容器 ${CONTAINER_NAME}(${IMAGE})"
|
||
if ! run_container; then
|
||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||
if [ -n "$had_old" ]; then
|
||
docker rename "$prev" "$CONTAINER_NAME" && docker start "$CONTAINER_NAME" >/dev/null \
|
||
&& warn "新容器没能启动,已换回原来的容器"
|
||
fi
|
||
die "容器启动失败,见上面 docker 的报错(端口被占、数据目录权限不对最常见)"
|
||
fi
|
||
|
||
if wait_healthy "$CONTAINER_NAME"; then
|
||
[ -z "$had_old" ] || docker rm "$prev" >/dev/null 2>&1 || true
|
||
say "AgentLab $VERSION 已就绪,数据目录 $DATA_ABS"
|
||
say "代码沙箱:$(sandbox_summary)"
|
||
print_access
|
||
return 0
|
||
fi
|
||
|
||
show_failure
|
||
if [ -n "$had_old" ]; then
|
||
echo "原来的容器还留着,名字改成了 ${prev}。要回退:" >&2
|
||
echo " docker rm -f $CONTAINER_NAME && docker rename $prev $CONTAINER_NAME && docker start $CONTAINER_NAME" >&2
|
||
fi
|
||
die "容器没通过健康检查。看上面的日志;常见原因见 README 的常见问题"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 子命令
|
||
# ---------------------------------------------------------------------------
|
||
cmd_install() {
|
||
check_docker
|
||
check_arch
|
||
read_env
|
||
ensure_image
|
||
prepare_data
|
||
check_existing
|
||
fix_data_owner
|
||
decide_microvm
|
||
deploy_container
|
||
}
|
||
|
||
cmd_upgrade() {
|
||
check_docker
|
||
check_arch
|
||
read_env
|
||
container_exists "$CONTAINER_NAME" || die "没有名为 $CONTAINER_NAME 的容器,这台机器还没部署过。运行 ./deploy.sh install"
|
||
check_is_agentlab
|
||
local old_image
|
||
old_image="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME")"
|
||
say "当前版本 ${old_image#agentlab:},升级到 $VERSION"
|
||
# 总是从包里导入:同名标签在本机可能指向别的构建
|
||
load_image
|
||
local arch
|
||
arch="$(docker image inspect -f '{{.Architecture}}' "$IMAGE")"
|
||
[ "$arch" = "$(host_arch)" ] || die "镜像 $IMAGE 的架构是 ${arch},本机是 $(uname -m),跑不起来"
|
||
prepare_data
|
||
check_existing
|
||
fix_data_owner
|
||
decide_microvm
|
||
deploy_container
|
||
if [ "$old_image" != "$IMAGE" ] && docker image inspect "$old_image" >/dev/null 2>&1; then
|
||
echo "旧镜像 $old_image 还留着,方便回退。确认新版本没问题后可以删:docker rmi $old_image"
|
||
fi
|
||
}
|
||
|
||
cmd_start() {
|
||
check_docker; read_env; require_container
|
||
docker start "$CONTAINER_NAME" >/dev/null
|
||
if wait_healthy "$CONTAINER_NAME"; then
|
||
say "已启动"; print_access
|
||
else
|
||
show_failure; die "容器没通过健康检查"
|
||
fi
|
||
}
|
||
|
||
cmd_stop() {
|
||
check_docker; read_env; require_container
|
||
say "停止 ${CONTAINER_NAME}(数据保留)"
|
||
docker stop "$CONTAINER_NAME" >/dev/null
|
||
say "已停止。开机也不会自动起来,要用时运行 ./deploy.sh start"
|
||
}
|
||
|
||
cmd_restart() {
|
||
check_docker; read_env; require_container
|
||
say "重启 $CONTAINER_NAME"
|
||
docker restart "$CONTAINER_NAME" >/dev/null
|
||
if wait_healthy "$CONTAINER_NAME"; then
|
||
say "已重启"; print_access
|
||
else
|
||
show_failure; die "容器没通过健康检查"
|
||
fi
|
||
}
|
||
|
||
cmd_status() {
|
||
check_docker; read_env
|
||
if ! container_exists "$CONTAINER_NAME"; then
|
||
echo "没有名为 $CONTAINER_NAME 的容器(还没部署,或已卸载)。部署:./deploy.sh install"
|
||
exit 1
|
||
fi
|
||
local state health image src
|
||
state="$(container_state "$CONTAINER_NAME")"
|
||
health=""
|
||
# 停着的容器,健康检查结果是停之前的,不显示免得误导
|
||
[ "$state" != running ] || health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$CONTAINER_NAME")"
|
||
image="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME")"
|
||
src="$(data_source_of "$CONTAINER_NAME")"
|
||
echo "容器 $CONTAINER_NAME"
|
||
echo "状态 $state${health:+(健康检查:${health})}"
|
||
echo "镜像 $image"
|
||
echo "端口 $(docker port "$CONTAINER_NAME" 8000/tcp 2>/dev/null | tr '\n' ' ')"
|
||
echo "数据目录 ${src:-(未挂载)}"
|
||
[ "$state" != running ] || echo "代码沙箱 $(sandbox_summary)"
|
||
echo "这个包 $IMAGE"
|
||
if [ "$state" = running ]; then print_access; fi
|
||
}
|
||
|
||
cmd_logs() {
|
||
check_docker; read_env; require_container
|
||
local args=(--tail "$TAIL")
|
||
[ -z "$FOLLOW" ] || args+=(--follow)
|
||
# exec:Ctrl-C 直接交给 docker logs,不留一个还在跟随的子进程
|
||
exec docker logs "${args[@]}" "$CONTAINER_NAME"
|
||
}
|
||
|
||
cmd_uninstall() {
|
||
check_docker; read_env
|
||
local src="" name
|
||
if container_exists "$CONTAINER_NAME"; then
|
||
check_is_agentlab
|
||
src="$(data_source_of "$CONTAINER_NAME")"
|
||
fi
|
||
for name in "$CONTAINER_NAME" "$CONTAINER_NAME-previous"; do
|
||
if container_exists "$name"; then
|
||
say "删除容器 $name"
|
||
docker rm -f "$name" >/dev/null
|
||
fi
|
||
done
|
||
if [ -n "$PURGE_IMAGE" ]; then
|
||
if image_present; then
|
||
say "删除镜像 $IMAGE"
|
||
docker rmi "$IMAGE" >/dev/null || warn "镜像 $IMAGE 没删掉(可能还有别的容器在用)"
|
||
fi
|
||
else
|
||
echo "镜像 $IMAGE 保留着,重新部署不用再导入。要删:./deploy.sh uninstall --purge-image"
|
||
fi
|
||
[ -n "$src" ] || src="$DATA_PATH"
|
||
echo
|
||
echo "数据没有删,还在:$src"
|
||
echo "确定不要了再手动删除(不可恢复):sudo rm -rf '$src'"
|
||
}
|
||
|
||
case "$CMD" in
|
||
install) cmd_install ;;
|
||
upgrade) cmd_upgrade ;;
|
||
start) cmd_start ;;
|
||
stop) cmd_stop ;;
|
||
restart) cmd_restart ;;
|
||
status) cmd_status ;;
|
||
logs) cmd_logs ;;
|
||
uninstall) cmd_uninstall ;;
|
||
esac
|