Skip to main content
本页概述 Lager 平台的各个组件如何协同工作,从您笔记本上的 CLI 命令,一直到接在被测设备(DUT)上的仪器。

总体概览

这三个入口都通过同一条 Tailscale 隧道到达 Box,最终都落到同一批驱动程序上。但在 Box 内部,它们走的路径不同。 通过 Box API 驱动 Net 的命令直接 POST 到端口 9000,lager supply 就是其中之一。 lager python 则把脚本上传到端口 5000 上的执行服务。该服务把脚本作为子进程运行,子进程可以完整访问 lager.* 硬件库。 CI 运行器就是一台临时的开发者计算机。它使用与所运行命令相同的路径。

术语

Lager Box 内部结构

一个名为 lager 的 Docker 容器(用 --restart always 启动)运行全部服务。这些服务是平级的进程,而不是一条流水线。一个启动脚本逐个启动它们,并在它们退出时重启它们。 其中两方会调用端口 8080 上的硬件服务。第一方是端口 9000 上的 Box API。第二方是执行服务启动的每个用户脚本。两者都在自己的进程中解析 Net 名称,然后 POST 到 /invoke。调试服务和 MCP 服务是独立的。 硬件服务是仪器 VISA 会话的唯一拥有者。自行打开会话的调用方会与它争抢该 USB 设备。 只有当仪器不是 VISA 仪器时,脚本才会直接驱动它。直连驱动包括 LabJack、USB-202、 FT232H、Aardvark、Joulescope 和 PPK2。电源、示波器、电池模拟器、电子负载和太阳能模拟器与其他调用方一样,都经过 /invoke
每个进程各自持有自己的 NetsCache 它是每个解释器一个的单例,而不是整台 Box 一个。 Box API、硬件服务、调试服务和 MCP 服务各持有一个,每个 lager python 子进程也是如此。每份副本都读取 saved_nets.json,并根据该文件的 mtime 失效。因此这些副本会自行收敛。由此产生三个后果:每个进程都要自己承担第一次读取的开销;在一次写入和下一次读取之间,两个进程可能短暂地不一致;一个崩溃并重启的服务会带着冷缓存回来。

端口一览

主机防火墙不会过滤已发布的端口。 Docker 把它的转发规则装在主机链之前。因此,无论 ufw status 报告什么,一个已发布的端口都会响应任何能路由到该 Box 的人。请把网络可达性当作边界,把 Box 放在 VPN 上或隔离的局域网中。请参阅 SECURITY.md 的 Security Model 部分。
是否暴露这一列描述的是发布端口的 Box,这是默认行为。用 start_box.sh --no-publish (或 LAGER_NO_PUBLISH=1)启动的 Box 不发布其中任何端口。所有服务仍然在容器内监听, lagernet Docker 网络仍然可以访问它们,主机端口则由反向代理拥有。此时没有任何标记为 “是” 的行会在 <box-ip>:<port> 上响应。端口 22 是例外。SSH 是主机自己的守护进程,而不是容器发布的端口,因此 --no-publish 对它没有影响。设置了 lager box-config network-mode host 的 Box 同样不发布端口。在那种模式下,容器直接在主机上绑定端口,由主机防火墙管辖它们。

为什么有两个 HTTP 端口

Box 在 :9000:5000 上都响应,这个划分是历史原因造成的,而不是功能上的区别。 :9000 是 Box API,也是主要端口。Net 的元数据、仪器发现、Box 锁定、文件下载和版本报告都走这里。 :5000 是更早的脚本上传路径。lager python 仍然用它把脚本发送到 Box,以及停止正在运行的脚本。不会再往它上面添加新功能。 有些状态在两个端口上都能查到。锁状态就是其中之一:Box 在每个服务器上都暴露它,而 CLI 从 :9000 读取。 请让这两个端口都能通过您的 VPN 访问。只发布 :9000 的 Box 能响应 lager netslager hello,但 lager python 会失败。

可选的控制平面集成

