Skip to content

开发环境(QEMU 虚拟机) ​

手头只有一台本地机器(Linux 笔记本 / 工作站 / WSL 2),没有独立的云 服务器,但仍然想体验、或者直接上手改 Cube Sandbox?dev-env/ 会在本机起 一台一次性的 OpenCloudOS 9 虚机,把 Cube Sandbox 装在虚机里跑,宿主机 保持干净。

三条命令即可上手:

bash
cd CubeSandbox/dev-env
./create_vm.sh    # ① 创建虚机(仅首次,约 10 分钟)
./run_vm.sh       # ② 在终端 A 启动虚机
./login.sh        # ③ 在终端 B 登录,拿到 root shell

先确认:你该用 dev-env 吗?

  • 已经有云服务器 → 不用往下看了。直接走快速开始的 PVM 部署:在一台普通云服务器(不需要 /dev/kvm)上装完整的 Cube Sandbox, 比在本机再套一层虚机更快、也更贴近生产,细节见 PVM 部署。
  • 只有一台本地机器(能开启 KVM 与嵌套虚拟化)→ 继续往下走。这正是 dev-env/ 要解决的场景:在虚机里随便编译、安装、折腾,不污染宿主机。

不是生产部署方式

dev-env/ 是单机、密码认证、用完即弃的开发 / 体验环境。生产部署请走 快速开始 或 多机集群部署,在 bare-metal 机器上执行。

前置条件 ​

要求说明
宿主机能使用 KVM/dev/kvm 存在且可读写
已开启嵌套虚拟化Cube Sandbox 在虚机内还要再起一层 MicroVM,否则沙箱创建会失败
已安装 qemu、ssh 等命令qemu-system-x86_64(ARM64 上为 qemu-system-aarch64)、qemu-img、curl、ssh、scp、setsid、python3

dev-env/ 面向的是本地环境:WSL 2(Windows 11 22H2+)、Linux 物理机,或本机已开启嵌套虚拟化的 Linux 虚机。如果你的开发机是云服务器, 请改用快速开始的 PVM 部署,不必再套这一层虚机。

快速自检:

bash
ls -l /dev/kvm
cat /sys/module/kvm_intel/parameters/nested   # AMD 则是 kvm_amd,期望 Y / 1

/dev/kvm 存在、nested 为 Y/1,就可以直接往下走了。

自检没过,或者想知道 ARM64 额外要装什么?

见文末 附录 A:宿主机自检与 nested virtualization。

第 1 步:创建虚机(仅首次) ​

bash
cd CubeSandbox/dev-env
./create_vm.sh

脚本会下载官方 OpenCloudOS 9 qcow2(约 800 MB)、把磁盘扩到 100 GB、 启动一次虚机完成初始化,然后干净关机。看到下面的输出就表示成功了:

text
[create_vm][OK] All done:
[create_vm][OK]   1. Image downloaded
[create_vm][OK]   2. qcow2 resized to 100G
[create_vm][OK]   3. VM booted and guest root filesystem expanded
[create_vm][OK]   4. Guest SELinux set to permissive
[create_vm][OK]   5. VM powered off cleanly
[create_vm][INFO] Next steps:
[create_vm][INFO]   1. ./run_vm.sh    # boot the dev VM (terminal A)
[create_vm][INFO]   2. ./login.sh     # log in from another terminal (terminal B)

只需要跑一次。之后再想重建一台干净虚机,删掉 dev-env/.workdir/ 重跑即可。

这十分钟里,虚机内被改了什么?

只有两处:根文件系统撑满 100 GB,以及把 SELinux 调成 permissive。 其他都没有动 —— 没有登录 banner、不改 PATH 与 secure_path、不装 systemd 自启服务,也没有同步二进制 / 收日志的辅助脚本。 细节见文末 附录 B。

第 2 步:启动虚机(终端 A) ​

bash
./run_vm.sh

QEMU 的串口控制台会挂在这个终端里,日志形如:

text
[run_vm][INFO] Booting OpenCloudOS 9 VM
[run_vm][INFO]   Login user : opencloudos
[run_vm][INFO]   SSH        : ssh -p 10022 opencloudos@127.0.0.1

默认只转发 SSH(宿主机 127.0.0.1:10022)。需要访问虚机内的其他端口, 见下文「日常操作 · 访问虚机内的服务」。

关机千万别用 Ctrl+a 然后 x

那相当于硬断电,可能损坏虚机状态。请在另一个终端用 ./login.sh 登录后, 在虚机内执行 poweroff;虚机正常关机后,run_vm.sh 自己会退出。

第 3 步:登录虚机(终端 B) ​

bash
./login.sh

密码由脚本自动处理,登录后直接是 root shell(安装软件、起服务都需要 root)。SSH 端口、用户、密码要改的话见 附录 C。

在虚机里装你需要的东西 ​

guest 就是一台普通的 OpenCloudOS 9 机器:dnf、curl、docker 都能直接 用。以 Cube Sandbox 为例,在虚机内执行官方一键安装:

