资讯动态

Terminus 节点状态增强:为 `/system/node-status` 注入主机明细字段的完整实现剖析

发布时间:2026/10/15 13:12:50 来源:尧图企业网站定制
云原生后端运维【免费下载链接】TerminusOpen-Source Personal Cloud OS for Always-On Agents项目地址https://gitcode.com/gh_mirrors/terminus2/Terminus点击查看免费下载本文以 Terminus 仓库中daemon/docs/superpowers/plans/2026-08-07-node-status-host-fields.md实现计划及配套设计规格为骨架结合cli/pkg/daemon/state、daemon/pkg/cluster/state、daemon/pkg/cluster/nodestatus与daemon/internel/apiserver/handlers的源码与测试深入剖析 Olares 守护进程如何在不改变/system/status公开 JSON 的前提下将 CPU 架构、内核版本、局域网 IP、有线连接状态与 Wi-Fi SSID 五个主机明细字段投影到节点详情接口。读完本文你将掌握该功能的字段契约、快照数据流、json:-隔离技巧、测试驱动实现路径以及如何验证与排查这条链路。一、背景与目标节点详情页需要的主机事实OlaresTerminus的每个节点由olaresd守护进程自报状态集群控制节点通过聚合各节点的本地报告获得集群视图。节点详情页需要展示五类主机层面的信息内核 / CPU 架构如amd64、arm64内核版本如6.8.0-60-generic内网 LAN IP有线连接状态Wi-Fi SSID设计规格daemon/docs/superpowers/specs/2026-08-07-node-status-host-fields-design.md给出了明确的约束只扩展GET /system/node-status绝不改动/system/status的线格式。原因在于/system/status是 TermiPass 与olares-cli已经轮询的遗留接口其 JSON 形状的任何变化都会波及 CLI、前端与 mDNS 客户端等所有消费方——cli/pkg/daemon/state/types.go中明确写着所有字段的 JSON tag 必须逐字节对齐线格式重命名 JSON key 必须先更新每一个消费者。最终落地的五个字段及其语义如下来自设计规格的 Wire format 表JSON keyGo 字段类型数据来源未知/缺失时的表现os_archOsArchstringState.OsArchos_kernelOsKernelstringState.OsKernel新增State 上带json:-hostIpHostIPstringState.HostIPwiredConnectedWiredConnectedboolState.WiredConnectedfalsewifiSSIDWifiSSIDstringState.WifiSSIDState 上为*string未连接/为空时整个 key 省略omitempty其中os_arch、hostIp、wiredConnected、wifiSSID的命名与/system/status中已有的同名 key 保持一致os_kernel是 node-status 独占字段不允许出现在/system/status上。二、架构决策从 5 秒快照投影不做逐请求探测实现计划明确了一件事不加新路由、不动鉴权、不增加按请求执行的探测。原因很直观——olaresd的状态刷新循环约每 5 秒一次本来就在采集这些信息机器信息GetMachineInfo通过子进程调用olares-cli osinfo show解析OS_TYPE、OS_INFO、OS_ARCH、OS_VERSION、OS_KERNEL。实现位于daemon/pkg/cluster/state/utils.go约 156 行起结果在进程生命周期内是静态的所以首次成功解析后会被缓存machineInfoCached避免每个 5 秒 tick 都 fork 一次子进程若 CLI 尚未就绪则返回空值并在下一个 tick 重试。此前osKernel已经被解析出来却被丢弃占位符_本次改造的核心之一就是把它持久化到 State 上。网络信息nets.GetInternalIpv4Addr()枚举内网接口utils.ManagedDeviceStatus(ctx)用单次nmcli device status枚举网络设备daemon/pkg/utils/network_linux.go按设备类型区分wifi置WifiConnectedtrue并记录WifiSSID与ethernet置WiredConnectedtruemacOS 上则按接口名前缀wl/en/eth推断SSID 通过utils.WifiName()获取daemon/pkg/utils/wifi.goLinux 走iwgetid --rawmacOS 走 airport-IHostIP来自nets.GetHostIp()并会校验/修复/etc/hosts中的域名与 IP 冲突。数据流可以概括为设计规格原图status refresh (≈5s) └─ GetMachineInfo → OsArch, OsKernel (以及已有 OS 字段) └─ network probe → HostIP, WiredConnected, WifiSSID │ ▼ CurrentState 快照 │ ▼ localNodeStatus → nodestatus.Build → GET /system/node-status刷新出的CurrentState通过snapshot.go的Snapshot()发布为不可变观测值每次读取都会复制一份绝不把共享切片交给调用方observedAt记录的是刷新完成时刻而非响应生成时刻避免把陈旧状态伪造成刚刚。nodestatus.Build只接受身份 快照 能力探测结果三样输入输出即节点本地视图——这保证了任何一次响应都对应同一个观测快照不会在刷新中途拼凑出前后矛盾的报告。三、Task 1把内核版本持久化到 State且不污染/system/status3.1 先写失败测试锁定线格式契约实现计划的第一步是测试先行。cli/pkg/daemon/state/types_test.go中新增了TestStateOsKernelIsExcludedFromSystemStatusWirefunc TestStateOsKernelIsExcludedFromSystemStatusWire(t *testing.T) { raw, err : json.Marshal(State{ OsArch: amd64, OsKernel: 6.8.0-60-generic, HostIP: 10.0.0.2, }) if err ! nil { t.Fatal(err) } var got map[string]any if err : json.Unmarshal(raw, got); err ! nil { t.Fatal(err) } if _, exists : got[os_kernel]; exists { t.Fatalf(os_kernel must not appear on /system/status wire, got %#v, got) } if _, exists : got[OsKernel]; exists { t.Fatalf(OsKernel must not appear on /system/status wire, got %#v, got) } if got[os_arch] ! amd64 { t.Fatalf(os_arch %#v, want amd64, got[os_arch]) } if got[hostIp] ! 10.0.0.2 { t.Fatalf(hostIp %#v, want 10.0.0.2, got[hostIp]) } }在实现前运行它必然失败State.OsKernel未定义或编译错误这正是 TDD 的红灯阶段。这个测试同时锁定了三层语义os_kernel与OsKernel都不得出现在/system/status线格式中而os_arch、hostIp必须保持存在且取值正确。3.2 在 State 结构体中用json:-隔离在cli/pkg/daemon/state/types.go中位于OsVersion字段块之后、CpuInfo之前插入// OsKernel is the kernel release string from GetMachineInfo (OS_KERNEL). // It is daemon-internal: excluded from /system/status so that endpoints // wire format stays unchanged. /system/node-status projects it as os_kernel. OsKernel string json:-json:-是 Go 标准库序列化的隐身衣该字段完全不出现在 JSON 中。这与文件中已有的模式一脉相承——InstallFinishedTime、ExternalIPProbeTime、InstallingProgressNum、UpgradingProgressNum、UpgradingDownloadProgressNum都是带json:-的守护进程内部字段既参与状态机逻辑又不向线格式泄漏。这样OsKernel就能安全地活在内存快照里由 node-status 单独投影为os_kernel。设计权衡为什么不在/system/status上直接加os_kernel,omitempty规格的 Out of scope 部分写得很清楚——改变/system/status公开 JSON包括在此新增os_kernel是明确不做的事。遗留端点要保持形状稳定新增能力一律走新端点。3.3 刷新循环不再丢弃内核值daemon/pkg/cluster/state/current.go的refreshCurrentStatus中原来的osType, osInfo, osArch, osVersion, _, err : GetMachineInfo(ctx)改为osType, osInfo, osArch, osVersion, osKernel, err : GetMachineInfo(ctx)并在其他 OS 赋值附近新增CurrentState.OsKernel osKernel。实际仓库代码中这一行落在daemon/pkg/cluster/state/current.go的CurrentState.OsArch osArch之后约 152 行与OsInfo、OsVersion、OsType的赋值并列共同构成每次刷新的机器信息落盘。注意GetMachineInfo失败时设计规格的约定是保留现有 warning 日志OsArch/OsKernel保持为空接口仍返回 HTTP 200——刷新循环异常不能拖垮状态读取。四、Task 2nodestatus.Build投影主机字段4.1 扩展现有的三组单元测试daemon/pkg/cluster/nodestatus/detail_test.go中追加了三个测试覆盖完整映射、Wi-Fi 省略与未知留空三类场景完整映射——五个值全部就位时逐一断言func TestBuildReportsHostDetailFields(t *testing.T) { ssid : olares-lab st : clistate.State{ TerminusState: clistate.TerminusRunning, OsArch: amd64, OsKernel: 6.8.0-60-generic, HostIP: 10.0.0.2, WiredConnected: true, WifiSSID: ssid, } got : Build(Identity{}, st, nil, time.Now()) if got.OsArch ! amd64 { t.Errorf(os_arch %q, want amd64, got.OsArch) } if got.OsKernel ! 6.8.0-60-generic { t.Errorf(os_kernel %q, want 6.8.0-60-generic, got.OsKernel) } if got.HostIP ! 10.0.0.2 { t.Errorf(hostIp %q, want 10.0.0.2, got.HostIP) } if !got.WiredConnected { t.Errorf(wiredConnected false, want true) } if got.WifiSSID ! ssid { t.Errorf(wifiSSID %q, want %q, got.WifiSSID, ssid) } }Wi-Fi 断开时省略 key——直接对json.Marshal(Build(...))的结果做 key 级检查func TestBuildOmitsWifiSSIDWhenDisconnected(t *testing.T) { st : clistate.State{ TerminusState: clistate.TerminusRunning, WiredConnected: true, HostIP: 10.0.0.2, } raw, err : json.Marshal(Build(Identity{}, st, nil, time.Now())) if err ! nil { t.Fatalf(marshal: %v, err) } var fields map[string]json.RawMessage if err : json.Unmarshal(raw, fields); err ! nil { t.Fatalf(decode %s: %v, raw, err) } if _, ok : fields[wifiSSID]; ok { t.Errorf(wifiSSID must be omitted when disconnected: %s, raw) } for _, key : range []string{os_arch, os_kernel, hostIp, wiredConnected} { if _, ok : fields[key]; !ok { t.Errorf(field %q missing: %s, key, raw) } } }未知值一律留空、不发明占位符func TestBuildLeavesUnknownHostDetailsEmpty(t *testing.T) { got : Build(Identity{}, clistate.State{TerminusState: clistate.TerminusRunning}, nil, time.Now()) if got.OsArch ! || got.OsKernel ! || got.HostIP ! || got.WifiSSID ! { t.Errorf(want empty host detail strings: %v, got) } if got.WiredConnected { t.Errorf(wiredConnected true, want false when unknown) } }4.2Status结构体与Build的映射实现daemon/pkg/cluster/nodestatus/nodestatus.go中Status在OlaresdVersion之后、ObservedAt之前新增// Host detail fields projected from the local state snapshot. Keys match // /system/status where those already exist; os_kernel is node-status only. OsArch string json:os_arch OsKernel string json:os_kernel HostIP string json:hostIp WiredConnected bool json:wiredConnected WifiSSID string json:wifiSSID,omitemptyBuild中Wi-Fi 的指针解引用被显式处理——只有State.WifiSSID非 nil 时才取字符串值var wifiSSID string if st.WifiSSID ! nil { wifiSSID *st.WifiSSID }并在返回的结构体字面量中补齐映射OsArch: st.OsArch, OsKernel: st.OsKernel, HostIP: st.HostIP, WiredConnected: st.WiredConnected, WifiSSID: wifiSSID,注意Status.WifiSSID是string非指针配合omitemptyState.WifiSSID则是*string——指针在 State 侧用于表达未采集到与空字符串的区分而线格式侧只要为空即整体省略 key。omitempty对空字符串自动生效因此 nil 指针解引用后的空字符串会在序列化时被丢弃测试TestBuildOmitsWifiSSIDWhenDisconnected验证的正是这一点。4.3 关键设计约束未知就是未知nodestatus包的包级注释点明了整个模块的哲学Anything this node cannot confirm is reported as unknown rather than guessed, so that a caller aggregating several nodes never turns a missing fact into a false one.本节点无法确认的东西一律报为 unknown绝不猜测否则聚合多个节点时会把缺失的事实变成错误的事实。主机明细字段同样遵循此原则表示未知false表示未连接永不填充占位值。五、Task 3HTTP 线格式覆盖与鉴权行为5.1 路由与鉴权daemon/internel/apiserver/handlers/system_group.go中注册system.Get(/node-status, handlers.RequireAuthorizationOrOwnerSignature(handlers.RequireLocal(handlers.GetNodeStatus)))RequireLocal限定本机可达RequireAuthorizationOrOwnerSignature要求携带有效鉴权头或 owner 签名——这解释了测试文件中为何有无鉴权返回 403与owner 签名可执行预检两种用例。处理器GetNodeStatushandler_node_status.go只做一件事取快照 → 构建身份 → 调用localNodeStatus返回OkJSON真正的组装发生在handler_cluster.go的localNodeStatusfunc localNodeStatus(ctx context.Context) nodestatus.Status { snap, observedAt : currentStateSnapshot() id : identityFrom(ctx, snap) return nodestatus.Build( id, snap, nodestatus.Detect(ctx, nodestatus.ProbeInput{Identity: id, State: snap}), observedAt, ) }一次请求只取一个快照贯穿全程——避免刷新循环中途落地导致响应前后矛盾。5.2 处理器级测试种子快照 逐 key 断言daemon/internel/apiserver/handlers/handler_node_status_test.go中TestNodeStatusReportsTheDetailFields通过withCurrentState安装带全部字段的快照然后调用/system/node-statusssid : olares-lab withCurrentState(t, clistate.State{ TerminusState: clistate.TerminusRunning, Memory: 128 G, Disk: 3725 G, OlaresdVersion: version, OsArch: amd64, OsKernel: 6.8.0-60-generic, HostIP: 10.0.0.2, WiredConnected: true, WifiSSID: ssid, }, time.Now())解码响应后要求 JSONdata对象同时包含十个 keyfor _, key : range []string{memory, disk, olaresdVersion, observedAt, deviceType, os_arch, os_kernel, hostIp, wiredConnected, wifiSSID} { if _, ok : env.Data[key]; !ok { t.Errorf(field %q missing from the node wire format: %s, key, body) } }并反向断言不存在的字段Olares 中不存在节点 id也不存在字节计数字段for _, absent : range []string{nodeId, node_id, memoryBytes, diskBytes} { if _, ok : env.Data[absent]; ok { t.Errorf(field %q is not something this node knows: %s, absent, body) } }配套的TestNodeStatusOmitsWifiSSIDWhenDisconnected则验证断开场景下wifiSSID从 HTTP body 中消失、hostIp与wiredConnected保持正确。5.3 集群聚合自动受益设计规格指出集群汇总逻辑扇出到各节点/system/node-status时会自动在每台节点的 body 中收到新字段——无需改动任何路由、鉴权或聚合协议。控制节点通过http://node-ip:18088/system/node-statusnodeStatusURL拉取各工作节点报告字段随每台节点各自的 daemon 版本逐步演进这也正是olaresdVersion字段存在的原因不同节点 daemon 版本漂移是某节点提供某能力而邻居没有的常见解释详情页需要显示它。六、测试矩阵与回归保障实现计划在末尾给出了 Spec 覆盖清单映射关系如下Spec 需求覆盖 Taskos_arch出现在 node-statusTask 2、3os_kernel仅出现在 node-statusTask 1json:-、Task 2、3hostIpTask 2、3wiredConnected布尔Task 2、3wifiSSID断开时 omitemptyTask 2、3从GetMachineInfo持久化内核Task 1/system/status线格式不变Task 1 测试空/未知 → 空字符串、wiredConnectedfalseTask 2处理器 HTTP 覆盖Task 3路由/鉴权/聚合不变明确 Out of scope三层测试构成了完整防线状态层测试cli/pkg/daemon/state/types_test.go保证 State 序列化契约不变映射层测试daemon/pkg/cluster/nodestatus/detail_test.go验证Build的字段映射、omitempty与空快照行为处理器层测试handler_node_status_test.go验证真实 HTTP 响应 body 的 key 集合。此外还有既有测试的回归保护例如TestSystemStatusIsUnchanged确保遗留端点持续应答且载荷形状不变。运行测试的命令以仓库布局为例cli与daemon是两个独立 Go modulego test ./pkg/daemon/state/ -run TestStateOsKernelIsExcludedFromSystemStatusWire -v go test ./pkg/cluster/nodestatus/ -run TestBuildReportsHostDetailFields|TestBuildOmitsWifiSSIDWhenDisconnected|TestBuildLeavesUnknownHostDetailsEmpty -v go test ./internel/apiserver/handlers/ -run TestNodeStatusReportsTheDetailFields|TestNodeStatusOmitsWifiSSIDWhenDisconnected -vdaemon模块通过 go.mod 中的 replace 指令引用本地cli模块因此state类型别名daemon/pkg/cluster/state/current.go中的type state clistate.State让 daemon 与 CLI 共享完全相同的线格式类型。七、实现要点速查与排查指南7.1 关键文件与职责文件职责cli/pkg/daemon/state/types.goState结构体新增OsKernel string json:-/system/status线格式的唯一权威定义cli/pkg/daemon/state/types_test.go断言OsKernel不出现在 State JSON 中daemon/pkg/cluster/state/current.gorefreshCurrentStatus每 5 秒刷新持久化OsKernel、HostIP、WiredConnected、WifiSSIDdaemon/pkg/cluster/state/snapshot.go不可变观测快照的发布与读取Snapshot()daemon/pkg/cluster/state/utils.goGetMachineInfo解析并缓存olares-cli osinfo show的输出daemon/pkg/cluster/nodestatus/nodestatus.goStatus结构体与Build映射逻辑daemon/pkg/cluster/nodestatus/detail_test.goBuild的映射 / omitempty / 空快照单测daemon/internel/apiserver/handlers/handler_node_status.goGetNodeStatus处理器daemon/internel/apiserver/handlers/handler_cluster.golocalNodeStatus快照 身份 能力探测 →Builddaemon/internel/apiserver/handlers/handler_node_status_test.goHTTP 线格式测试daemon/internel/apiserver/handlers/system_group.go/system/node-status路由注册与鉴权7.2 排查要点os_kernel出现在/system/status里检查State.OsKernel的 tag 是否被误删——它必须保持json:-/system/status的公开 JSON 以cli/pkg/daemon/state/types.go为准。wifiSSID未按预期省略确认Status.WifiSSID带omitempty且Build只在st.WifiSSID ! nil时取值线格式上空字符串 omitempty才会让 key 消失。字段全是空串 /wiredConnectedfalse先看GetMachineInfo是否解析成功CLI 未就绪会返回空并重试再看网络探测是否枚举到设备——未知即空是设计行为不是 bug。响应时间戳可疑observedAt是刷新完成时刻而非响应时刻快照发布与读取机制snapshot.go保证读者绝不会排队等待持锁的刷新循环。赞分享云原生后端运维【免费下载链接】TerminusOpen-Source Personal Cloud OS for Always-On Agents项目地址https://gitcode.com/gh_mirrors/terminus2/Terminus点击查看免费下载相关推荐5步完成iOS激活锁绕过Applera1n专业工具终极指南5步完成iOS激活锁绕过Applera1n专业工具终极指南 忘记Apple ID密码导致iPhone无法激活购买的二手设备被激活锁锁定无法使用ApplerAPI网关后端云原生微服务KubeEdge 节点任务状态结构增强v1alpha2 下的 Node Job / Node Task 状态模型设计解析KubeEdge 节点任务状态结构增强v1alpha2 下的 Node Job / Node Task 状态模型设计解析 导读 本篇文章以 KubeEdge云原生边缘计算物联网容器编排边缘网关Apache APISIX node-status 插件详解通过 /apisix/status 接口实时查询网关节点状态Apache APISIX node status 插件详解通过 /apisix/status 接口实时查询网关节点状态 node status 是 Apac后端微服务云原生上一篇Tracy Profiler如何快速定位游戏帧率卡顿和热点函数下一篇Resgate客户端协议详解WebSocket通信与自动重连机制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