diff --git a/.env.example b/.env.example index 6459949..9f0faea 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,7 @@ GH_PAT= # 多组织共享同一块板时显式设相同 ID: # RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi # RUNNER_RESOURCE_ID_ROC_RK3568_PC=board-roc-rk3568-pc +# RUNNER_RESOURCE_ID_X86_64=board-x86_64 # RUNNER_LOCK_DIR=/tmp/github-runner-locks # RUNNER_LOCK_HOST_PATH=/tmp/github-runner-locks diff --git a/Dockerfile b/Dockerfile index 846842a..4f7eb62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,9 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ build-essential \ pkg-config \ + cmake \ git \ + tftpd-hpa \ ca-certificates \ binfmt-support \ dosfstools \ @@ -55,8 +57,14 @@ RUN apt-get update \ python3-pip \ python3-tomli \ python3-sphinx \ + clang \ + libclang-dev \ + llvm-dev \ ninja-build \ libslirp0 \ + && libclang_so="$(find /usr/lib -path '*/libclang.so' | head -n1)" \ + && test -n "$libclang_so" \ + && ln -sfn "$(dirname "$libclang_so")" /usr/lib/libclang-runtime \ && rm -rf /var/lib/apt/lists/* # Build and install QEMU 10.1.2 from source @@ -80,6 +88,25 @@ RUN mkdir -p /tmp/qemu-build \ RUN usermod -aG dialout runner RUN usermod -aG kvm runner +# Install prebuilt musl cross toolchains +RUN set -eux; \ + mkdir -p /env; \ + cd /env; \ + wget https://musl.cc/aarch64-linux-musl-cross.tgz; \ + wget https://musl.cc/riscv64-linux-musl-cross.tgz; \ + wget https://musl.cc/x86_64-linux-musl-cross.tgz; \ + wget https://github.com/arceos-org/setup-musl/releases/download/prebuilt/loongarch64-linux-musl-cross.tgz; \ + tar zxf aarch64-linux-musl-cross.tgz -C /env; \ + tar zxf riscv64-linux-musl-cross.tgz -C /env; \ + tar zxf x86_64-linux-musl-cross.tgz -C /env; \ + tar zxf loongarch64-linux-musl-cross.tgz -C /env; \ + printf '%s\n' \ + '' \ + '# musl cross toolchains' \ + 'export PATH=/env/x86_64-linux-musl-cross/bin:/env/aarch64-linux-musl-cross/bin:/env/riscv64-linux-musl-cross/bin:/env/loongarch64-linux-musl-cross/bin:$PATH' \ + >> /home/runner/.bashrc; \ + chown runner:runner /home/runner/.bashrc + # 多组织共享硬件锁:runner-wrapper 用于多 org 共享同一硬件时的并发控制(Job 级别锁) COPY runner-wrapper /home/runner/runner-wrapper RUN chmod +x /home/runner/runner-wrapper/runner-wrapper.sh \ @@ -90,7 +117,8 @@ RUN chmod +x /home/runner/runner-wrapper/runner-wrapper.sh \ USER runner # Rust development for runner user -ENV PATH=/home/runner/.cargo/bin:$PATH \ +ENV PATH=/env/x86_64-linux-musl-cross/bin:/env/aarch64-linux-musl-cross/bin:/env/riscv64-linux-musl-cross/bin:/env/loongarch64-linux-musl-cross/bin:/home/runner/.cargo/bin:$PATH \ + LIBCLANG_PATH=/usr/lib/libclang-runtime \ RUST_VERSION=nightly RUN set -eux; \ diff --git a/README.md b/README.md index e197145..fe3d950 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ cp .env.example .env | `./runner.sh rm [runner- ...] [-y]` | Unregister and remove containers; `-y` skips confirmation | | `./runner.sh purge [-y]` | Remove containers and generated files (`docker-compose.yml`, caches, etc.) | -> **Note**: The `init` command creates two hardware-based runners (phytiumpi and roc-rk3568-pc) by default. This behavior is not controlled by the `-n` parameter. +> **Note**: The `init` command creates three hardware-based runners (phytiumpi, roc-rk3568-pc, and x86_64-pc) by default. This behavior is not controlled by the `-n` parameter. ## Configuration @@ -68,7 +68,7 @@ The default prefix automatically includes `ORG` (and `REPO` if set), formatted a name:label1[,label2];name2:label1 ``` -Example: `phytiumpi:arm64,phytiumpi;roc-rk3568-pc:arm64,roc-rk3568-pc` +Example: `phytiumpi:arm64,phytiumpi;roc-rk3568-pc:arm64,roc-rk3568-pc;x86_64-pc:x86_64` Board instances will only use labels defined in `BOARD_RUNNERS` and will not append global `RUNNER_LABELS`. @@ -98,7 +98,7 @@ When multiple containers need to access the same physical hardware, concurrent j ### Configuration -When using environments generated by **runner.sh**, board runners have the wrapper enabled by default with the shared lock directory `/tmp/github-runner-locks` mounted. Board-level environment variables (e.g., `RUNNER_RESOURCE_ID_PHYTIUMPI`) take priority; otherwise, default values are used (phytiumpi: `board-phytiumpi`, roc-rk3568-pc: `board-roc-rk3568-pc`). +When using environments generated by **runner.sh**, board runners have the wrapper enabled by default with the shared lock directory `/tmp/github-runner-locks` mounted. Board-level environment variables (e.g., `RUNNER_RESOURCE_ID_PHYTIUMPI`) take priority; otherwise, default values are used (phytiumpi: `board-phytiumpi`, roc-rk3568-pc: `board-roc-rk3568-pc`, x86_64-pc: `board-x86_64-pc`). To **share the same board across multiple organizations**, simply set the same lock ID for that board in each `.env` file. This ensures that even if runner containers from both organizations receive jobs simultaneously, they will queue via file locks to avoid hardware conflicts. diff --git a/README_CN.md b/README_CN.md index 30ed56c..4c446d6 100644 --- a/README_CN.md +++ b/README_CN.md @@ -54,7 +54,7 @@ cp .env.example .env | `./runner.sh rm [runner- ...] [-y]` | 取消注册并删除容器;`-y` 跳过确认 | | `./runner.sh purge [-y]` | 删除容器并移除生成文件(`docker-compose.yml`、缓存等) | -> **注意**:`init` 命令默认会创建两个基于硬件的 Runner(phytiumpi 和 roc-rk3568-pc),此行为不受 `-n` 参数控制。 +> **注意**:`init` 命令默认会创建三个基于硬件的 Runner(phytiumpi、roc-rk3568-pc 和 x86_64),此行为不受 `-n` 参数控制。 ## 配置说明 @@ -68,7 +68,7 @@ cp .env.example .env name:label1[,label2];name2:label1 ``` -示例:`phytiumpi:arm64,phytiumpi;roc-rk3568-pc:arm64,roc-rk3568-pc` +示例:`phytiumpi:arm64,phytiumpi;roc-rk3568-pc:arm64,roc-rk3568-pc;x86_64:x86_64` 开发板实例将仅使用 `BOARD_RUNNERS` 中定义的标签,不会追加全局 `RUNNER_LABELS`。 @@ -98,7 +98,7 @@ name:label1[,label2];name2:label1 ### 配置方法 -使用 **runner.sh** 生成的环境,板子 runner 默认启用 wrapper 并挂载共享锁目录 `/tmp/github-runner-locks`。优先使用板子级环境变量(如 `RUNNER_RESOURCE_ID_PHYTIUMPI`),否则使用默认值(phytiumpi: `board-phytiumpi`,roc-rk3568-pc: `board-roc-rk3568-pc`)。 +使用 **runner.sh** 生成的环境,板子 runner 默认启用 wrapper 并挂载共享锁目录 `/tmp/github-runner-locks`。优先使用板子级环境变量(如 `RUNNER_RESOURCE_ID_PHYTIUMPI`),否则使用默认值(phytiumpi: `board-phytiumpi`,roc-rk3568-pc: `board-roc-rk3568-pc`,x86_64-pc: `board-x86_64-pc`)。 如果要在**多组织中共享同一块板时**,则只需要在各自的 `.env` 中为该板设置相同的锁 ID。这样,即使两个组织的 runner 容器同时收到 job,也会通过文件锁排队执行,避免硬件冲突。 diff --git "a/docs/PXE\351\203\250\347\275\262\350\257\264\346\230\216.md" "b/docs/PXE\351\203\250\347\275\262\350\257\264\346\230\216.md" new file mode 100644 index 0000000..4851747 --- /dev/null +++ "b/docs/PXE\351\203\250\347\275\262\350\257\264\346\230\216.md" @@ -0,0 +1,336 @@ +# PXE 使用说明 + +本文档用于指导在当前仓库中使用 `runner.sh pxe` 部署和维护 PXE 服务,实现: + +- 在全新环境中通过单条命令完成 PXE 基础部署 +- 自动生成 `dnsmasq` 配置并准备 TFTP 启动文件 +- 统一通过 `runner.sh` 管理 PXE 的安装、启动、停止和状态查看 +- 将 PXE 服务部署与业务 `kernel` 发布解耦 + +--- + +## 1. 适用场景 + +- 需要在一台 Linux 主机上快速搭建 PXE 启动服务。 +- 希望统一使用 `runner.sh` 管理,而不再单独维护 `pxe.sh` 主逻辑。 +- 需要将仓库中的 PXE 模板文件渲染后部署到系统目录,例如: + - `github-runners/pxe-boot/pxe-physical.conf` → `/etc/dnsmasq.d/pxe-physical.conf` + - `github-runners/pxe-boot/boot.ipxe` → `/boot.ipxe` + - `github-runners/pxe-boot/grub-embedded.cfg` → 动态生成 `grubx64.efi` + +--- + +## 2. 前置条件 + +- 一台 Linux 主机。 +- 具备 `root` 或 `sudo` 权限。 +- 目标网卡已经配置 IPv4 地址。 +- 仓库中已包含 PXE 模板目录: + +```text +github-runners/ +├── runner.sh +└── pxe-boot/ + ├── pxe-physical.conf + ├── boot.ipxe + ├── grub-embedded.cfg + └── ipxe-mb.efi +``` + +> 注意:`pxe --install` 现在默认不会发布 `kernel`。它负责准备 PXE 服务、iPXE 和 GRUB 引导链;`kernel` 或 `grub.cfg` 可以在后续由其他流程放入 TFTP 目录。 + +--- + +## 3. 命令入口 + +统一入口为: + +```bash +./runner.sh pxe [options] +``` + +--- + +## 4. 快速开始 + +推荐首次部署命令: + +```bash +sudo ./runner.sh pxe --install +``` + +如果需要指定网卡和模式: + +```bash +sudo ./runner.sh pxe \ + --install \ + --interface eno1np0 \ + --mode proxy +``` + +部署完成后查看状态: + +```bash +./runner.sh pxe --status +``` + +--- + +## 5. 常用命令 + +### 5.1 安装并部署 + +```bash +sudo ./runner.sh pxe --install +``` + +### 5.2 启动服务 + +```bash +sudo ./runner.sh pxe --start +``` + +### 5.3 停止服务 + +```bash +sudo ./runner.sh pxe --stop +``` + +### 5.4 查看状态 + +```bash +./runner.sh pxe --status +``` + +### 5.5 清理环境 + +```bash +sudo ./runner.sh pxe --clean +``` + +跳过确认: + +```bash +sudo ./runner.sh pxe --clean --yes +``` + +--- + +## 6. 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `--install` | 安装依赖、渲染配置、准备 TFTP 文件并启动服务 | - | +| `--start` | 启动 `dnsmasq` PXE 服务 | - | +| `--stop` | 停止 `dnsmasq` PXE 服务 | - | +| `--clean` | 清理 PXE 配置,并可选删除 TFTP 根目录 | - | +| `--status` | 查看当前 PXE 状态 | - | +| `--interface NAME` | 监听的网卡名 | `eno1np0` | +| `--server-ip IP` | 服务端 IP;实际部署时会校正为网卡当前 IPv4 | 自动探测 | +| `--client-ip IP` | 可选的静态客户端 IP,仅在需要为 GRUB 强制指定地址时使用 | 空 | +| `--mode MODE` | DHCP 模式:`proxy`、`exclusive`、`none` | `proxy` | +| `--tftp-root DIR` | TFTP 根目录 | `/home/root/test/x86_64-pc` | +| `--yes` | 清理时跳过确认 | `false` | + +--- + +## 7. 部署产物 + +执行 `--install` 后,脚本会完成以下工作: + +### 7.1 安装依赖 + +自动检查并按需安装: + +- `dnsmasq` +- `iproute2` +- `grub-efi-amd64-bin` +- `ipxe` + +### 7.2 生成系统配置 + +将模板渲染并写入: + +```text +/etc/dnsmasq.d/pxe-physical.conf +``` + +同时确保 `/etc/dnsmasq.conf` 启用了: + +```text +conf-dir=/etc/dnsmasq.d/,*.conf +``` + +### 7.3 准备 TFTP 目录 + +在 `` 下准备: + +- `ipxe-mb.efi` +- `undionly.kpxe`(若系统存在) +- `ipxe.efi`(若系统存在) +- `boot.ipxe` +- `autoexec.ipxe` +- `grubx64.efi` + +默认不会主动复制 `kernel` 到 ``。 + +--- + +## 8. DHCP 模式说明 + +### 8.1 `proxy` 模式 + +```bash +sudo ./runner.sh pxe --install --mode proxy +``` + +特点: + +- 推荐默认模式。 +- 适合网络中已存在 DHCP 服务器的场景。 +- 当前主机只负责 PXE 引导相关响应,不直接分配完整地址池。 + +### 8.2 `exclusive` 模式 + +```bash +sudo ./runner.sh pxe --install --mode exclusive +``` + +特点: + +- 当前主机独占提供 DHCP 服务。 +- 会生成完整地址池配置。 +- 不适合与已有 DHCP 服务器同时工作。 + +### 8.3 `none` 模式 + +```bash +sudo ./runner.sh pxe --install --mode none +``` + +特点: + +- 仅提供 TFTP,不主动提供 DHCP 地址池。 +- 适合由外部 DHCP 统一下发地址和 `next-server` 的场景。 + +--- + +## 9. 启动链说明 + +当前默认启动链为: + +1. UEFI 固件通过 PXE 下载 `ipxe-mb.efi` +2. iPXE 加载 `boot.ipxe`(兼容场景下也可走 `autoexec.ipxe`) +3. `boot.ipxe` 链接到 `grubx64.efi` +4. `grubx64.efi` 优先尝试加载 `(tftp,)/grub.cfg` +5. 如果没有 `grub.cfg`,则尝试加载 `(tftp,)/kernel` +6. 如果两者都不存在,则停留在 GRUB 供人工调试 + +说明: + +- 默认情况下,GRUB 只执行 `net_bootp` 获取地址,不再写死 `net_default_ip` +- 这对 `proxy` 模式更友好,因为 `proxy` 模式下客户端 IP 来自上游 DHCP,部署阶段通常无法预先知道 +- 只有在特殊调试场景下,才建议显式传 `--client-ip` + +--- + +## 10. 目录与模板说明 + +PXE 相关模板位于: + +- [runner.sh](/home/root/github-runners/runner.sh) +- [pxe-physical.conf](/home/root/github-runners/pxe-boot/pxe-physical.conf) +- [autoexec.ipxe](/home/root/github-runners/pxe-boot/autoexec.ipxe) +- [grub-embedded.cfg](/home/root/github-runners/pxe-boot/grub-embedded.cfg) +- [boot.ipxe](/home/root/github-runners/pxe-boot/boot.ipxe) + +其中: + +- 必需的源文件:`pxe-physical.conf`、`boot.ipxe`、`autoexec.ipxe`、`grub-embedded.cfg`、`ipxe-mb.efi` +- 运行期生成文件:`grubx64.efi` +- `pxe-boot/` 中若存在 `grubx64.efi`,它更适合作为备份参考,不是部署时必须依赖的源文件 + +模板中的以下字段会在部署时动态替换: + +- `__INTERFACE__` +- `__SERVER_IP__` +- `__CLIENT_IP__` +- `__DHCP_RANGE_LINE__` +- `__DHCP_HOST_LINE__` +- `__NO_DHCP_INTERFACE_LINE__` +- `__BIOS_BOOT_LINE__` +- `__FALLBACK_EFI_BOOT_LINE__` + +--- + +## 11. 常见问题 + +### 11.1 执行时报错 “Network interface does not exist” + +说明指定的网卡名不存在,可先查看系统网卡: + +```bash +ip -o link show +``` + +然后重新指定: + +```bash +sudo ./runner.sh pxe --install --interface --kernel /path/to/kernel +``` + +### 11.2 执行时报错找不到 `kernel` + +`pxe --install` 本身不再要求 `kernel`。如果后续需要自动引导业务镜像,有两种方式: + +```bash +cp /path/to/kernel /kernel +``` + +或提供: + +```bash +cp /path/to/grub.cfg /grub.cfg +``` + +### 11.3 `dnsmasq` 启动失败 + +先检查配置测试: + +```bash +sudo dnsmasq --test +``` + +再查看服务日志: + +```bash +sudo journalctl -u dnsmasq -n 50 --no-pager +``` + +### 11.4 BIOS 客户端无法启动 + +当前实现会优先从系统中查找 `undionly.kpxe`。如果系统未安装对应文件,部署会继续,但 BIOS 引导能力会被禁用。 + +可重新执行安装以补齐依赖: + +```bash +sudo ./runner.sh pxe --install +``` + +--- + +## 12. 推荐操作流程 + +日常推荐流程: + +```bash +sudo ./runner.sh pxe --install --interface eno1np0 --mode proxy +./runner.sh pxe --status +``` + +如果只替换内核文件,可重新执行安装命令覆盖生成产物;如果只想重启服务,可执行: + +```bash +sudo ./runner.sh pxe --stop +sudo ./runner.sh pxe --start +``` diff --git a/pxe-boot/autoexec.ipxe b/pxe-boot/autoexec.ipxe new file mode 100644 index 0000000..34c1171 --- /dev/null +++ b/pxe-boot/autoexec.ipxe @@ -0,0 +1,6 @@ +#!ipxe +echo =================================== +echo ArceOS Boot via GRUB +echo =================================== +echo Loading GRUB EFI bootloader... +chain tftp://192.168.1.2/grubx64.efi diff --git a/pxe-boot/boot.ipxe b/pxe-boot/boot.ipxe new file mode 100755 index 0000000..0a0a3f8 --- /dev/null +++ b/pxe-boot/boot.ipxe @@ -0,0 +1,6 @@ +#!ipxe +echo =================================== +echo ArceOS Boot via GRUB +echo =================================== +echo Loading GRUB EFI bootloader... +chain tftp://__SERVER_IP__/grubx64.efi diff --git a/pxe-boot/grub-embedded.cfg b/pxe-boot/grub-embedded.cfg new file mode 100644 index 0000000..5adb1f0 --- /dev/null +++ b/pxe-boot/grub-embedded.cfg @@ -0,0 +1,19 @@ +serial --unit=0 --speed=115200 +terminal_input console serial +terminal_output console serial + +net_bootp +__NET_DEFAULT_IP_LINE__ +set net_default_server=__SERVER_IP__ + +if [ -f (tftp,__SERVER_IP__)/grub.cfg ]; then + configfile (tftp,__SERVER_IP__)/grub.cfg +fi + +if [ -f (tftp,__SERVER_IP__)/kernel ]; then + multiboot (tftp,__SERVER_IP__)/kernel + boot +fi + +echo No grub.cfg or kernel found on TFTP server. +echo Entering GRUB shell for manual debugging. diff --git a/pxe-boot/grubx64.efi b/pxe-boot/grubx64.efi new file mode 100644 index 0000000..5f10e7a Binary files /dev/null and b/pxe-boot/grubx64.efi differ diff --git a/pxe-boot/ipxe-mb.efi b/pxe-boot/ipxe-mb.efi new file mode 100644 index 0000000..13b17c2 Binary files /dev/null and b/pxe-boot/ipxe-mb.efi differ diff --git a/pxe-boot/pxe-physical.conf b/pxe-boot/pxe-physical.conf new file mode 100644 index 0000000..8888fb4 --- /dev/null +++ b/pxe-boot/pxe-physical.conf @@ -0,0 +1,47 @@ +# AxVisor PXE Configuration - 独立DHCP模式(直连网络) +# 使用场景:服务器和开发板直连,或独立网络环境 + +# 关闭本地 DNS 功能 +port=0 + +# 监听物理网卡 +interface=__INTERFACE__ +bind-interfaces + +__DHCP_RANGE_LINE__ +__DHCP_HOST_LINE__ +__NO_DHCP_INTERFACE_LINE__ + +# UEFI客户端识别 +dhcp-match=set:efi-x86_64,option:client-arch,7 +dhcp-match=set:efi-x86_64,option:client-arch,9 +dhcp-match=set:bios,option:client-arch,0 + +# PXE启动选项 +# TFTP服务器地址 +dhcp-option=option:tftp-server-address,__SERVER_IP__ + +# 识别 iPXE(二阶段 DHCP) +dhcp-userclass=set:ipxe,iPXE + +# UEFI 客户端使用我们自编译的 ipxe-mb.efi +dhcp-boot=tag:!ipxe,tag:efi-x86_64,ipxe-mb.efi,,__SERVER_IP__ +# Legacy BIOS 客户端使用 undionly.kpxe +__BIOS_BOOT_LINE__ +# 兜底:非 iPXE 且既不是 UEFI x86_64 也不是 BIOS 时才下发 ipxe.efi +__FALLBACK_EFI_BOOT_LINE__ + +# iPXE:下发启动脚本(随后由脚本加载 GRUB) +dhcp-boot=tag:ipxe,boot.ipxe,,__SERVER_IP__ + +# iPXE启动脚本 +pxe-service=x86PC, "iPXE", boot.ipxe + +# TFTP 配置 +enable-tftp +tftp-root=/home/root/test/x86_64-pc +tftp-no-fail + +# 日志 +log-dhcp +log-queries diff --git a/runner.sh b/runner.sh index 989d0b9..dd3c98f 100755 --- a/runner.sh +++ b/runner.sh @@ -5,6 +5,8 @@ set -euo pipefail export COMPOSE_IGNORE_ORPHANS=1 ENV_FILE="${ENV_FILE:-.env}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PXE_BOOT_DIR="${PXE_BOOT_DIR:-${SCRIPT_DIR}/pxe-boot}" # ------------------------------- load .env file ------------------------------- if [[ -f "$ENV_FILE" ]]; then @@ -37,11 +39,13 @@ fi RUNNER_GROUP="${RUNNER_GROUP:-Default}" RUNNER_WORKDIR="${RUNNER_WORKDIR:-}" RUNNER_LABELS="${RUNNER_LABELS:-intel}" -RUNNER_BOARD="2" +RUNNER_BOARD="4" DISABLE_AUTO_UPDATE="${DISABLE_AUTO_UPDATE:-false}" # 板子级:未设置时用本板默认值(同类型板串行、不同类型板并行);多组织共享同一块板时显式设为相同 ID 即可 RUNNER_RESOURCE_ID_PHYTIUMPI="${RUNNER_RESOURCE_ID_PHYTIUMPI:-}" RUNNER_RESOURCE_ID_ROC_RK3568_PC="${RUNNER_RESOURCE_ID_ROC_RK3568_PC:-}" +RUNNER_RESOURCE_ID_X86_64_PC="${RUNNER_RESOURCE_ID_X86_64_PC:-}" +RUNNER_RESOURCE_ID_VISIONFIVE2="${RUNNER_RESOURCE_ID_VISIONFIVE2:-}" RUNNER_LOCK_DIR="${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}" RUNNER_LOCK_HOST_PATH="${RUNNER_LOCK_HOST_PATH:-/tmp/github-runner-locks}" # Compose 文件名:未显式设置时自动拼入 ORG/REPO,避免同一主机多组织时文件冲突 @@ -79,6 +83,20 @@ fi REG_TOKEN_CACHE_TTL="${REG_TOKEN_CACHE_TTL:-300}" # seconds, default 5 minutes # ------------------------------- Helpers ------------------------------- +if [[ -t 1 || -t 2 ]]; then + SHELL_COLOR_RED=$'\033[0;31m' + SHELL_COLOR_GREEN=$'\033[0;32m' + SHELL_COLOR_YELLOW=$'\033[1;33m' + SHELL_COLOR_BLUE=$'\033[0;34m' + SHELL_COLOR_RESET=$'\033[0m' +else + SHELL_COLOR_RED='' + SHELL_COLOR_GREEN='' + SHELL_COLOR_YELLOW='' + SHELL_COLOR_BLUE='' + SHELL_COLOR_RESET='' +fi + shell_usage() { local COLW=48 echo "Usage: ./runner.sh COMMAND [options] Where [options] depend on COMMAND. Available COMMANDs:" @@ -110,7 +128,12 @@ shell_usage() { printf " %-${COLW}s %s\n" "./runner.sh image" "Rebuild Docker image based on Dockerfile" echo - echo "6. Help" + echo "6. PXE deployment commands:" + printf " %-${COLW}s %s\n" "./runner.sh pxe --install [options]" "Deploy PXE service using templates from ./pxe-boot" + printf " %-${COLW}s %s\n" "./runner.sh pxe --status" "Show PXE service status" + echo + + echo "7. Help" printf " %-${COLW}s %s\n" "./runner.sh help" "Show this help" echo @@ -124,6 +147,8 @@ shell_usage() { printf " %-${KEYW}s %s\n" "RUNNER_CUSTOM_IMAGE" "Image tag used for auto-build (can override)" printf " %-${KEYW}s %s\n" "RUNNER_RESOURCE_ID_PHYTIUMPI" "Lock ID for phytiumpi board (default: board-phytiumpi); same ID = serial across runners" printf " %-${KEYW}s %s\n" "RUNNER_RESOURCE_ID_ROC_RK3568_PC" "Lock ID for roc-rk3568-pc board (default: board-roc-rk3568-pc); same ID = serial" + printf " %-${KEYW}s %s\n" "RUNNER_RESOURCE_ID_X86_64_PC" "Lock ID for x86_64-pc board (default: board-x86_64-pc); same ID = serial" + printf " %-${KEYW}s %s\n" "RUNNER_RESOURCE_ID_VISIONFIVE2" "Lock ID for visionfive2 board (default: board-visionfive2); same ID = serial" printf " %-${KEYW}s %s\n" "RUNNER_LOCK_DIR" "Lock dir in container (default /tmp/github-runner-locks)" printf " %-${KEYW}s %s\n" "RUNNER_LOCK_HOST_PATH" "Lock dir on host for bind mount (default /tmp/github-runner-locks)" echo @@ -135,9 +160,9 @@ shell_usage() { echo "- Re-start/up will reuse existing volumes; Runner configuration and tool caches will not be lost." } -shell_die() { echo "[ERROR] $*" >&2; exit 1; } -shell_info() { echo "[INFO] $*"; } -shell_warn() { echo "[WARN] $*" >&2; } +shell_die() { printf '%s[ERROR]%s %s\n' "${SHELL_COLOR_RED}" "${SHELL_COLOR_RESET}" "$*" >&2; exit 1; } +shell_info() { printf '%s[INFO]%s %s\n' "${SHELL_COLOR_BLUE}" "${SHELL_COLOR_RESET}" "$*"; } +shell_warn() { printf '%s[WARN]%s %s\n' "${SHELL_COLOR_YELLOW}" "${SHELL_COLOR_RESET}" "$*" >&2; } shell_prompt_confirm() { # Return 0 for confirm, 1 for cancel @@ -146,6 +171,474 @@ shell_prompt_confirm() { [[ "$ans" == "y" || "$ans" == "Y" || "$ans" == "yes" || "$ans" == "YES" ]] } +shell_escape_sed_replacement() { + local value="${1:-}" + value="${value//\\/\\\\}" + value="${value//&/\\&}" + printf '%s' "$value" +} + +pxe_usage() { + cat <<'EOF' +Usage: ./runner.sh pxe [options] + +Options: + -i, --install Install and configure PXE environment + -s, --start Start dnsmasq PXE service + -t, --stop Stop dnsmasq PXE service + -c, --clean Clean PXE configuration and optionally TFTP root + --status Show PXE service status + -h, --help Show this help + --yes Skip confirmation when used with --clean + --interface NAME Network interface to listen on (default: eno1np0) + --server-ip IP PXE server IP (default: detected from interface) + --client-ip IP Optional static client IP embedded into GRUB config (default: empty) + --mode MODE DHCP mode: proxy | exclusive | none (default: proxy) + --tftp-root DIR TFTP root directory (default: /home/root/test/x86_64-pc) + +Examples: + ./runner.sh pxe --install + ./runner.sh pxe --install --mode proxy + ./runner.sh pxe --status +EOF +} + +pxe_ipv4_to_int() { + local ip="$1" + local a b c d + IFS=. read -r a b c d <<< "$ip" + echo $(( (a << 24) | (b << 16) | (c << 8) | d )) +} + +pxe_int_to_ipv4() { + local value="$1" + printf '%d.%d.%d.%d\n' \ + $(( (value >> 24) & 255 )) \ + $(( (value >> 16) & 255 )) \ + $(( (value >> 8) & 255 )) \ + $(( value & 255 )) +} + +pxe_prefix_to_mask_int() { + local prefix="$1" + if [[ "$prefix" -eq 0 ]]; then + echo 0 + else + echo $(( (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF )) + fi +} + +pxe_prefix_to_netmask() { + local prefix="$1" + pxe_int_to_ipv4 "$(pxe_prefix_to_mask_int "$prefix")" +} + +pxe_require_root() { + [[ "$EUID" -eq 0 ]] || shell_die "PXE deployment requires root privileges. Please run with sudo or as root." +} + +pxe_require_file() { + local path="$1" + [[ -f "$path" ]] || shell_die "Required file not found: $path" +} + +pxe_require_command() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || shell_die "Required command not found: $cmd" +} + +pxe_parse_args() { + PXE_ACTION="" + PXE_INTERFACE="${PXE_INTERFACE:-eno1np0}" + PXE_SERVER_IP="${PXE_SERVER_IP:-}" + PXE_CLIENT_IP="${PXE_CLIENT_IP:-}" + PXE_DHCP_MODE="${PXE_DHCP_MODE:-proxy}" + PXE_TFTP_ROOT="${PXE_TFTP_ROOT:-/home/root/test/x86_64-pc}" + PXE_KERNEL_FILE="${PXE_KERNEL_FILE:-}" + PXE_ASSUME_YES=0 + + while [[ $# -gt 0 ]]; do + case "$1" in + -i|--install) PXE_ACTION="install"; shift ;; + -s|--start) PXE_ACTION="start"; shift ;; + -t|--stop) PXE_ACTION="stop"; shift ;; + -c|--clean) PXE_ACTION="clean"; shift ;; + --status) PXE_ACTION="status"; shift ;; + --interface) + [[ $# -ge 2 ]] || shell_die "Missing value for --interface" + PXE_INTERFACE="$2" + shift 2 + ;; + --server-ip) + [[ $# -ge 2 ]] || shell_die "Missing value for --server-ip" + PXE_SERVER_IP="$2" + shift 2 + ;; + --client-ip) + [[ $# -ge 2 ]] || shell_die "Missing value for --client-ip" + PXE_CLIENT_IP="$2" + shift 2 + ;; + --kernel) + [[ $# -ge 2 ]] || shell_die "Missing value for --kernel" + PXE_KERNEL_FILE="$2" + shift 2 + ;; + --mode) + [[ $# -ge 2 ]] || shell_die "Missing value for --mode" + PXE_DHCP_MODE="$2" + shift 2 + ;; + --tftp-root) + [[ $# -ge 2 ]] || shell_die "Missing value for --tftp-root" + PXE_TFTP_ROOT="$2" + shift 2 + ;; + --yes|-y) PXE_ASSUME_YES=1; shift ;; + -h|--help) PXE_ACTION="help"; shift ;; + *) shell_die "Unknown pxe option: $1" ;; + esac + done + + [[ -n "$PXE_ACTION" ]] || PXE_ACTION="help" +} + +pxe_validate_mode() { + case "$PXE_DHCP_MODE" in + proxy|exclusive|none) ;; + *) shell_die "Unsupported PXE mode: ${PXE_DHCP_MODE}. Supported modes: proxy, exclusive, none." ;; + esac +} + +pxe_check_network_interface() { + pxe_require_command ip + + if ! ip link show "$PXE_INTERFACE" >/dev/null 2>&1; then + shell_warn "Available network interfaces:" + ip -o link show | awk -F': ' '{print $2}' >&2 + shell_die "Network interface does not exist: $PXE_INTERFACE" + fi + + local actual_cidr actual_ip prefix mask_int network_int + actual_cidr="$(ip -o -4 addr show dev "$PXE_INTERFACE" | awk '{print $4}' | head -n1)" + actual_ip="${actual_cidr%%/*}" + prefix="${actual_cidr##*/}" + [[ -n "$actual_ip" && -n "$prefix" ]] || shell_die "Network interface ${PXE_INTERFACE} does not have an IPv4 address." + + if [[ -n "$PXE_SERVER_IP" && "$PXE_SERVER_IP" != "$actual_ip" ]]; then + shell_warn "Interface ${PXE_INTERFACE} currently uses ${actual_ip}; overriding requested server IP ${PXE_SERVER_IP}." + fi + PXE_SERVER_IP="$actual_ip" + PXE_SERVER_PREFIX="$prefix" + mask_int="$(pxe_prefix_to_mask_int "$prefix")" + network_int=$(( $(pxe_ipv4_to_int "$actual_ip") & mask_int )) + PXE_SERVER_NETMASK="$(pxe_prefix_to_netmask "$prefix")" + PXE_SERVER_NETWORK="$(pxe_int_to_ipv4 "$network_int")" +} + +pxe_find_artifact() { + local candidate + for candidate in "$@"; do + [[ -n "$candidate" && -f "$candidate" ]] || continue + printf '%s\n' "$candidate" + return 0 + done + return 1 +} + +pxe_install_packages() { + local pkgs=() + + pxe_require_command apt-get + + command -v ip >/dev/null 2>&1 || pkgs+=(iproute2) + command -v dnsmasq >/dev/null 2>&1 || pkgs+=(dnsmasq) + command -v grub-mkimage >/dev/null 2>&1 || pkgs+=(grub-efi-amd64-bin) + + if ! pxe_find_artifact \ + /usr/lib/ipxe/undionly.kpxe \ + /usr/lib/ipxe/ipxe.efi \ + /usr/lib/ipxe/snponly.efi \ + /usr/lib/ipxe/snp.efi >/dev/null 2>&1; then + pkgs+=(ipxe) + fi + + if [[ ${#pkgs[@]} -gt 0 ]]; then + shell_info "Installing PXE dependencies: ${pkgs[*]}" + apt-get update -qq + apt-get install -y "${pkgs[@]}" + fi +} + +pxe_ensure_dnsmasq_conf_dir() { + local dnsmasq_conf="/etc/dnsmasq.conf" + pxe_require_file "$dnsmasq_conf" + + if ! grep -Eq '^[[:space:]]*conf-dir=/etc/dnsmasq\.d/?' "$dnsmasq_conf"; then + printf '\nconf-dir=/etc/dnsmasq.d/,*.conf\n' >> "$dnsmasq_conf" + fi +} + +pxe_render_template() { + local src="$1" + local dest="$2" + shift 2 + + pxe_require_file "$src" + + local sed_args=() + local kv key value escaped + for kv in "$@"; do + key="${kv%%=*}" + value="${kv#*=}" + escaped="$(shell_escape_sed_replacement "$value")" + sed_args+=(-e "s|__${key}__|${escaped}|g") + done + + sed "${sed_args[@]}" "$src" > "$dest" +} + +pxe_prepare_tftp_directory() { + install -d -m 755 "$PXE_TFTP_ROOT" + install -m 0644 "${PXE_BOOT_DIR}/ipxe-mb.efi" "${PXE_TFTP_ROOT}/ipxe-mb.efi" + + local bios_source fallback_efi_source + bios_source="$(pxe_find_artifact /usr/lib/ipxe/undionly.kpxe)" || bios_source="" + fallback_efi_source="$(pxe_find_artifact /usr/lib/ipxe/ipxe.efi /usr/lib/ipxe/snponly.efi /usr/lib/ipxe/snp.efi)" || fallback_efi_source="" + + if [[ -n "$bios_source" ]]; then + install -m 0644 "$bios_source" "${PXE_TFTP_ROOT}/undionly.kpxe" + PXE_BIOS_BOOT_LINE="dhcp-boot=tag:!ipxe,tag:bios,undionly.kpxe,,${PXE_SERVER_IP}" + else + PXE_BIOS_BOOT_LINE="# BIOS support disabled: undionly.kpxe not found" + shell_warn "undionly.kpxe was not found; legacy BIOS PXE clients will be unsupported." + fi + + if [[ -n "$fallback_efi_source" ]]; then + install -m 0644 "$fallback_efi_source" "${PXE_TFTP_ROOT}/ipxe.efi" + PXE_FALLBACK_EFI_BOOT_LINE="dhcp-boot=tag:!ipxe,tag:!efi-x86_64,tag:!bios,ipxe.efi,,${PXE_SERVER_IP}" + else + PXE_FALLBACK_EFI_BOOT_LINE="# Fallback EFI support disabled: ipxe.efi not found" + shell_warn "No fallback EFI iPXE binary was found; non-x86_64 EFI clients will be unsupported." + fi +} + +pxe_render_dnsmasq_conf() { + local dhcp_range_line dhcp_host_line no_dhcp_interface_line + + case "$PXE_DHCP_MODE" in + proxy) + dhcp_range_line="dhcp-range=${PXE_SERVER_NETWORK},proxy,${PXE_SERVER_NETMASK}" + dhcp_host_line="# dhcp-host disabled in proxy mode" + no_dhcp_interface_line="# full DHCP enabled in proxy mode" + ;; + exclusive) + local network_int range_start range_end + network_int="$(pxe_ipv4_to_int "$PXE_SERVER_NETWORK")" + range_start="$(pxe_int_to_ipv4 $(( network_int + 100 )))" + range_end="$(pxe_int_to_ipv4 $(( network_int + 200 )))" + dhcp_range_line="dhcp-range=${range_start},${range_end},${PXE_SERVER_NETMASK},12h" + dhcp_host_line="dhcp-host=88:88:88:88:87:88,${PXE_CLIENT_IP},infinite" + no_dhcp_interface_line="# full DHCP enabled in exclusive mode" + ;; + none) + dhcp_range_line="# DHCP range disabled in TFTP-only mode" + dhcp_host_line="# dhcp-host disabled in TFTP-only mode" + no_dhcp_interface_line="no-dhcp-interface=${PXE_INTERFACE}" + ;; + esac + + install -d -m 755 /etc/dnsmasq.d + pxe_render_template \ + "${PXE_BOOT_DIR}/pxe-physical.conf" \ + "/etc/dnsmasq.d/pxe-physical.conf" \ + "INTERFACE=${PXE_INTERFACE}" \ + "SERVER_IP=${PXE_SERVER_IP}" \ + "CLIENT_IP=${PXE_CLIENT_IP}" \ + "TFTP_ROOT=${PXE_TFTP_ROOT}" \ + "DHCP_RANGE_LINE=${dhcp_range_line}" \ + "DHCP_HOST_LINE=${dhcp_host_line}" \ + "NO_DHCP_INTERFACE_LINE=${no_dhcp_interface_line}" \ + "BIOS_BOOT_LINE=${PXE_BIOS_BOOT_LINE}" \ + "FALLBACK_EFI_BOOT_LINE=${PXE_FALLBACK_EFI_BOOT_LINE}" +} + +pxe_render_boot_assets() { + local grub_cfg boot_ipxe autoexec_ipxe + grub_cfg="$(mktemp /tmp/grub-embedded.XXXXXX.cfg)" + boot_ipxe="$(mktemp /tmp/boot.XXXXXX.ipxe)" + autoexec_ipxe="$(mktemp /tmp/autoexec.XXXXXX.ipxe)" + + local net_default_ip_line + if [[ -n "${PXE_CLIENT_IP:-}" ]]; then + net_default_ip_line="set net_default_ip=${PXE_CLIENT_IP}" + else + net_default_ip_line="# set net_default_ip is intentionally omitted" + fi + + pxe_render_template \ + "${PXE_BOOT_DIR}/grub-embedded.cfg" \ + "$grub_cfg" \ + "NET_DEFAULT_IP_LINE=${net_default_ip_line}" \ + "SERVER_IP=${PXE_SERVER_IP}" + + grub-mkimage -o "${PXE_TFTP_ROOT}/grubx64.efi" -O x86_64-efi \ + -p "" \ + -c "$grub_cfg" \ + normal configfile tftp net boot multiboot multiboot2 \ + efinet linux linux16 serial terminal \ + echo cat ls test + + pxe_render_template \ + "${PXE_BOOT_DIR}/boot.ipxe" \ + "$boot_ipxe" \ + "SERVER_IP=${PXE_SERVER_IP}" + + if [[ -f "${PXE_BOOT_DIR}/autoexec.ipxe" ]]; then + pxe_render_template \ + "${PXE_BOOT_DIR}/autoexec.ipxe" \ + "$autoexec_ipxe" \ + "SERVER_IP=${PXE_SERVER_IP}" + else + cp "$boot_ipxe" "$autoexec_ipxe" + fi + + install -m 0644 "$boot_ipxe" "${PXE_TFTP_ROOT}/boot.ipxe" + install -m 0644 "$autoexec_ipxe" "${PXE_TFTP_ROOT}/autoexec.ipxe" + rm -f "$grub_cfg" "$boot_ipxe" "$autoexec_ipxe" +} + +pxe_stop_conflicting_services() { + if command -v systemctl >/dev/null 2>&1; then + systemctl stop dnsmasq 2>/dev/null || true + systemctl stop tftpd-hpa 2>/dev/null || true + systemctl disable tftpd-hpa 2>/dev/null || true + fi +} + +pxe_start_service() { + pxe_require_command dnsmasq + pxe_require_command systemctl + + if ! dnsmasq --test >/dev/null 2>&1; then + dnsmasq --test || true + shell_die "dnsmasq configuration test failed." + fi + + systemctl enable dnsmasq >/dev/null 2>&1 || true + systemctl restart dnsmasq + systemctl is-active --quiet dnsmasq || shell_die "dnsmasq failed to start." +} + +pxe_stop_service() { + pxe_require_command systemctl + systemctl stop dnsmasq 2>/dev/null || true +} + +pxe_print_port_status() { + if command -v ss >/dev/null 2>&1; then + ss -lun | awk 'NR==1 || /:(67|69|4011)[[:space:]]/' + fi +} + +pxe_show_status() { + pxe_check_network_interface + + echo "PXE status" + echo "==========" + echo "Interface : ${PXE_INTERFACE}" + echo "Server IP : ${PXE_SERVER_IP}" + echo "Subnet : ${PXE_SERVER_NETWORK}/${PXE_SERVER_PREFIX}" + echo "TFTP root : ${PXE_TFTP_ROOT}" + echo + + if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet dnsmasq; then + echo "dnsmasq : running" + else + echo "dnsmasq : stopped" + fi + + if [[ -f /etc/dnsmasq.d/pxe-physical.conf ]]; then + echo "Config : /etc/dnsmasq.d/pxe-physical.conf" + else + echo "Config : missing" + fi + + for file in ipxe-mb.efi undionly.kpxe ipxe.efi boot.ipxe autoexec.ipxe grubx64.efi grub.cfg kernel; do + if [[ -f "${PXE_TFTP_ROOT}/${file}" ]]; then + printf 'File : %s\n' "$file" + fi + done + + echo + pxe_print_port_status || true +} + +pxe_clean_environment() { + pxe_stop_service + rm -f /etc/dnsmasq.d/pxe-physical.conf + + if [[ -d "$PXE_TFTP_ROOT" ]]; then + if [[ "$PXE_ASSUME_YES" -eq 1 ]] || shell_prompt_confirm "Delete TFTP root ${PXE_TFTP_ROOT}? [y/N] "; then + rm -rf "$PXE_TFTP_ROOT" + shell_info "Removed TFTP root: ${PXE_TFTP_ROOT}" + else + shell_info "Kept TFTP root: ${PXE_TFTP_ROOT}" + fi + fi +} + +pxe_install() { + pxe_require_root + pxe_validate_mode + pxe_require_file "${PXE_BOOT_DIR}/pxe-physical.conf" + pxe_require_file "${PXE_BOOT_DIR}/grub-embedded.cfg" + pxe_require_file "${PXE_BOOT_DIR}/boot.ipxe" + pxe_require_file "${PXE_BOOT_DIR}/autoexec.ipxe" + pxe_require_file "${PXE_BOOT_DIR}/ipxe-mb.efi" + + pxe_install_packages + pxe_check_network_interface + if [[ -n "${PXE_KERNEL_FILE:-}" ]]; then + shell_warn "--kernel is ignored by pxe now; deploy only prepares PXE service and bootloader files." + fi + pxe_stop_conflicting_services + pxe_ensure_dnsmasq_conf_dir + pxe_prepare_tftp_directory + pxe_render_dnsmasq_conf + pxe_render_boot_assets + pxe_start_service + + shell_info "Interface: ${PXE_INTERFACE}" + shell_info "Server IP: ${PXE_SERVER_IP}" + shell_info "Subnet : ${PXE_SERVER_NETWORK}/${PXE_SERVER_PREFIX}" + shell_info "TFTP root: ${PXE_TFTP_ROOT}" + shell_info "PXE deployment completed." +} + +pxe_main() { + pxe_parse_args "$@" + + case "$PXE_ACTION" in + help) pxe_usage ;; + install) pxe_install ;; + start) + pxe_require_root + pxe_check_network_interface + pxe_start_service + ;; + stop) + pxe_require_root + pxe_stop_service + ;; + clean) + pxe_require_root + pxe_clean_environment + ;; + status) pxe_show_status ;; + esac +} + shell_get_org_and_pat() { # Fast path when both provided via env/.env if [[ -n "${ORG:-}" && -n "${GH_PAT:-}" ]]; then @@ -492,9 +985,13 @@ shell_generate_compose_file() { # 第一步:为两种板子 runner 类型定义资源 ID # ════════════════════════════════════════════════════════════════ # 硬件板 phytiumpi - 总是启用文件锁 - local res_phytiumpi="${RUNNER_RESOURCE_ID_PHYTIUMPI:-}" + local res_phytiumpi="${RUNNER_RESOURCE_ID_PHYTIUMPI:-board-phytiumpi}" # 硬件板 roc - 总是启用文件锁 - local res_roc="${RUNNER_RESOURCE_ID_ROC_RK3568_PC:-}" + local res_roc="${RUNNER_RESOURCE_ID_ROC_RK3568_PC:-board-roc-rk3568-pc}" + # 硬件板 x86_64 - 总是启用文件锁 + local res_x86_64="${RUNNER_RESOURCE_ID_X86_64_PC:-board-x86_64-pc}" + # 硬件板 visionfive2 - 总是启用文件锁 + local res_visionfive2="${RUNNER_RESOURCE_ID_VISIONFIVE2:-board-visionfive2}" # ════════════════════════════════════════════════════════════════ # 第二步:两种板子 runner 类型的 entrypoint 配置 @@ -504,9 +1001,13 @@ shell_generate_compose_file() { # 普通 runner 始终使用 /home/runner/run.sh(不经过 runner-wrapper) local runner_entrypoint_phytiumpi="/home/runner/run.sh" local runner_entrypoint_roc="/home/runner/run.sh" + local runner_entrypoint_x86_64="/home/runner/run.sh" + local runner_entrypoint_visionfive2="/home/runner/run.sh" # 若设置了资源 ID,则改用 runner-wrapper 来处理文件锁 [[ -n "$res_phytiumpi" ]] && runner_entrypoint_phytiumpi="/home/runner/runner-wrapper/runner-wrapper.sh" [[ -n "$res_roc" ]] && runner_entrypoint_roc="/home/runner/runner-wrapper/runner-wrapper.sh" + [[ -n "$res_x86_64" ]] && runner_entrypoint_x86_64="/home/runner/runner-wrapper/runner-wrapper.sh" + [[ -n "$res_visionfive2" ]] && runner_entrypoint_visionfive2="/home/runner/runner-wrapper/runner-wrapper.sh" # ════════════════════════════════════════════════════════════════ # 第三步:为两种板子 runner 类型准备额外的环境变量数组 @@ -520,10 +1021,14 @@ shell_generate_compose_file() { # 原因:两种板子 runner 都可能需要文件锁机制 local extra_env_phytiumpi=() local extra_env_roc=() + local extra_env_x86_64=() + local extra_env_visionfive2=() local extra_proxy_env=() # 只有设置了相应的资源 ID,才为该类型 runner 添加锁相关环境变量 [[ -n "$res_phytiumpi" ]] && extra_env_phytiumpi=(" RUNNER_RESOURCE_ID: \"$res_phytiumpi\"" " RUNNER_SCRIPT: \"/home/runner/run.sh\"" " RUNNER_LOCK_DIR: \"${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}\"") [[ -n "$res_roc" ]] && extra_env_roc=(" RUNNER_RESOURCE_ID: \"$res_roc\"" " RUNNER_SCRIPT: \"/home/runner/run.sh\"" " RUNNER_LOCK_DIR: \"${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}\"") + [[ -n "$res_x86_64" ]] && extra_env_x86_64=(" RUNNER_RESOURCE_ID: \"$res_x86_64\"" " RUNNER_SCRIPT: \"/home/runner/run.sh\"" " RUNNER_LOCK_DIR: \"${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}\"") + [[ -n "$res_visionfive2" ]] && extra_env_visionfive2=(" RUNNER_RESOURCE_ID: \"$res_visionfive2\"" " RUNNER_SCRIPT: \"/home/runner/run.sh\"" " RUNNER_LOCK_DIR: \"${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}\"") [[ -n "${HTTP_PROXY:-}" ]] && extra_proxy_env+=(" HTTP_PROXY: \"${HTTP_PROXY}\"") [[ -n "${HTTPS_PROXY:-}" ]] && extra_proxy_env+=(" HTTPS_PROXY: \"${HTTPS_PROXY}\"") [[ -n "${NO_PROXY:-}" ]] && extra_proxy_env+=(" NO_PROXY: \"${NO_PROXY}\"") @@ -536,9 +1041,13 @@ shell_generate_compose_file() { # 原因:文件锁机制需要在主机和容器间共享锁文件 local extra_vol_phytiumpi="" local extra_vol_roc="" + local extra_vol_x86_64="" + local extra_vol_visionfive2="" # 只有设置了相应的资源 ID,才为该类型 runner 挂载锁文件目录 [[ -n "$res_phytiumpi" ]] && extra_vol_phytiumpi=" - ${RUNNER_LOCK_HOST_PATH:-/tmp/github-runner-locks}:${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}" [[ -n "$res_roc" ]] && extra_vol_roc=" - ${RUNNER_LOCK_HOST_PATH:-/tmp/github-runner-locks}:${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}" + [[ -n "$res_x86_64" ]] && extra_vol_x86_64=" - ${RUNNER_LOCK_HOST_PATH:-/tmp/github-runner-locks}:${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}" + [[ -n "$res_visionfive2" ]] && extra_vol_visionfive2=" - ${RUNNER_LOCK_HOST_PATH:-/tmp/github-runner-locks}:${RUNNER_LOCK_DIR:-/tmp/github-runner-locks}" # 使用 printf 输出文件头 printf '%s\n' \ @@ -703,6 +1212,97 @@ shell_generate_compose_file() { " - ${RUNNER_NAME_PREFIX}runner-roc-rk3568-pc-data:/home/runner" \ " - ${RUNNER_NAME_PREFIX}runner-roc-rk3568-pc-udev-rules:/etc/udev/rules.d" \ "" >> "${COMPOSE_FILE}" + + # x86_64 板子配置 + printf '%s\n' \ + " ${RUNNER_NAME_PREFIX}runner-x86_64-pc:" \ + " <<: *runner_base" \ + " container_name: \"${RUNNER_NAME_PREFIX}runner-x86_64-pc\"" \ + " command: [\"${runner_entrypoint_x86_64}\"]" \ + " devices:" \ + " - /dev/loop-control:/dev/loop-control" \ + " - /dev/loop0:/dev/loop0" \ + " - /dev/loop1:/dev/loop1" \ + " - /dev/loop2:/dev/loop2" \ + " - /dev/loop3:/dev/loop3" \ + " - /dev/kvm:/dev/kvm" \ + " - /dev/ttyUSB4:/dev/ttyUSB4" \ + " - /dev/ttyUSB5:/dev/ttyUSB5" \ + " group_add:" \ + " - 993" \ + " - dialout" \ + " environment:" \ + " <<: *runner_env" \ + " RUNNER_NAME: \"${RUNNER_NAME_PREFIX}runner-x86_64-pc\"" \ + " RUNNER_LABELS: \"x86_64-pc\"" \ + " BOARD_POWER_ON: \"mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB4 1\"" \ + " BOARD_POWER_OFF: \"mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB4 0\"" \ + " BOARD_POWER_RESET: \"mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB4 0 && sleep 2 && mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB4 1\"" \ + " BOARD_COMM_UART_DEV: \"/dev/ttyUSB5\"" \ + " BOARD_COMM_UART_BAUD: \"115200\"" \ + " BIN_DIR: \"/home/$(whoami)/test/x86_64-pc\"" \ + "${extra_env_x86_64[@]}" \ + " volumes:" \ + " - /home/$(whoami)/test/x86_64-pc:/home/runner/tftp" \ + "$extra_vol_x86_64" \ + " - ./runner-wrapper:/home/runner/runner-wrapper:ro" \ + " - ${RUNNER_NAME_PREFIX}runner-x86_64-pc-data:/home/runner" \ + " - ${RUNNER_NAME_PREFIX}runner-x86_64-pc-udev-rules:/etc/udev/rules.d" \ + "" >> "${COMPOSE_FILE}" + + # visionfive2 板子配置 + printf '%s\n' \ + " ${RUNNER_NAME_PREFIX}runner-visionfive2:" \ + " <<: *runner_base" \ + " container_name: \"${RUNNER_NAME_PREFIX}runner-visionfive2\"" \ + " command:" \ + " - /bin/bash" \ + " - -c" \ + " - |" \ + " set -e" \ + " mkdir -p /home/runner/board" \ + " cd /home/runner/board" \ + " echo \"Attempting to download visionfive2 files...\"" \ + " if curl -fsSL --connect-timeout 30 --max-time 300 https://github.com/user-attachments/files/26230373/visionfive2.tar.gz -o visionfive2.tar.gz; then" \ + " echo \"Download successful, extracting...\"" \ + " tar -xzf visionfive2.tar.gz" \ + " echo \"Extraction completed\"" \ + " else" \ + " echo \"Download failed, continuing with existing files if any...\"" \ + " fi" \ + " ${runner_entrypoint_visionfive2}" \ + " devices:" \ + " - /dev/loop-control:/dev/loop-control" \ + " - /dev/loop0:/dev/loop0" \ + " - /dev/loop1:/dev/loop1" \ + " - /dev/loop2:/dev/loop2" \ + " - /dev/loop3:/dev/loop3" \ + " - /dev/kvm:/dev/kvm" \ + " - /dev/ttyUSB6:/dev/ttyUSB6" \ + " - /dev/ttyUSB7:/dev/ttyUSB7" \ + " group_add:" \ + " - 993" \ + " - dialout" \ + " environment:" \ + " <<: *runner_env" \ + " RUNNER_NAME: \"${RUNNER_NAME_PREFIX}runner-visionfive2\"" \ + " RUNNER_LABELS: \"visionfive2\"" \ + " BOARD_POWER_ON: \"mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB7 1\"" \ + " BOARD_POWER_OFF: \"mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB7 0\"" \ + " BOARD_POWER_RESET: \"mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB7 0 && sleep 2 && mbpoll -m rtu -a 1 -r 1 -t 0 -b 38400 -P none -v /dev/ttyUSB7 1\"" \ + " BOARD_DTB: \"/home/runner/board/jh7110-visionfive-v2.dtb\"" \ + " BOARD_COMM_UART_DEV: \"/dev/ttyUSB6\"" \ + " BOARD_COMM_UART_BAUD: \"115200\"" \ + " TFTP_DIR: \"visionfive2\"" \ + " BIN_DIR: \"/home/runner/test/visionfive2\"" \ + "${extra_env_visionfive2[@]}" \ + " volumes:" \ + " - /home/$(whoami)/test/visionfive2:/home/runner/tftp" \ + "$extra_vol_visionfive2" \ + " - ./runner-wrapper:/home/runner/runner-wrapper:ro" \ + " - ${RUNNER_NAME_PREFIX}runner-visionfive2-data:/home/runner" \ + " - ${RUNNER_NAME_PREFIX}runner-visionfive2-udev-rules:/etc/udev/rules.d" \ + "" >> "${COMPOSE_FILE}" fi # 生成 volumes @@ -731,6 +1331,20 @@ shell_generate_compose_file() { " name: ${RUNNER_NAME_PREFIX}runner-roc-rk3568-pc-data" \ " ${RUNNER_NAME_PREFIX}runner-roc-rk3568-pc-udev-rules:" \ " name: ${RUNNER_NAME_PREFIX}runner-roc-rk3568-pc-udev-rules" >> "${COMPOSE_FILE}" + + # 为 x86_64 板子生成 volumes + printf '%s\n' \ + " ${RUNNER_NAME_PREFIX}runner-x86_64-pc-data:" \ + " name: ${RUNNER_NAME_PREFIX}runner-x86_64-pc-data" \ + " ${RUNNER_NAME_PREFIX}runner-x86_64-pc-udev-rules:" \ + " name: ${RUNNER_NAME_PREFIX}runner-x86_64-pc-udev-rules" >> "${COMPOSE_FILE}" + + # 为 visionfive2 板子生成 volumes + printf '%s\n' \ + " ${RUNNER_NAME_PREFIX}runner-visionfive2-data:" \ + " name: ${RUNNER_NAME_PREFIX}runner-visionfive2-data" \ + " ${RUNNER_NAME_PREFIX}runner-visionfive2-udev-rules:" \ + " name: ${RUNNER_NAME_PREFIX}runner-visionfive2-udev-rules" >> "${COMPOSE_FILE}" fi } @@ -937,7 +1551,6 @@ docker_runner_register() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - DC=$(docker_pick_compose) CMD="${1:-help}"; shift || true case "$CMD" in # ./runner.sh help|-h|--help @@ -945,8 +1558,14 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then shell_usage ;; + # ./runner.sh pxe ... + pxe) + pxe_main "$@" + ;; + # ./runner.sh ps|ls|list|status ps|ls|list|status) + DC=$(docker_pick_compose) shell_get_org_and_pat echo "--------------------------------- Containers -----------------------------------------" docker_print_existing_containers_status @@ -968,6 +1587,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh init -n|--count N init) + DC=$(docker_pick_compose) count=0 if [[ "${1:-}" == "-n" || "${1:-}" == "--count" ]]; then shift @@ -995,6 +1615,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh compose compose) + DC=$(docker_pick_compose) cont_count=0 cont_list="$(docker_list_existing_containers)" if [[ -n "$cont_list" ]]; then cont_count=$(echo "$cont_list" | wc -l | tr -d ' '); fi @@ -1020,6 +1641,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh register [${RUNNER_NAME_PREFIX}runner- ...] register) + DC=$(docker_pick_compose) REG_TOKEN="$(shell_get_reg_token)" shell_update_compose_file "RUNNER_TOKEN" "$REG_TOKEN" @@ -1034,6 +1656,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh start [${RUNNER_NAME_PREFIX}runner- ...] start) + DC=$(docker_pick_compose) if [[ $# -ge 1 ]]; then ids=() for s in "$@"; do @@ -1064,6 +1687,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh stop [${RUNNER_NAME_PREFIX}runner- ...] stop) + DC=$(docker_pick_compose) if [[ $# -ge 1 ]]; then ids=() for s in "$@"; do @@ -1094,6 +1718,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh restart [${RUNNER_NAME_PREFIX}runner- ...] restart) + DC=$(docker_pick_compose) if [[ $# -ge 1 ]]; then ids=() for s in "$@"; do @@ -1124,6 +1749,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh log ${RUNNER_NAME_PREFIX}runner- log) + DC=$(docker_pick_compose) [[ $# -eq 1 ]] || shell_die "Usage: ./runner.sh logs ${RUNNER_NAME_PREFIX}runner-" docker_container_exists "$1" || shell_die "Container $1 not found" @@ -1138,6 +1764,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh rm|remove|delete [${RUNNER_NAME_PREFIX}runner- ...] [-y|--yes] rm|remove|delete) + DC=$(docker_pick_compose) shell_get_org_and_pat if [[ "$#" -eq 0 || "$1" == "-y" || "$1" == "--yes" ]]; then if [[ "$#" -ge 1 ]]; then @@ -1186,6 +1813,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh purge [-y|--yes] purge) + DC=$(docker_pick_compose) REG_TOKEN="$(shell_get_reg_token)" if [[ "${1:-}" == "-y" || "${1:-}" == "--yes" ]]; then shell_delete_all_execute "" @@ -1207,6 +1835,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # ./runner.sh image image) + DC=$(docker_pick_compose) if [[ ! -f Dockerfile ]]; then shell_die "Dockerfile not found in current directory!" fi diff --git a/verify-changes.sh b/verify-changes.sh index 63a8297..4be299a 100755 --- a/verify-changes.sh +++ b/verify-changes.sh @@ -7,14 +7,15 @@ cd "$(dirname "$0")" echo "========== 1. 验证板子锁 ID(不回退到 RUNNER_RESOURCE_ID)==========" export RUNNER_RESOURCE_ID=global-should-not-be-used -unset RUNNER_RESOURCE_ID_PHYTIUMPI RUNNER_RESOURCE_ID_ROC_RK3568_PC +unset RUNNER_RESOURCE_ID_PHYTIUMPI RUNNER_RESOURCE_ID_ROC_RK3568_PC RUNNER_RESOURCE_ID_X86_64 # 加载 .env 会覆盖,所以这里直接按 runner.sh 逻辑算板子默认 res_phytiumpi="${RUNNER_RESOURCE_ID_PHYTIUMPI:-board-phytiumpi}" res_roc="${RUNNER_RESOURCE_ID_ROC_RK3568_PC:-board-roc-rk3568-pc}" -if [[ "$res_phytiumpi" == "board-phytiumpi" && "$res_roc" == "board-roc-rk3568-pc" ]]; then +res_x86_64="${RUNNER_RESOURCE_ID_X86_64_PC:-board-x86_64-pc}" +if [[ "$res_phytiumpi" == "board-phytiumpi" && "$res_roc" == "board-roc-rk3568-pc" && "$res_x86_64" == "board-x86_64-pc" ]]; then echo " [OK] 未设板子变量时使用 per-board 默认,未使用 global RUNNER_RESOURCE_ID" else - echo " [FAIL] res_phytiumpi=$res_phytiumpi res_roc=$res_roc" + echo " [FAIL] res_phytiumpi=$res_phytiumpi res_roc=$res_roc res_x86_64=$res_x86_64" exit 1 fi