Lager Box 会从一个密钥目录 /etc/lager/authorized_keys.d/ 发布 SSH 密钥。因此,外部控制平面可以在无人手动输入 SSH 命令的情况下开通访问权限。把一个 <name>.pub 文件放进去,该密钥会在大约五秒内进入 Box 账户的 ~/.ssh/authorized_keys。Box 把 /etc/lager 绑定挂载到运行时容器中,因此控制平面可以从容器内部写入该文件。它就是这样在还没有任何 SSH 访问权限时完成自举的。 start_box.sh 只拥有 authorized_keys 中位于它的 # BEGIN LAGER MANAGED KEYS# END LAGER MANAGED KEYS 标记之间的区域。它每一轮都会根据密钥目录重建该区域。由此产生两个后果:
  • 删除一个 .pub 就是吊销该密钥。 没有别的方法能做到;在标记区域内手动编辑 authorized_keys,会在下一轮被撤销。
  • 通过其他方式安装的密钥不受影响。 ssh-copy-id 和 cloud-init 追加在标记区域之外, start_box.sh 会原样保留这些行。任何其他管理该文件的系统都必须使用自己独立的一对标记。共用同一对标记的两个管理者,每一轮都会重建对方的区域。
  • 被保留不等于持久。 start_box.sh 保护零散的行不被它自己的重建删除,但它无法保护那一行不被别人删除。 第二个密钥管理者会根据它自己的来源重建 authorized_keys。它只保留自己的标记区域,因此会丢弃所有零散的行。随后 start_box.sh 只会根据密钥目录重建它自己的区域。从未进入过该目录的密钥不会回来。 因此,lager ssh-setuplager updatelager install 两件事都做:它们追加公钥,同时把它以 lager-box-<user>-<host>.pub 的名称写入密钥目录。任何希望所装密钥能够留存的工具,都必须这样做。
Lager 本身不要求也不运行控制平面 —— 这是一个挂钩,不是依赖。密钥目录不存在或为空,只意味着不从它发布任何密钥。 专业服务目录 列出了基于这个挂钩构建的商业控制平面。它们在 Lager 之上增加了组织管理、RBAC 和 SSO、审计日志以及调度功能。

Net 抽象

Net 是把 CLI 命令与物理硬件细节解耦的核心抽象。 psu1 背后的记录是这样的:
更换物理电源只需要编辑这条记录。每一条使用 psu1 的命令都继续有效。

受支持的 Net 类型

执行流程

CLI 命令的执行

lager supply psu1 voltage 3.3 --yes 的逐步数据路径: 硬件服务拥有并缓存每台物理设备的驱动程序。它在按设备的锁下串行化访问。因此,对 Box API 的并发请求不会在同一台仪器上交错进行 I/O。

自定义脚本的执行(lager python

lager python 命令把用户编写的 Python 脚本上传到端口 5000 上的执行服务。这条路径与上面的 Box API 命令不同。该服务把脚本作为独立的子进程运行,子进程有自己的解释器和自己的缓存。 接下来会发生什么,取决于仪器的类型。VISA 仪器会经过 Box API 使用的同一个 /invoke 代理,这包括电源、示波器、电池模拟器、电子负载和太阳能模拟器。其他仪器都在子进程内部构造和驱动: LabJack、USB-202、FT232H、Aardvark、Joulescope 和 PPK2。 这些直连 USB 的驱动程序会独占地占用它们的设备。因此,执行服务会先请硬件服务释放它自己的占用。它故意让共享的 VISA 会话保持打开。拆掉那些会话,正是下一条电源命令出现 [Errno 16] Resource busy 的原因。
脚本在 Docker 容器内部运行,可以完整访问 lager.* 硬件库。Box 会实时把它的输出回传给您。

物理接线

仪器在 Lager Box 和被测设备之间的物理连接方式:

连接类型

在 CI 中运行

CI 运行器用与开发者相同的命令驱动 Lager Box。Lager 不需要任何 CI 专用的基础设施。 有两种部署方式。位于独立主机上的运行器通过网络访问 Box,通常经由 Tailscale VPN。安装在 Box 上的运行器不需要网络跳转,也不需要任何密钥,并且它会把该实验台上的作业串行化。 在 CI 中使用 Lager 介绍这两种方式及各自的工作流程。它还涵盖实验台锁定、固件交付,以及作业被取消后的清理。