mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:31:58 +00:00
Docker 镜像内置 microVM:构建时装好运行时、预拉沙箱镜像;deploy.sh 检测到 KVM 先试启动一台,起得来才交给容器;没有 KVM 时服务端不再选 microVM
- microvm_sandbox:Linux 上 available() 要求 /dev/kvm 存在且可读写,不可用时给出具体原因(没装、运行时没装、没有 KVM、没有权限) - 新增 app.sandbox.selftest:真起一台 VM 执行一行代码,部署脚本用它判断 - Dockerfile:WITH_MICROVM(默认 1)构建期 install 并 pull python:3.12-slim,自带的 msb、libkrunfw 改为链接,镜像约多 150 MB - deploy.sh:ENABLE_MICROVM=auto|0|1,有 KVM 时用临时容器试启动,成功才加 --device /dev/kvm 和所属组;部署完和 status 打印实际用的沙箱 - package-docker.sh:--no-microvm;部署说明、配置模板、README 同步
This commit is contained in:
+31
-4
@@ -24,8 +24,8 @@ RUN pnpm build
|
||||
# ---- 运行 ----
|
||||
FROM python:3.12-slim
|
||||
|
||||
# bubblewrap:代码沙箱。默认容器权限下它建不了命名空间,服务端会自动退回本地子进程,
|
||||
# 给了额外权限才用得上(见 deploy/agentlab.env 的沙箱一节)。健康检查用 python 自己探,不装 curl
|
||||
# bubblewrap:代码沙箱的第二档。默认容器权限下它建不了命名空间,给了额外权限才用得上
|
||||
# (见 deploy/agentlab.env 的沙箱一节)。第一档是下面的 microVM。健康检查用 python 自己探,不装 curl
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends bubblewrap \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -48,13 +48,39 @@ RUN groupadd --gid 10001 agentlab \
|
||||
&& mkdir -p /data \
|
||||
&& chown 10001:10001 /data
|
||||
|
||||
# ---- microVM:运行时和沙箱镜像在构建时备好,部署的机器不用联网去拉 ----
|
||||
# 宿主机有 KVM、deploy.sh 把 /dev/kvm 交给容器时才用得上;拿不到 KVM 时服务端不会选它。
|
||||
# 不需要的话构建时加 --build-arg WITH_MICROVM=0(package-docker.sh --no-microvm),镜像小约 150 MB。
|
||||
# 放在拷源码之前:拉镜像要一分钟左右,源码改了这一层还能用缓存
|
||||
ARG WITH_MICROVM=1
|
||||
ARG MICROVM_IMAGE=python:3.12-slim
|
||||
USER 10001:10001
|
||||
RUN <<'SH'
|
||||
set -eu
|
||||
[ "$WITH_MICROVM" = 1 ] || exit 0
|
||||
python - <<'PY'
|
||||
import asyncio, microsandbox
|
||||
async def main():
|
||||
await microsandbox.install()
|
||||
asyncio.run(main())
|
||||
PY
|
||||
# install() 把 wheel 自带的 msb 和 libkrunfw 又拷了一份,换成指回原件的链接,省 55 MB
|
||||
bundled="$(python -c 'import microsandbox, os; print(os.path.dirname(microsandbox.__file__))')/_bundled"
|
||||
for f in "$HOME"/.microsandbox/bin/* "$HOME"/.microsandbox/lib/*; do
|
||||
src="$bundled/$(basename "$(dirname "$f")")/$(basename "$f")"
|
||||
if [ -f "$f" ] && [ ! -L "$f" ] && cmp -s "$f" "$src"; then ln -sf "$src" "$f"; fi
|
||||
done
|
||||
"$HOME/.microsandbox/bin/msb" pull "$MICROVM_IMAGE"
|
||||
SH
|
||||
|
||||
WORKDIR /app
|
||||
COPY LICENSE COMMERCIAL-LICENSE.md /app/
|
||||
COPY backend/app /app/backend/app
|
||||
COPY --from=web /src/frontend/dist /app/web
|
||||
|
||||
ENV AGENTLAB_WEB_DIST=/app/web \
|
||||
AGENTLAB_DATA_DIR=/data
|
||||
AGENTLAB_DATA_DIR=/data \
|
||||
AGENTLAB_MICROVM_IMAGE=${MICROVM_IMAGE}
|
||||
VOLUME /data
|
||||
EXPOSE 8000
|
||||
USER 10001:10001
|
||||
@@ -69,7 +95,8 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --start-interval=2s -
|
||||
ARG VERSION=dev
|
||||
LABEL org.opencontainers.image.title="AgentLab" \
|
||||
org.opencontainers.image.description="可视化 Agent 编排实验台" \
|
||||
org.opencontainers.image.version="${VERSION}"
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
agentlab.microvm="${WITH_MICROVM}"
|
||||
|
||||
# 单进程:运行管理器在进程内、数据库是 SQLite,不能开多个 worker
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -67,7 +67,11 @@ http://127.0.0.1:8000 。前端产物不存在或比源码旧时才重新构建
|
||||
`--platform linux/arm64` 或 `linux/amd64`),连同一键部署脚本、配置模板和部署说明打成
|
||||
`release/agentlab-<版本>-linux-<arch>.tar`。拷到只装了 Docker Engine 的离线机器上,解包、按需修改
|
||||
`agentlab.env`、运行 `./deploy.sh` 即可,不需要 docker compose。安装、升级、备份、沙箱权限与常见问题见
|
||||
[deploy/README.md](deploy/README.md)。容器默认权限下代码沙箱退回本地子进程,隔离边界只有容器本身。
|
||||
[deploy/README.md](deploy/README.md)。
|
||||
|
||||
镜像里默认带 microVM 的运行时和沙箱镜像(约 150 MB,`--no-microvm` 可不带)。目标机有 KVM 时,deploy.sh
|
||||
先用临时容器试启动一台 microVM,起得来就把 `/dev/kvm` 交给容器,代码在 microVM 里执行,部署时不用联网。
|
||||
没有 KVM(Mac、Windows 上的 Docker Desktop,多数云主机)时,默认权限下代码沙箱退回本地子进程,隔离边界只有容器本身。
|
||||
|
||||
---
|
||||
|
||||
@@ -496,8 +500,8 @@ Excel / CSV 若传入知识库,只能被切块检索,数字即成为模型
|
||||
|
||||
### 沙箱后端选择
|
||||
|
||||
设置页「运行环境」列出全部候选及其可用性,并标注当前选中项。安装 microVM 依赖且镜像已缓存时
|
||||
auto 选择 `microvm`;否则 macOS 使用 `seatbelt`、Linux 上安装 `bwrap`
|
||||
设置页「运行环境」列出全部候选及其可用性,并标注当前选中项。安装 microVM 依赖、镜像已缓存,
|
||||
Linux 上还要拿得到 `/dev/kvm` 时,auto 选择 `microvm`;否则 macOS 使用 `seatbelt`、Linux 上安装 `bwrap`
|
||||
(`apt install bubblewrap`)后使用 `bubblewrap`。装了 `bwrap` 还要能真正创建命名空间才算可用:
|
||||
启动后首次选择时实际起一次 `bwrap` 探测,Docker 默认权限、Ubuntu 24.04 的 AppArmor 限制下会失败并跳过,
|
||||
「选择原因」写明。均不可用时降级至 `local`——**该路径不含任何访问控制**,界面明确标红。可通过 `AGENTLAB_SANDBOX_BACKEND` 强制指定,
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -34,6 +36,29 @@ _LANG = {
|
||||
|
||||
_MAX_FILE_BYTES = 8 * 1024 * 1024
|
||||
_MANIFEST_CACHE = Path.home() / ".microsandbox" / "cache" / "manifests"
|
||||
_KVM_DEVICE = Path("/dev/kvm")
|
||||
|
||||
|
||||
def _on_linux() -> bool:
|
||||
return sys.platform.startswith("linux")
|
||||
|
||||
|
||||
def kvm_problem() -> str:
|
||||
"""Linux 上 microVM 靠 KVM 启动:拿不到 /dev/kvm 时返回原因,能用(或不在 Linux 上)返回空串。
|
||||
|
||||
macOS 走 Hypervisor.framework,不查。以前 available() 只看运行时装没装,
|
||||
运行时和镜像打进 Docker 镜像以后,没有 KVM 的机器上 auto 会选中 microVM,
|
||||
之后每次执行代码都启动失败。
|
||||
"""
|
||||
if not _on_linux():
|
||||
return ""
|
||||
if not _KVM_DEVICE.exists():
|
||||
return ("这台机器没有 KVM(找不到 /dev/kvm)。"
|
||||
"在容器里运行时,宿主机要支持 KVM,并把 /dev/kvm 挂进容器")
|
||||
if not os.access(_KVM_DEVICE, os.R_OK | os.W_OK):
|
||||
return ("当前用户没有读写 /dev/kvm 的权限。把运行服务的用户加入 kvm 组;"
|
||||
"在容器里运行时,要把宿主机上 /dev/kvm 所属的组加给容器")
|
||||
return ""
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -86,8 +111,7 @@ class MicroVMSandbox(Sandbox):
|
||||
# ---------------- 可用性 ----------------
|
||||
|
||||
@staticmethod
|
||||
def available() -> bool:
|
||||
"""SDK 装了、运行时也装好了才算可用。"""
|
||||
def _runtime_installed() -> bool:
|
||||
try:
|
||||
import microsandbox as ms
|
||||
except ImportError:
|
||||
@@ -97,6 +121,22 @@ class MicroVMSandbox(Sandbox):
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def available(cls) -> bool:
|
||||
"""SDK 和运行时都装好、Linux 上还拿得到 KVM,才算可用。"""
|
||||
return cls._runtime_installed() and not kvm_problem()
|
||||
|
||||
@classmethod
|
||||
def unavailable_reason(cls) -> str:
|
||||
"""不可用的原因;可用时返回空串。"""
|
||||
try:
|
||||
import microsandbox # noqa: F401
|
||||
except ImportError:
|
||||
return "未安装 microsandbox(pip install 'agentlab-backend[microvm]')"
|
||||
if not cls._runtime_installed():
|
||||
return "运行时未就绪(await microsandbox.install(),约 50 MB)"
|
||||
return kvm_problem()
|
||||
|
||||
@staticmethod
|
||||
def image_ready() -> bool:
|
||||
"""镜像是否已经拉到本地。
|
||||
@@ -112,7 +152,7 @@ class MicroVMSandbox(Sandbox):
|
||||
|
||||
async def health(self) -> dict[str, Any]:
|
||||
try:
|
||||
import microsandbox as ms
|
||||
import microsandbox # noqa: F401
|
||||
except ImportError:
|
||||
return {
|
||||
"backend": self.name,
|
||||
@@ -120,17 +160,13 @@ class MicroVMSandbox(Sandbox):
|
||||
"isolated": True,
|
||||
"error": "未安装 microsandbox(pip install 'agentlab-backend[microvm]')",
|
||||
}
|
||||
installed = False
|
||||
try:
|
||||
installed = bool(ms.is_installed())
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
reason = self.unavailable_reason()
|
||||
cached = self.image_ready()
|
||||
return {
|
||||
"backend": self.name,
|
||||
"available": installed,
|
||||
"available": not reason,
|
||||
"isolated": True,
|
||||
"isolation": "microVM:独立内核 + 独立内存 + 独立进程表(libkrun / Hypervisor.framework)",
|
||||
"isolation": "microVM:独立内核 + 独立内存 + 独立进程表(libkrun;Linux 上用 KVM,macOS 上用 Hypervisor.framework)",
|
||||
# 只有 network 不是真的——别的都由虚拟化边界强制执行
|
||||
"enforced": {
|
||||
"timeout": True,
|
||||
@@ -148,7 +184,7 @@ class MicroVMSandbox(Sandbox):
|
||||
"image": self._image,
|
||||
"image_cached": cached,
|
||||
"live_vms": len(self._leases),
|
||||
**({} if installed else {"error": "运行时未就绪(await microsandbox.install(),约 50 MB)"}),
|
||||
**({"error": reason} if reason else {}),
|
||||
**({} if cached else {"first_run_cost": "镜像未缓存,首次启动需要拉取镜像(约 55 秒)"}),
|
||||
}
|
||||
|
||||
@@ -261,10 +297,8 @@ class MicroVMSandbox(Sandbox):
|
||||
if language not in _LANG:
|
||||
return ExecResult(ok=False, backend=self.name, error=f"不支持的语言:{language}")
|
||||
if not self.available():
|
||||
return ExecResult(
|
||||
ok=False, backend=self.name,
|
||||
error="microVM 运行时未就绪:pip install 'agentlab-backend[microvm]' 后执行一次 install",
|
||||
)
|
||||
reason = self.unavailable_reason() or "运行时未就绪"
|
||||
return ExecResult(ok=False, backend=self.name, error=f"microVM 不可用:{reason}")
|
||||
|
||||
ext, cmd = _LANG[language]
|
||||
entry = entry_name(ext)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""microVM 自检:真起一台 VM,跑一行代码。
|
||||
|
||||
deploy.sh 用它决定要不要把 /dev/kvm 交给容器。设备在、权限也对,不等于 VM 起得来:
|
||||
Docker 的系统调用过滤、宿主机没开嵌套虚拟化,都可能让启动失败。起不来还把设备交给容器,
|
||||
auto 就会选中 microVM,之后每次执行代码都失败。
|
||||
|
||||
python -m app.sandbox.selftest
|
||||
|
||||
成功退出码 0,失败 1。输出一行结论,给人看。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from app.sandbox.base import SandboxLimits
|
||||
from app.sandbox.microvm_sandbox import MicroVMSandbox
|
||||
|
||||
MARK = "agentlab-microvm-ok"
|
||||
|
||||
|
||||
async def check() -> tuple[bool, str]:
|
||||
sandbox = MicroVMSandbox()
|
||||
if not sandbox.available():
|
||||
return False, f"microVM 不可用:{sandbox.unavailable_reason() or '运行时未就绪'}"
|
||||
try:
|
||||
result = await sandbox.run(
|
||||
f"print({MARK!r})",
|
||||
limits=SandboxLimits(timeout=60, memory_mb=256, cpus=1, network=False),
|
||||
)
|
||||
finally:
|
||||
await sandbox.close()
|
||||
if result.ok and MARK in result.stdout:
|
||||
return True, f"microVM 可用:启动并执行一行代码共用时 {result.duration_ms} ms"
|
||||
detail = result.error or result.stderr.strip() or f"退出码 {result.exit_code},没有输出"
|
||||
return False, detail if detail.startswith("microVM") else f"microVM 启动失败:{detail}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ok, message = asyncio.run(check())
|
||||
print(message)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Linux 上 microVM 没有 KVM 就起不来,可用性判断要把这一条算进去。
|
||||
|
||||
以前 available() 只看运行时装没装。运行时和沙箱镜像打进 Docker 镜像以后,没有 KVM 的机器上
|
||||
(Mac 上的 Docker Desktop、没开嵌套虚拟化的云主机)auto 会选中 microVM,每次执行代码都启动失败。
|
||||
selftest 是部署脚本用的:真起一台 VM,起得来才把 /dev/kvm 交给容器。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.sandbox import manager as mgr
|
||||
from app.sandbox import microvm_sandbox as mv
|
||||
from app.sandbox import selftest
|
||||
from app.sandbox.base import ExecResult
|
||||
from app.sandbox.local_sandbox import LocalSandbox
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def linux(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
||||
"""在 Linux 上、运行时已装好、镜像已缓存;返回假的 /dev/kvm 路径,默认不存在。"""
|
||||
kvm = tmp_path / "kvm"
|
||||
monkeypatch.setattr(mv, "_on_linux", lambda: True)
|
||||
monkeypatch.setattr(mv, "_KVM_DEVICE", kvm)
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "_runtime_installed", staticmethod(lambda: True))
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "image_ready", staticmethod(lambda: True))
|
||||
return kvm
|
||||
|
||||
|
||||
def test_no_kvm_device_means_unavailable(linux: Path) -> None:
|
||||
assert mv.MicroVMSandbox.available() is False
|
||||
reason = mv.MicroVMSandbox.unavailable_reason()
|
||||
assert "/dev/kvm" in reason and "挂进容器" in reason
|
||||
|
||||
|
||||
def test_kvm_without_permission_means_unavailable(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
linux.touch()
|
||||
monkeypatch.setattr(mv.os, "access", lambda path, mode: False)
|
||||
assert mv.MicroVMSandbox.available() is False
|
||||
assert "kvm 组" in mv.MicroVMSandbox.unavailable_reason()
|
||||
|
||||
|
||||
def test_kvm_present_and_writable(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
linux.touch()
|
||||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||||
assert mv.MicroVMSandbox.available() is True
|
||||
assert mv.MicroVMSandbox.unavailable_reason() == ""
|
||||
|
||||
|
||||
def test_macos_does_not_need_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(mv, "_on_linux", lambda: False)
|
||||
assert not linux.exists()
|
||||
assert mv.kvm_problem() == ""
|
||||
assert mv.MicroVMSandbox.available() is True
|
||||
|
||||
|
||||
def test_runtime_missing_is_reported_before_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "_runtime_installed", staticmethod(lambda: False))
|
||||
assert "运行时未就绪" in mv.MicroVMSandbox.unavailable_reason()
|
||||
|
||||
|
||||
def test_auto_skips_microvm_without_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# 运行时装了、镜像也缓存了,只差 KVM:auto 不能选它
|
||||
monkeypatch.setattr(mgr.BubblewrapSandbox, "available", staticmethod(lambda: False))
|
||||
monkeypatch.setattr(mgr.BubblewrapSandbox, "probe_failure", staticmethod(lambda: ""))
|
||||
monkeypatch.setattr(mgr.SeatbeltSandbox, "available", staticmethod(lambda: False))
|
||||
monkeypatch.setattr(mgr.settings, "sandbox_backend", "auto")
|
||||
manager = mgr.SandboxManager()
|
||||
assert isinstance(manager._resolve(), LocalSandbox)
|
||||
# 节点上选 strict 档也点不到它
|
||||
assert manager._resolve_named("strict") is None
|
||||
|
||||
|
||||
def test_auto_picks_microvm_with_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
linux.touch()
|
||||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||||
monkeypatch.setattr(mgr.settings, "sandbox_backend", "auto")
|
||||
manager = mgr.SandboxManager()
|
||||
assert isinstance(manager._resolve(), mv.MicroVMSandbox)
|
||||
|
||||
|
||||
def test_run_says_why_without_booting(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def must_not_boot(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
||||
raise AssertionError("没有 KVM 还去启动 VM")
|
||||
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "_lease", must_not_boot)
|
||||
result = asyncio.run(mv.MicroVMSandbox().run("print(1)"))
|
||||
assert result.ok is False
|
||||
assert result.error and result.error.startswith("microVM 不可用:") and "/dev/kvm" in result.error
|
||||
|
||||
|
||||
def test_health_reports_missing_kvm(linux: Path) -> None:
|
||||
info = asyncio.run(mv.MicroVMSandbox().health())
|
||||
assert info["available"] is False
|
||||
assert "/dev/kvm" in info["error"]
|
||||
assert "KVM" in info["isolation"]
|
||||
|
||||
|
||||
# ---------------- selftest ----------------
|
||||
|
||||
|
||||
def _fake_run(result: ExecResult, calls: list[str]):
|
||||
async def run(self, code, **kwargs): # noqa: ANN001, ANN003, ANN202
|
||||
calls.append(code)
|
||||
return result
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def test_selftest_without_kvm_does_not_boot(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
calls: list[str] = []
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(ExecResult(), calls))
|
||||
assert selftest.main() == 1
|
||||
assert calls == []
|
||||
out = capsys.readouterr().out.strip()
|
||||
assert out.startswith("microVM 不可用:") and "/dev/kvm" in out
|
||||
|
||||
|
||||
def test_selftest_passes_when_the_vm_prints_the_mark(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
linux.touch()
|
||||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||||
calls: list[str] = []
|
||||
ok = ExecResult(stdout=selftest.MARK + "\n", duration_ms=812, backend="microvm")
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(ok, calls))
|
||||
assert selftest.main() == 0
|
||||
assert len(calls) == 1 and selftest.MARK in calls[0]
|
||||
assert "812 ms" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_selftest_reports_boot_failure(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
linux.touch()
|
||||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||||
failed = ExecResult(ok=False, backend="microvm", error="microVM 启动失败:虚拟机没有应答")
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(failed, []))
|
||||
assert selftest.main() == 1
|
||||
assert capsys.readouterr().out.strip() == "microVM 启动失败:虚拟机没有应答"
|
||||
|
||||
|
||||
def test_selftest_does_not_trust_exit_zero_without_output(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# 退出码 0 但什么都没打印:VM 里的代码其实没跑起来,不能当成功
|
||||
linux.touch()
|
||||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(ExecResult(backend="microvm"), []))
|
||||
assert selftest.main() == 1
|
||||
assert capsys.readouterr().out.strip().startswith("microVM 启动失败:")
|
||||
+40
-12
@@ -21,7 +21,8 @@
|
||||
- **架构要一致**:这个包是 `linux/__AGENTLAB_ARCH__`。在目标机上运行 `uname -m`:
|
||||
`aarch64` 对应 arm64(ARM 单板机要装 64 位系统),`x86_64` 对应 amd64。不一致的镜像跑不起来,
|
||||
deploy.sh 会在导入前就报出来;
|
||||
- 磁盘:镜像导入后约 0.6~0.8 GB(看 Docker 用的存储方式),另留数据目录的空间;
|
||||
- 磁盘:镜像导入后约 0.8~1 GB(看 Docker 用的存储方式),另留数据目录的空间;
|
||||
- 可选:宿主机支持 KVM(有 `/dev/kvm`)时,代码会在 microVM 里执行,隔离最强,见下面的「代码沙箱」;
|
||||
- 当前用户能用 docker(在 `docker` 组里),或者用 `sudo` 运行 deploy.sh。
|
||||
|
||||
## 三步部署
|
||||
@@ -90,7 +91,7 @@ cp ../agentlab-<旧版本>-linux-__AGENTLAB_ARCH__/agentlab.env . # 沿用原
|
||||
## 查看状态和日志
|
||||
|
||||
```bash
|
||||
./deploy.sh status # 容器状态、健康检查、数据目录、访问地址
|
||||
./deploy.sh status # 容器状态、健康检查、数据目录、代码沙箱、访问地址
|
||||
./deploy.sh logs # 最后 200 行
|
||||
./deploy.sh logs -f # 持续跟随,Ctrl-C 退出
|
||||
./deploy.sh logs --tail 1000
|
||||
@@ -123,26 +124,43 @@ cp ../agentlab-<旧版本>-linux-__AGENTLAB_ARCH__/agentlab.env . # 沿用原
|
||||
|
||||
## 代码沙箱
|
||||
|
||||
工作流里的代码节点、界面上的沙箱都在服务端执行代码。容器里能用哪种隔离,实测结果如下:
|
||||
工作流里的代码节点、界面上的沙箱都在服务端执行代码。AgentLab 按下表从上往下,挑第一种能用的:
|
||||
|
||||
| 容器权限 | 实际使用的沙箱 | 隔离效果 |
|
||||
| 沙箱 | 条件 | 隔离效果 |
|
||||
| --- | --- | --- |
|
||||
| 默认(`ENABLE_BWRAP=0`) | local:容器里的普通子进程 | 隔离边界只有容器本身。代码伤不到宿主机,但和 AgentLab 同一个容器、同一个用户,读得到数据目录里的数据库和加密密钥、读得到传进容器的 API Key、能联网 |
|
||||
| `ENABLE_BWRAP=1` | bubblewrap | 代码看不到数据目录、看不到别的进程,默认断网 |
|
||||
| microVM | 宿主机有 KVM(默认 `ENABLE_MICROVM=auto`,自动检测) | 每段代码在独立内核的虚拟机里执行:看不到数据目录、密钥和别的进程,内存上限真正生效 |
|
||||
| bubblewrap | `ENABLE_BWRAP=1` | 共享内核的隔离环境:看不到数据目录和别的进程,默认断网 |
|
||||
| local | 前两种都用不了 | 容器里的普通子进程。隔离边界只有容器本身:代码伤不到宿主机,但和 AgentLab 同一个容器、同一个用户,读得到数据目录里的数据库和加密密钥、读得到传进容器的 API Key、能联网 |
|
||||
|
||||
默认权限下 bubblewrap 无法创建命名空间(Docker 默认的系统调用过滤不允许),AgentLab 会自动退回 local,
|
||||
「设置 → 运行环境」里的「选择原因」会写明。只运行你信得过的工作流;完全不需要执行代码的话,
|
||||
在 `agentlab.env` 里设 `AGENTLAB_SANDBOX_BACKEND=off`。
|
||||
实际用的是哪一种,deploy.sh 部署完会打印「代码沙箱:……」,`./deploy.sh status` 和界面「设置 → 运行环境」里
|
||||
也看得到。只运行你信得过的工作流;完全不需要执行代码的话,在 `agentlab.env` 里设 `AGENTLAB_SANDBOX_BACKEND=off`。
|
||||
|
||||
要用 bubblewrap:把 `agentlab.env` 里的 `ENABLE_BWRAP` 改成 `1`,运行 `./deploy.sh install`。
|
||||
### microVM
|
||||
|
||||
要宿主机是支持 KVM 的 Linux:物理机、树莓派 5 这类 64 位 ARM 单板机、开了嵌套虚拟化的云主机。
|
||||
Mac、Windows 上的 Docker Desktop 和多数云主机没有 KVM,用不了。检查:`ls -l /dev/kvm` 能看到这个设备就行。
|
||||
|
||||
运行时和沙箱镜像(`python:3.12-slim`)已经打在镜像里,部署时不用联网。`./deploy.sh install` 会这样处理:
|
||||
|
||||
1. 本机没有 `/dev/kvm`:不启用,照常部署;
|
||||
2. 有 `/dev/kvm`:先用一个临时容器真起一台 microVM、执行一行代码。起得来,就给正式容器加上
|
||||
`--device /dev/kvm` 和 `/dev/kvm` 所属的组(容器里的服务以 uid 10001 运行,要靠这个组读写它);
|
||||
起不来,打印原因,照常部署但不启用。
|
||||
|
||||
`agentlab.env` 里的 `ENABLE_MICROVM`:`auto` 按上面处理(默认),`1` 起不来就报错停下,`0` 不启用。
|
||||
|
||||
已知缺口:代码节点关掉网络时,HTTP/HTTPS 和域名解析都会断,但 UDP/53 仍然放行,存在经 DNS 外泄数据的可能。
|
||||
|
||||
### bubblewrap
|
||||
|
||||
把 `agentlab.env` 里的 `ENABLE_BWRAP` 改成 `1`,运行 `./deploy.sh install`。microVM 已经启用时用不上它。
|
||||
Docker 默认权限下 bubblewrap 无法创建命名空间(默认的系统调用过滤不允许),所以
|
||||
deploy.sh 会给容器加 `--security-opt seccomp=unconfined`、`--security-opt systempaths=unconfined`、
|
||||
`--security-opt apparmor=unconfined`。代价是整个容器少了系统调用过滤和对 `/proc`、`/sys` 部分路径的遮蔽,
|
||||
内核暴露给容器的面变大(AgentLab 自己仍以非 root 运行)。这组权限在 Docker Desktop(arm64)上实测生效。
|
||||
Ubuntu 24.04 默认还限制非特权 user namespace:开了之后「设置 → 运行环境」里仍显示 local 的话,
|
||||
要在宿主机上执行 `sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0`,这会放宽整台机器的限制,自行权衡。
|
||||
|
||||
microVM(独立内核)需要 `/dev/kvm` 和额外下载的运行时与镜像,离线包默认不支持。
|
||||
|
||||
## 常见问题
|
||||
|
||||
**端口被占用。** deploy.sh 报「端口 8000 已被别的程序占用」,或 docker 报 `port is already allocated` /
|
||||
@@ -165,3 +183,13 @@ microVM(独立内核)需要 `/dev/kvm` 和额外下载的运行时与镜像
|
||||
- `agentlab.env` 里的值带了引号、或者注释写在了值后面:去掉。
|
||||
|
||||
**`sha256sum -c` 报 image.tar FAILED。** 包在拷贝中损坏了,重新拷一份。
|
||||
|
||||
**有 `/dev/kvm`,microVM 却没启用。** deploy.sh 会打印试启动失败的原因。常见的几种:
|
||||
|
||||
- 权限类报错(`Permission denied`、`Operation not permitted`):先确认 `ls -l /dev/kvm` 的属组是 `kvm` 之类的普通组。
|
||||
仍然不行的话,可能是 Docker 默认的系统调用过滤拦住了,可以试 `ENABLE_BWRAP=1`(它会放开这项过滤)再 `./deploy.sh install`;
|
||||
- 云主机上报虚拟化相关的错:机型不支持或没打开嵌套虚拟化,这种情况用不了 microVM;
|
||||
- 想让它起不来就停下、而不是悄悄不用:把 `ENABLE_MICROVM` 设成 `1`。
|
||||
|
||||
**启用 microVM 后,机器重启时容器没起来。** 容器启动时要用到 `/dev/kvm`,开机时 kvm 模块没加载,
|
||||
Docker 就启动不了这个容器。`ls -l /dev/kvm` 确认设备在,再运行 `./deploy.sh install`,它会重新检测。
|
||||
|
||||
+23
-11
@@ -5,7 +5,7 @@
|
||||
# - 值不要加引号,引号会原样变成值的一部分;
|
||||
# - 注释只能单独成行,不能写在值后面。
|
||||
#
|
||||
# 前四项(HOST_PORT、BIND_ADDR、DATA_DIR、CONTAINER_NAME)和 ENABLE_BWRAP 是给 deploy.sh 用的;
|
||||
# 前四项(HOST_PORT、BIND_ADDR、DATA_DIR、CONTAINER_NAME)和 ENABLE_MICROVM、ENABLE_BWRAP 是给 deploy.sh 用的;
|
||||
# 其余的变量原样传进容器,给 AgentLab 自己用。
|
||||
|
||||
# ---- 访问 ----
|
||||
@@ -46,16 +46,30 @@ CONTAINER_NAME=agentlab
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# ---- 代码沙箱 ----
|
||||
# auto:按 microvm → bubblewrap → local 挑第一个能用的。可选值 auto | bubblewrap | local | off
|
||||
#
|
||||
# 容器里实测(Docker 默认权限):bubblewrap 无法创建命名空间,auto 自动退回 local,也就是在容器里
|
||||
# 起一个普通子进程执行代码。这时隔离的边界只有容器本身:代码伤不到宿主机,但和 AgentLab 在同一个容器、
|
||||
# 同一个用户下,读得到数据目录里的数据库和加密密钥、读得到传进容器的 API Key、能联网。
|
||||
# 只运行你信得过的工作流;不需要执行代码就设成 off。
|
||||
# auto:按 microvm → bubblewrap → local 挑第一个能用的。可选值 auto | microvm | bubblewrap | local | off
|
||||
# microvm 每段代码在独立内核的虚拟机里执行,看不到容器里的数据目录、密钥和别的进程,内存上限真正生效。
|
||||
# 隔离最强,但要宿主机有 KVM,见下面的 ENABLE_MICROVM。
|
||||
# bubblewrap 共享内核的隔离环境:看不到数据目录和别的进程,默认断网。要额外的容器权限,见下面的 ENABLE_BWRAP。
|
||||
# local 前两种都用不了时的退路:在容器里起一个普通子进程执行代码。隔离的边界只有容器本身,
|
||||
# 代码伤不到宿主机,但和 AgentLab 在同一个容器、同一个用户下,读得到数据目录里的数据库和
|
||||
# 加密密钥、读得到传进容器的 API Key、能联网。只运行你信得过的工作流。
|
||||
# off 不执行代码。
|
||||
# 实际用的是哪一种,deploy.sh 部署完会打印出来,./deploy.sh status 和界面「设置 → 运行环境」里也看得到。
|
||||
AGENTLAB_SANDBOX_BACKEND=auto
|
||||
#
|
||||
# 想让代码在 bubblewrap 里执行(看不到数据目录、看不到别的进程、默认断网),把下面改成 1,
|
||||
# 然后运行 ./deploy.sh install。deploy.sh 会给容器加三项权限:
|
||||
# microVM 要宿主机是支持 KVM 的 Linux:物理机、树莓派 5 这类 64 位 ARM 单板机、开了嵌套虚拟化的云主机。
|
||||
# Mac、Windows 上的 Docker Desktop 和多数云主机没有 KVM,用不了。
|
||||
# auto 本机有 /dev/kvm 时,先用一个临时容器试着启动一台 microVM,起得来才启用;起不来照常部署,
|
||||
# 只是不用 microVM(默认)
|
||||
# 1 必须启用,启动不了就报错停下
|
||||
# 0 不启用
|
||||
# 启用后 deploy.sh 给容器加上 --device /dev/kvm,以及 /dev/kvm 所属的组(容器里的服务以 uid 10001 运行,
|
||||
# 要靠这个组读写 /dev/kvm)。运行时和沙箱镜像(python:3.12-slim)已经打在镜像里,部署时不用联网。
|
||||
# 已知缺口:代码节点关掉网络时,HTTP/HTTPS 和域名解析都会断,但 UDP/53 仍然放行,存在经 DNS 外泄数据的可能。
|
||||
ENABLE_MICROVM=auto
|
||||
#
|
||||
# 想让代码在 bubblewrap 里执行,把下面改成 1,然后运行 ./deploy.sh install。microVM 已经启用时用不上它。
|
||||
# 容器里实测(Docker 默认权限):bubblewrap 无法创建命名空间,所以 deploy.sh 要给容器加三项权限:
|
||||
# --security-opt seccomp=unconfined 放开 Docker 默认的系统调用过滤,才能创建命名空间
|
||||
# --security-opt systempaths=unconfined 取消对 /proc、/sys 部分路径的遮蔽,才能在沙箱里挂载 /proc
|
||||
# --security-opt apparmor=unconfined Ubuntu 上 Docker 默认的 AppArmor 配置不允许沙箱里的挂载
|
||||
@@ -65,5 +79,3 @@ AGENTLAB_SANDBOX_BACKEND=auto
|
||||
# sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
|
||||
# 这一步会放宽整台机器的限制,自行权衡。
|
||||
ENABLE_BWRAP=0
|
||||
#
|
||||
# microVM(独立内核)需要 /dev/kvm 和额外下载的运行时与镜像,离线包默认不支持。
|
||||
|
||||
+83
-2
@@ -21,10 +21,11 @@ usage() {
|
||||
|
||||
install 部署(默认):导入镜像、创建数据目录、启动容器,等健康检查通过后打印访问地址。
|
||||
已经有同名容器的,先停掉再换成新的,数据保留。改了 agentlab.env 也用它让配置生效。
|
||||
本机有 KVM 时会先试着启动一台 microVM,起得来才让代码在 microVM 里执行(见 agentlab.env)。
|
||||
upgrade 用这个包里的镜像替换正在用的容器,数据保留。旧镜像留着,方便回退。
|
||||
start | stop | restart
|
||||
启动 / 停止 / 重启容器。
|
||||
status 查看容器状态、健康检查、数据目录和访问地址。
|
||||
status 查看容器状态、健康检查、数据目录、代码沙箱和访问地址。
|
||||
logs [-f] [--tail N]
|
||||
查看日志(默认最后 200 行),-f 持续跟随。
|
||||
uninstall [--purge-image]
|
||||
@@ -104,6 +105,7 @@ 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 在同一个目录"
|
||||
@@ -118,7 +120,7 @@ read_env() {
|
||||
key="$(trim "${line%%=*}")"
|
||||
val="${line#*=}"
|
||||
case "$key" in
|
||||
HOST_PORT|BIND_ADDR|DATA_DIR|CONTAINER_NAME|ENABLE_BWRAP)
|
||||
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" ;;
|
||||
@@ -137,6 +139,7 @@ read_env() {
|
||||
''|[!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 所在的目录为准,和从哪里运行无关
|
||||
# 下面比较的就是字面上的 ~,不需要展开
|
||||
@@ -273,6 +276,79 @@ fix_data_owner() {
|
||||
|| 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 "查不到"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 容器
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -318,6 +394,7 @@ run_container() {
|
||||
-v "$DATA_ABS:/data" \
|
||||
--log-opt max-size=10m --log-opt max-file=3 \
|
||||
${extra[@]+"${extra[@]}"} \
|
||||
${MICROVM_OPTS[@]+"${MICROVM_OPTS[@]}"} \
|
||||
"$IMAGE" >/dev/null
|
||||
}
|
||||
|
||||
@@ -397,6 +474,7 @@ deploy_container() {
|
||||
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
|
||||
@@ -420,6 +498,7 @@ cmd_install() {
|
||||
prepare_data
|
||||
check_existing
|
||||
fix_data_owner
|
||||
decide_microvm
|
||||
deploy_container
|
||||
}
|
||||
|
||||
@@ -440,6 +519,7 @@ cmd_upgrade() {
|
||||
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"
|
||||
@@ -492,6 +572,7 @@ cmd_status() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
用法:./scripts/package-docker.sh [--platform linux/arm64|linux/amd64] [--version <版本>] [--out <目录>] [--gzip]
|
||||
用法:./scripts/package-docker.sh [--platform linux/arm64|linux/amd64] [--version <版本>] [--out <目录>] [--gzip] [--no-microvm]
|
||||
|
||||
--platform <平台> 目标机的平台。默认按本机架构取 linux/<arch>。64 位系统的 ARM 单板机是 linux/arm64,
|
||||
普通 PC 和服务器是 linux/amd64。和本机架构不同时 docker 用模拟构建,会慢很多。
|
||||
--version <版本> 镜像标签和包名里的版本。默认 git describe --tags --always --dirty,拿不到就用短哈希加日期。
|
||||
--out <目录> 产物放哪里,默认仓库下的 release/。
|
||||
--gzip 另外再产出一份 .tar.gz。docker 用 containerd 镜像存储时镜像层本身已经压缩过,.tar.gz 小不了多少。
|
||||
--no-microvm 不在镜像里带 microVM 运行时和沙箱镜像,镜像小约 150 MB。目标机没有 KVM 时用不上它们。
|
||||
默认带上:目标机有 KVM 时,deploy.sh 会自动启用 microVM,而且不用联网。
|
||||
-h, --help 显示这段说明。
|
||||
|
||||
产物:<out>/agentlab-<版本>-linux-<arch>.tar,里面有 image.tar、deploy.sh、agentlab.env、README.md、SHA256SUMS。
|
||||
构建需要联网(拉基础镜像、装依赖);部署不需要。镜像只留在本机,不会推到任何仓库。
|
||||
构建需要联网(拉基础镜像、装依赖、拉 microVM 的沙箱镜像);部署不需要。镜像只留在本机,不会推到任何仓库。
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -25,6 +27,7 @@ PLATFORM=""
|
||||
VERSION=""
|
||||
OUT="$ROOT/release"
|
||||
GZIP=""
|
||||
WITH_MICROVM=1
|
||||
need_value() { [ $# -ge 2 ] && [ -n "$2" ] || { echo "$1 后面要跟一个值"; echo; usage; exit 2; }; }
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -35,6 +38,7 @@ while [ $# -gt 0 ]; do
|
||||
--out) need_value "$@"; OUT="$2"; shift ;;
|
||||
--out=*) OUT="${1#--out=}" ;;
|
||||
--gzip) GZIP=1 ;;
|
||||
--no-microvm) WITH_MICROVM=0 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "认不出的参数:$1"; echo; usage; exit 2 ;;
|
||||
esac
|
||||
@@ -86,7 +90,8 @@ OUT="$(cd "$OUT" && pwd)"
|
||||
STAGE="$OUT/$NAME"
|
||||
TAR="$OUT/$NAME.tar"
|
||||
|
||||
echo "==> 版本 $VERSION · 平台 $PLATFORM · 产物 $TAR"
|
||||
if [ "$WITH_MICROVM" = 1 ]; then MICROVM_NOTE="带 microVM"; else MICROVM_NOTE="不带 microVM"; fi
|
||||
echo "==> 版本 $VERSION · 平台 $PLATFORM · $MICROVM_NOTE · 产物 $TAR"
|
||||
|
||||
# ---- 1. 构建 ----
|
||||
# 不带 provenance / SBOM:它们会让导出的镜像多出几份附加清单,老版本的 docker load 认不全
|
||||
@@ -95,6 +100,7 @@ docker buildx build \
|
||||
--platform "$PLATFORM" \
|
||||
--provenance=false --sbom=false \
|
||||
--build-arg VERSION="$VERSION" \
|
||||
--build-arg WITH_MICROVM="$WITH_MICROVM" \
|
||||
--load \
|
||||
-t "$IMAGE" \
|
||||
"$ROOT"
|
||||
@@ -150,7 +156,7 @@ echo
|
||||
echo "完成:"
|
||||
echo " $TAR ($(size "$TAR"))"
|
||||
[ -z "$GZIP" ] || echo " $TAR.gz ($(size "$TAR.gz"))"
|
||||
echo " 镜像 $IMAGE 留在本机(没有推送到任何仓库)"
|
||||
echo " 镜像 $IMAGE 留在本机(没有推送到任何仓库),$MICROVM_NOTE"
|
||||
echo
|
||||
echo "下一步:"
|
||||
echo " 1. 把 $NAME.tar 拷到目标机(scp、U 盘都行)"
|
||||
|
||||
Reference in New Issue
Block a user