bash
curl -sL https://github.com/tencentcloud/CubeSandbox/raw/master/deploy/one-click/online-install.sh | bash

国内环境建议走腾讯云镜像

bash
curl -sL https://cnb.cool/CubeSandbox/CubeSandbox/-/git/raw/master/deploy/one-click/online-install.sh | MIRROR=cn bash

安装完成后,按常规快速开始在虚机内创建模板、跑第一个 沙箱即可。

日常操作 ​

传文件(宿主机 ↔ 虚机) ​

SSH 已转发到 127.0.0.1:10022 且允许密码登录(密码 opencloudos), 宿主机上直接用 scp / rsync / ssh 即可。

宿主机 → 虚机

bash
# 单个文件
scp -P 10022 ./local-file opencloudos@127.0.0.1:/tmp/

# 整个目录(递归)
scp -P 10022 -r ./local-dir opencloudos@127.0.0.1:/tmp/

# 大目录增量同步,只传改动过的文件
rsync -avz -e 'ssh -p 10022' ./_output/bin/ opencloudos@127.0.0.1:/tmp/bin/

scp 登录的是普通用户,写不了系统目录

opencloudos 不是 root,/usr/local/bin 这类目录要先落到 /tmp,再用 sudo 安装:

bash
scp -P 10022 ./cubelet opencloudos@127.0.0.1:/tmp/
ssh -p 10022 opencloudos@127.0.0.1 \
  'sudo install -m 0755 /tmp/cubelet /usr/local/bin/cubelet'

虚机 → 宿主机

bash
# 单个文件拉到当前目录
scp -P 10022 opencloudos@127.0.0.1:/data/log/cubelet.log ./

# 整个目录拉下来
scp -P 10022 -r opencloudos@127.0.0.1:/data/log ./guest-logs/

# 或者把命令输出直接写到宿主机文件
ssh -p 10022 opencloudos@127.0.0.1 'sudo journalctl -u cubelet --no-pager' > cubelet-journal.log

访问虚机内的服务 ​

默认只转发 SSH,有两种补法:

启动时按需转发(推荐,长期使用)——EXTRA_FORWARDS 接受空格分隔的 宿主机端口:虚机端口,统一绑定在宿主机 127.0.0.1:

bash
# Cube API -> 宿主机 13000,CubeProxy HTTPS -> 宿主机 11443
EXTRA_FORWARDS="13000:3000 11443:443" ./run_vm.sh

条目格式非法时,脚本会在 QEMU 启动前直接报错退出。

临时开一条隧道(不改启动参数):

bash
ssh -N -L 13000:127.0.0.1:3000 -p 10022 opencloudos@127.0.0.1

关机与重启 ​

bash
# 虚机内
poweroff

# 宿主机上重新启动同一台虚机(虚机内装的东西都还在)
./run_vm.sh

附录 ​

下面是补充说明、参考表格与排查手册。按主线走通之后,遇到问题再来查即可。

附录 A:宿主机自检与 nested virtualization ​

宿主机需要的软件依赖:

  • Linux x86_64 或 aarch64(ARM64),已启用 KVM(存在 /dev/kvm)
  • 开启了 nested virtualization
  • 已安装 qemu-system-x86_64(ARM64 上为 qemu-system-aarch64)、qemu-img、curl、ssh、scp、setsid、python3
aarch64(ARM64)额外依赖

ARM64 上虚机使用 QEMU 的 virt 机型并以 UEFI 固件启动,因此还需安装 EDK2/AAVMF 固件(QEMU_EFI.fd,例如 qemu-efi-aarch64 包)。脚本会自动 检测宿主机架构,必要时可用 TARGET_ARCH 覆盖。

快速自检:

bash
ls -l /dev/kvm

# Intel
cat /sys/module/kvm_intel/parameters/nested
# AMD
cat /sys/module/kvm_amd/parameters/nested

如果 nested 返回 N 或 0,请先在宿主机开启它。以 Intel 为例:

bash
echo 'options kvm_intel nested=1' | sudo tee /etc/modprobe.d/kvm.conf
sudo modprobe -r kvm_intel && sudo modprobe kvm_intel

附录 B:create_vm.sh 在虚机里做了什么 ​

只做两件事,都发生在一次性的首次启动里:

  • 把根分区和根文件系统撑满整个 100 GB 磁盘。
  • 把 SELinux 切成 permissive(运行时 + /etc/selinux/config)。 Cube Sandbox 的 MySQL 容器会把 /docker-entrypoint-initdb.d 以 bind mount 方式挂进容器;如果 SELinux enforcing 加上 container-selinux 策略生效,容器进程会被拒绝,mysql 容器反复重启。

除这两项之外都不自动化:没有登录 banner、不改 PATH 与 secure_path、 不装 systemd 自启单元,也没有二进制同步和收日志的辅助脚本。虚机里需要 什么自己装,传文件用 scp / rsync。

附录 C:端口映射与环境变量 ​

端口映射 ​

run_vm.sh 始终转发 SSH,其余端口通过 EXTRA_FORWARDS 按需追加:

宿主机虚机用途
127.0.0.1:10022:22虚机 SSH
(由 EXTRA_FORWARDS 指定)(自选)任何你想访问的端口,例如用 13000:3000 访问 Cube Sandbox 兼容 E2B 的 API

环境变量 ​

create_vm.sh
变量默认值说明
IMAGE_URLOpenCloudOS 9.6覆盖镜像下载地址。
IMAGE_PATH.workdir/<镜像名>虚机磁盘镜像的完整路径。
TARGET_SIZE100Gqcow2 最终虚拟大小。
SSH_PORT10022转发到 guest 22 的宿主机端口。
VM_USER / VM_PASSWORDopencloudos初始化脚本使用的 guest 凭据。
FORCE_KILL_ON_EXIT0失败时强制杀掉仍在运行的 QEMU,而不是留下它供排查。
run_vm.sh
变量默认值说明
VM_MEMORY_MB8192guest 内存。
VM_CPUS4guest vCPU 数。
SSH_PORT10022宿主机 -> guest SSH。
EXTRA_FORWARDS(空)除 SSH 外额外转发的 宿主机端口:虚机端口 列表(空格分隔)。
REQUIRE_NESTED_KVM1宿主机没开 nested KVM 时拒绝启动。设为 0 可跳过(沙箱跑不起来)。
VM_BACKGROUND0设为 1 时用 -daemonize 后台启动,不占用串口控制台。
login.sh
变量默认值说明
LOGIN_AS_ROOT1设为 0 时留在普通用户,不切 root。

常用组合:

bash
# 给虚机更多资源(默认 4 CPU / 8192 MB)
VM_MEMORY_MB=16384 VM_CPUS=8 ./run_vm.sh

# 额外把 Cube API 与 CubeProxy HTTPS 转发到宿主机
EXTRA_FORWARDS="13000:3000 11443:443" ./run_vm.sh

# 不强制要求 nested KVM(只想把系统起来看看,不跑沙箱)
REQUIRE_NESTED_KVM=0 ./run_vm.sh

# 登录后留在普通用户,不切 root
LOGIN_AS_ROOT=0 ./login.sh

# 创建更大的磁盘镜像(默认 100G)
TARGET_SIZE=200G ./create_vm.sh

所有脚本都支持用 VM_USER / VM_PASSWORD / SSH_HOST / SSH_PORT 覆盖 SSH 连接参数。

附录 D:dev-env 里的 cubecow 存储 ​

Cubelet 默认使用 reflink-only 的 cubecow 存储后端。dev 虚机只需要 data_path 所在文件系统支持 reflink(例如 xfs -m reflink=1 或 Btrfs), 不再需要额外裸盘或 LVM / dm-thin 工具链。 [plugins."io.cubelet.internal.v1.storage".cow.*] 的默认配置会把 reflink 卷落在 <data_path>/../cubecow-reflink 目录下。

附录 E:重置 / 清理 ​

bash
# 1. 停掉正在运行的虚机(终端 A 里 Ctrl+C 之后,或在虚机内 poweroff)
# 2. 删掉虚机磁盘与状态
rm -rf dev-env/.workdir
# 3. 重新创建一台干净虚机
./create_vm.sh

开发环境本身就是一次性的:虚机里装乱了,重建即可。

附录 F:常见问题 ​

现象可能原因解决方法
虚机内没有 /dev/kvm宿主机未开启 nested KVM在宿主机启用 nested virtualization 后重启虚机
./login.sh 连不上虚机还没启动,或宿主机 10022 端口被占确认 ./run_vm.sh 还在运行,或通过 SSH_PORT 换端口
cube-sandbox-mysql 反复重启且报 Permission denied虚机里 SELinux 还是 enforcing在虚机里执行 sudo setenforce 0 && sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config && sudo docker restart cube-sandbox-mysql
df -h / 仍然很小虚机内扩容没走完看 .workdir/qemu-serial.log,再 scp -P 10022 internal/grow_rootfs.sh opencloudos@127.0.0.1:/tmp/,在虚机里 sudo 手动跑一次
EXTRA_FORWARDS 的端口被占宿主机上已有服务占用该端口换一个宿主机端口,例如 EXTRA_FORWARDS="23000:3000"
写了非法的 EXTRA_FORWARDS 直接启动失败条目不是 宿主机端口:虚机端口用空格分隔的数字对,例如 EXTRA_FORWARDS="13000:3000"

附录 G:目录结构 ​

text
dev-env/
├── create_vm.sh       # 一次性:下载 + 扩容 + 首次启动时初始化虚机
├── run_vm.sh          # 日常:启动虚机
├── login.sh           # 日常:SSH 登录并切到 root
├── internal/          # create_vm.sh 传进虚机执行的辅助脚本
│   ├── grow_rootfs.sh
│   └── setup_selinux.sh
├── README.md
└── README_zh.md

更简短的说明见仓库里的 dev-env/README_zh.md。