外观
Control Server —— 管理面 API
面向管理员与自动化脚本。全部路由前缀 /control/v1/admin,下文表格的「路径」列省略该前缀,全部需要 Admin Bearer(两种凭据见 鉴权模型)。
管理端登录端点 /control/v1/auth/* 不在 admin 前缀下,但属于同一信任域,一并收录在本文开头。
账号登录
源码:apps/control-server/app/api/v1/auth/admin.py。管理员账号(用户名 + 密码 + Cap 人机验证)登录,替代人手分发共享 admin token;静态 token 保持原样可用。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
POST | /control/v1/auth/login | 无 | 请求体 {username, password, captcha_token}(历史字段名 turnstile_token 等价收取)。成功 200 返回 {token, expires_at}——不透明会话令牌 + ISO 8601 过期时间 |
POST | /control/v1/auth/password | 账号会话令牌 | 请求体 {old_password, new_password}(新密码 ≥8 字符)。成功 204,并吊销该用户全部存活会话(含当前,前端 401 后全局登出重登);静态 admin token 调用返回 403 |
/auth/login 的错误语义(前端按状态码 + detail 分支):
| 状态码 | detail(稳定字符串) | 含义 |
|---|---|---|
400 | turnstile verification failed | 人机验证 token 缺失、伪造或复用。token 一次性,服务端调 Cap 实例 siteverify 硬校验;[auth] cap_* 三项配置不齐同样 400(fail-closed)。措辞为历史锁定契约,不随提供方变化 |
401 | invalid credentials | 用户不存在与密码错误统一文案(防枚举)。服务端对不存在的用户也跑一次 argon2 对照校验,抹平时间侧信道 |
403 | 同管理面 | 未配置静态 admin token,管理面整体锁定 |
429 | too many attempts | 同用户名或同 IP 连续凭据失败 5 次后的 15 分钟封禁窗口;登录成功清零计数。IP 取 CF-Connecting-IP → X-Forwarded-For → 直连地址 |
登录成功、失败、限速与改密全部写入管理审计(path 为 /control/v1/auth/login 或 /control/v1/auth/password;成功记 actor 为用户名,失败记尝试的用户名与 IP)。密码明文绝不入日志。
操作步骤见 认证与账号。
节点(Node)
源码:apps/control-server/app/api/v1/admin/nodes.py
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /nodes | 列出所有节点(按 node_id 排序) |
POST | /nodes | 创建节点,201;node_id 已存在 409 |
GET | /nodes/{node_id} | 取单节点 |
PATCH | /nodes/{node_id} | 局部更新;已发布过(current_generation > 0)则同事务重物化并广播 |
DELETE | /nodes/{node_id} | 删除,204;活节点(已发布且非 decommissioned)409,须先退役 |
POST | /nodes/{node_id}/decommission | 退役:发布空 DesiredState,拆除隧道、撤 BGP,保留节点与子表 |
POST | /nodes/{node_id}/recommission | 撤销退役,恢复 active 并重新物化 |
GET | /nodes/{node_id}/desired-state | 该节点已发布的完整 DesiredState;无则 404 |
GET | /nodes/{node_id}/generations?limit= | 世代历史元信息(limit 1..500,默认 50) |
GET | /nodes/{node_id}/generations/{generation} | 单代详情(元信息 + 完整快照) |
GET | /nodes/{node_id}/generations/{generation}/diff?against= | 两代字段级 diff;against 缺省取 generation-1,第 1 代须显式传 |
POST | /nodes/{node_id}/generations/{generation}/rollback | 把某代快照重新发布为新一代并广播 |
POST | /nodes/{node_id}/notify | 手动门铃:desired_state_updated(递增世代)或 snapshot_request;无在线 agent 订阅时 409 |
NodeIn:node_id(1..64)、asn(≥1)、router_id;可选 site / loopback_ipv4 / loopback_ipv6 / link_local / ipv4_prefixes[] / ipv6_prefixes[] / inventory{} / labels{} / base_template{}。
NodePatch 同字段全可选(exclude_unset 语义,省略即不动)。
NodeOut 额外含 current_generation / lifecycle / dns_group_id / wireguard_public_key / created_at / updated_at。其中 wireguard_public_key 是节点密钥表公钥的只读投影(写路径只有密钥表本身,见 fleet 供给收权),未生成密钥时为 null。
link_local 是外部 eBGP link-local 地址的单一真相源:materializer 由它派生外部 WG 接口地址。取值须为 fe80::/10 内、不带 %zone 后缀的 IPv6 link-local 地址,非法值 422。
json
// POST /control/v1/admin/nodes
{
"node_id": "edge1",
"asn": 4242420000,
"router_id": "172.20.0.1",
"site": "hkg",
"loopback_ipv4": "172.20.0.1",
"loopback_ipv6": "fd00:1234::1",
"ipv4_prefixes": ["172.20.0.0/27"],
"ipv6_prefixes": ["fd00:1234::/48"],
"labels": {"region": "apac"}
}json
// 201 Created
{
"node_id": "edge1",
"asn": 4242420000,
"router_id": "172.20.0.1",
"site": "hkg",
"loopback_ipv4": "172.20.0.1",
"loopback_ipv6": "fd00:1234::1",
"link_local": null,
"ipv4_prefixes": ["172.20.0.0/27"],
"ipv6_prefixes": ["fd00:1234::/48"],
"inventory": {},
"labels": {"region": "apac"},
"base_template": {},
"current_generation": 0,
"lifecycle": "active",
"dns_group_id": null,
"wireguard_public_key": null,
"created_at": "2026-08-10T00:00:00Z",
"updated_at": "2026-08-10T00:00:00Z"
}NotifyRequest:{ "event": "desired_state_updated" | "snapshot_request", "reason": "..." }(默认 desired_state_updated)。NotifyResponse:{ node_id, event, generation, subscribers, delivered }。RollbackResponse:{ node_id, target_generation, new_generation, reason, subscribers, delivered }。
Peering
源码:apps/control-server/app/api/v1/admin/peerings.py。Peering 是对等关系的元信息,本身不进入 DesiredState;普通 CRUD 不触发 materialize,provision 与 full 才会一并物化子资源。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /nodes/{node_id}/peerings | 列出节点的 peering(按 name) |
POST | /nodes/{node_id}/peerings | 创建 peering,201;同节点重名 409 |
GET | /peerings/{peering_id} | 取单条 peering |
PATCH | /peerings/{peering_id} | 局部更新 peering 元信息 |
DELETE | /peerings/{peering_id} | 删除,204。关联的活跃自动对等请求行同事务推进 cancelled(reject_reason='removed by operator'),配额位与 (asn, node) 活跃唯一位随之释放 |
POST | /nodes/{node_id}/peerings/provision | 一键:同事务建 Peering + WgInterface +(可选)BgpSession 并物化广播,201 |
GET | /nodes/{node_id}/peerings/full | 组合读:节点全部 peering 及其名下接口与 BGP 会话 |
GET | /peerings/{peering_id}/full | 单 peering 的组合读 |
PUT | /nodes/{node_id}/peerings/full | 按 (node_id, peering.name) create-or-replace 完整 peer(子资源整集替换),200 |
POST | /nodes/{node_id}/peerings/backfill | 把孤儿接口与会话归并成 Peering(dry_run 只返回计划,不物化、不广播) |
PeeringIn:name(1..64)、remote_asn(≥1);可选 remote_node_id / remote_label / is_internal(默认 false)/ enabled(默认 true)/ notes。GET /nodes/{node_id}/peerings 的行额外带 remote_as_name(registry 副本的 as-name,与路由表起源名称同源;未配 registry 服务或未收录为 null)。
PeeringProvisionIn:peering(PeeringIn)+ interface_spec(InterfaceSpec 的 JSON dump)+ interface_enabled / interface_sort_order;可选 bgp_spec(BgpSessionSpec dump)+ bgp_sort_order。三段先各自 schema 校验(失败 422),任一唯一约束冲突 409 整笔回滚。
json
// POST /control/v1/admin/nodes/edge1/peerings/provision
{
"peering": {
"name": "as4242421111",
"remote_asn": 4242421111,
"remote_label": "example-peer",
"is_internal": false
},
"interface_spec": {
"name": "wg-peer1",
"kind": "wireguard",
"listen_port": 51820,
"addresses": ["fe80::1/64"]
},
"bgp_spec": {
"name": "as4242421111",
"remote_asn": 4242421111,
"neighbor_address": "fe80::2",
"enabled": true
}
}json
// 201 Created(节选)
{
"peering": { "id": 12, "local_node_id": "edge1", "name": "as4242421111", "remote_asn": 4242421111 },
"interface": { "id": 34, "node_id": "edge1", "name": "wg-peer1", "kind": "wireguard" },
"bgp_session": { "id": 56, "node_id": "edge1", "name": "as4242421111", "remote_asn": 4242421111 },
"generation": 5
}PeeringFullIn(PUT .../peerings/full):{ peering: PeeringIn, interfaces: [{spec, enabled, sort_order}], bgp_sessions: [{spec, sort_order}] };返回 { peering: <full>, generation }。
聚合根语义见 Control Server 内部,操作步骤见 建立互联。
接口(WgInterface)
源码:apps/control-server/app/api/v1/admin/interfaces.py。写端点接受完整 InterfaceSpec(spec 字段),先 schema 校验,写完物化并广播。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /nodes/{node_id}/interfaces | 列出节点接口(按 sort_order, id) |
POST | /nodes/{node_id}/interfaces | 创建接口并物化广播,201;同节点重名 409 |
GET | /interfaces/{iface_id} | 取单接口 |
PATCH | /interfaces/{iface_id} | 局部更新(spec / enabled / sort_order / peering_id),物化广播 |
DELETE | /interfaces/{iface_id} | 删除并物化广播,204 |
InterfaceIn:spec(InterfaceSpec dump);可选 peering_id / enabled(默认 true)/ sort_order(默认 0)。InterfacePatch 各字段可选,并有 clear_peering(显式把 peering_id 置 null)。InterfaceOut:{ id, node_id, peering_id, name, kind, enabled, sort_order, spec }。
WireGuard 端口策略:启用的 WG 接口若设 listen_port,须落在端口池对应段位内(越界 422),且不得与同节点其他启用 WG 接口端口冲突(409)。段位划分见 fleet 供给收权。
BGP 会话(BgpSession)
源码:apps/control-server/app/api/v1/admin/bgp_sessions.py。写端点接受完整 BgpSessionSpec(spec 字段),校验后物化广播。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /nodes/{node_id}/bgp-sessions | 列出节点会话(按 sort_order, id) |
POST | /nodes/{node_id}/bgp-sessions | 创建会话并物化广播,201;同节点重名 409 |
GET | /bgp-sessions/{session_id} | 取单会话 |
PATCH | /bgp-sessions/{session_id} | 局部更新(spec / sort_order / peering_id),物化广播 |
DELETE | /bgp-sessions/{session_id} | 删除并物化广播,204 |
SessionIn:spec(BgpSessionSpec dump);可选 peering_id / sort_order。SessionPatch 各字段可选 + clear_peering。SessionOut:{ id, node_id, peering_id, name, remote_asn, enabled, sort_order, spec }。
iBGP / OSPF 内部互联不是
bgp_sessions记录,由bird.internal_topology合成。视图见控制台 BFF 的GET /ui/nodes/{node_id}/internal-topology,语义见 内部互联。
DNS 组(DnsGroup / Zone / Record)
源码:apps/control-server/app/api/v1/admin/dns_groups.py。三级模型:DnsGroup(name + bind_addresses)→ DnsGroupZone(组声明的权威 zone + 可选 SOA)→ DnsRecord(扁平 name / type / content)。节点经 Node.dns_group_id 订阅;多节点订阅同组即构成 anycast。组、zone、记录的写入会重新物化该组全部成员节点并广播。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /dns-groups | 列出 DNS 组 |
POST | /dns-groups | 创建组,201;重名 409 |
GET | /dns-groups/{group_id} | 取单组(含 zone_count / member_count) |
PATCH | /dns-groups/{group_id} | 更新组,重物化成员并广播 |
DELETE | /dns-groups/{group_id} | 删除组(成员节点 dns_group_id 置 null),重物化广播,204 |
GET | /dns-groups/{group_id}/zones | 列出组的权威 zone |
POST | /dns-groups/{group_id}/zones | 创建 zone,201;组内重名 409 |
PATCH | /dns-groups/{group_id}/zones/{zone_id} | 更新 zone |
DELETE | /dns-groups/{group_id}/zones/{zone_id} | 删除 zone,200 返回最新组摘要(zone_count 已更新) |
GET | /dns-groups/{group_id}/zones/{zone_id}/records | 列出 zone 记录(按 sort_order, id) |
POST | /dns-groups/{group_id}/zones/{zone_id}/records | 创建记录,201 |
PATCH | /dns-groups/{group_id}/zones/{zone_id}/records/{record_id} | 更新记录 |
DELETE | /dns-groups/{group_id}/zones/{zone_id}/records/{record_id} | 删除记录,200 返回最新 zone 摘要(record_count 已更新) |
PUT | /nodes/{node_id}/dns-group | 给节点分配或取消 DNS 组,重物化广播 |
DnsGroupIn:name、bind_addresses[]、cache_ttl_seconds(默认 300)、forwards[]、enabled(默认 true)。 DnsZoneIn:zone(合法域名,放行 RFC 2317 的 allow_slash);可选 primary_ns / admin_email / soa_* / default_ttl / enabled。 DnsRecordIn:name、type、content;可选 ttl / comment / enabled / sort_order。 PUT /nodes/{id}/dns-group 请求 { "dns_group_id": <int|null> }。
操作步骤见 DNS 与任播。
Agent Token
源码:apps/control-server/app/api/v1/admin/tokens.py
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /nodes/{node_id}/agent-tokens | 列出该节点全部 token 元信息(不含 secret) |
POST | /nodes/{node_id}/agent-tokens | 签发新 token,201,响应一次性返回 secret |
POST | /agent-tokens/{token_id}/rotate | 轮换:撤旧签新,201,返回新 secret |
DELETE | /agent-tokens/{token_id} | 撤销 token,204 |
AgentTokenIssueIn(请求体可省略):可选 token(自定义字面量,须 base64url 且满足最小熵,否则 400)/ agent_id / ttl_seconds(≥1)。AgentTokenRotateIn:可选 ttl_seconds。响应 AgentTokenOut:{ token, secret?, node_id, agent_id, issued_at, expires_at?, revoked_at? }——签发与轮换时 token 字段即完整 secret,仅此一次。
json
// POST /control/v1/admin/nodes/edge1/agent-tokens
{ "ttl_seconds": 2592000 }json
// 201 Created(token 字段即完整 secret,仅此一次)
{
"token": "agt_ab12cd.s3cr3tVALUEonlyShownOnce",
"secret": "agt_ab12cd.s3cr3tVALUEonlyShownOnce",
"node_id": "edge1",
"agent_id": "agent-edge1",
"issued_at": "2026-08-10T00:00:00Z",
"expires_at": "2026-09-09T00:00:00Z",
"revoked_at": null
}Enrollment Token
源码:apps/control-server/app/api/v1/admin/enrollment_tokens.py
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /enrollment-tokens | 列出全部门票元信息(不含 secret) |
POST | /enrollment-tokens | 创建门票,201,响应一次性返回 secret |
DELETE | /enrollment-tokens/{token_id} | 删除门票,204 |
EnrollmentTokenIn(可省略):可选 token(字面量,须 base64url 且满足最小熵,已存在 409)/ node_id(绑定到该节点,未知 400)/ description / expires_at。响应 EnrollmentTokenCreated:{ token_id, node_id?, description?, expires_at?, used_at?, created_at, secret }(secret 仅创建可见)。
注册审批(Registrations)
源码:apps/control-server/app/api/v1/admin/registrations.py。审批只是门禁名单:approve 不自动 provision、不自动发 token,真正下发仍需 POST /admin/provision 或逐资源 CRUD。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /registrations?status= | 列出注册请求;status ∈ pending / approved / rejected,缺省全部 |
POST | /registrations/{registration_id}/approve | 标记 approved;未知 404 |
POST | /registrations/{registration_id}/reject | 标记 rejected;未知 404 |
请求体 RegistrationDecisionIn(可省略):{ "note": "..." }。GET 返回 { "registrations": [...] },审批端点返回该注册记录。
Provision
源码:apps/control-server/app/api/v1/admin/provision.py。接受一份完整 DesiredState,一次性落库并发布为新一代;同 node_id 重复 provision 幂等覆盖,不报 409。
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /provision | 整节点 provision,201;DesiredState 校验失败 422 |
ProvisionIn:{ "state": <完整 DesiredState 的 JSON dump>, "agent_token": "<可选固定 token>" }。ProvisionOut:{ node_id, generation, subscribers, delivered }。
json
// POST /control/v1/admin/provision
{
"state": {
"node": { "node_id": "edge1", "asn": 4242420000, "router_id": "172.20.0.1" },
"interfaces": [],
"bgp_sessions": [],
"bird": { "internal_topology": { "routers": [], "hosts": [] } }
},
"agent_token": "agt_fixed.bootstrapTokenForLabUse"
}state 的完整字段见 DesiredState 参考。
健康(只读)
源码:apps/control-server/app/api/v1/admin/health.py。数据来自 NodeStatusStore(agent 上报)。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /health | 全 fleet 健康概览({ summary, nodes }) |
GET | /nodes/{node_id}/health | 单节点健康 + 最近 snapshot / report / apply;无上报 404 |
GET | /nodes/{node_id}/status-events?kind=&limit= | 上报历史;kind ∈ snapshot / report / apply / reresolve,limit 1..500(默认 50) |
健康五态的判定顺序见 Control Server 内部。
路由(只读)
源码:apps/control-server/app/api/v1/admin/routing.py。数据来自 RoutingStore(agent 周期上报的路由全表观测聚合)。控制台侧另有面向界面的聚合端点,见 控制台 BFF。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /routing/fleet | 全 fleet 路由概览 |
GET | /nodes/{node_id}/routing/summary | 全表规模 + RPKI / 前缀长度 / AS path 分布;无上报 404 |
GET | /nodes/{node_id}/routing/origins?limit= | 起源 AS Top 榜(limit 1..1000,默认 50) |
GET | /nodes/{node_id}/routing/prefixes?… | 分页与过滤的路由检索 |
GET | /nodes/{node_id}/routing/timeline?limit= | 路由表趋势与 churn(limit 1..500,默认 200) |
prefixes 查询参数:
| 参数 | 取值 | 语义 |
|---|---|---|
family | 4 / 6 | 地址族,缺省全部 |
scope | all / local / external | 默认 all |
q | ≤128 字符 | 文本过滤,匹配前缀 / 协议 / 起源 ASN |
rpki | valid / invalid / not_found | 按校验状态过滤 |
path_asn | ASN | 匹配 as_path 包含该 AS 的路由(含起源跳)。带边界匹配,不做十进制子串伪匹配;与 q 独立可组合 |
limit | 1..1000 | 默认 100 |
offset | ≥0 | 默认 0 |
响应顶层另有两个派生字段:
rpki_counts——除rpki自身外当前条件下的三态分布,驱动前端 RPKI 计数徽标。快照rpki_observed=false时为null,前端隐藏该组件。as_names——当前页出现的 origin ASN → registry as-name 映射(键为 ASN 字符串,未收录省略,未接 registry 服务时恒空)。
审计(只读)
源码:apps/control-server/app/api/v1/admin/audit.py
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /audit-log?limit= | 按时间倒序返回最近管理写操作(limit 1..1000,默认 100),返回 { "entries": [...] } |
控制台侧有支持游标翻页与文本过滤的同源端点,见 控制台 BFF。
Agent 发布
源码:apps/control-server/app/api/v1/admin/agent_releases.py。agent 自更新的管理侧:把 wheel 产物上传到控制面(落发布存储目录并生成含 sha256 的 manifest),再设全局目标版本。agent 经 WS 心跳上报版本,低于目标即被推送一次 agent_update_available;分发端点见 节点面 API。
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /agent-releases | 上传一个版本的全部 wheel(multipart:version 字段 + 多个 files),201;同版本重复上传覆盖,上传不自动放量 |
POST | /agent-releases/target | 设全局目标版本;版本未上传(无 manifest)422 |
GET | /agent-releases | 已上传版本 + 当前目标 + 各节点 agent 版本一览 |
版本号限定 x.y.z、文件名限定 *.whl,二者过严格白名单(防路径穿越),非法 422。
上传响应 AgentReleaseManifest:{ version, wheels: [{ filename, sha256, size }] }。/target 请求 { "version": "1.0.312" }。/target 与 GET 均返回 ReleasesStatusOut:{ target, versions[], nodes[] }——nodes 来自 WS 心跳注册表({ node_id, agent_version, applied_generation, apply_status, last_seen, up_to_date, liveness })。up_to_date 表示该节点版本是否等于当前目标;liveness 是服务端按心跳新鲜度分出的 online / stale / offline,阈值与 /ui/nodes 行同源,并经 /ui/session 下发给前端。
滚动升级步骤见 Agent 滚动升级。
flap 处置(mitigations)
源码:apps/control-server/app/api/v1/admin/mitigations.py。从 flap 事件一键发起的人工处置——检测层只告警,不做任何自动抑制。处置声明式、可撤销、全程审计。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /fleet/mitigations | 当前生效处置汇总:拒收前缀(按前缀归组列节点)+ 全部 disabled 会话(撤销入口的数据源) |
POST | /fleet/mitigations/prefix-block | 拒收前缀:写进目标节点 base_template.bird.blocked_prefixes,eBGP 导入过滤器精确匹配 reject。{prefix, nodes?},缺省 fleet 级(全部未退役节点);幂等;非法前缀 422、未知节点 404、退役节点 409 |
POST | /fleet/mitigations/prefix-unblock | 撤销前缀拒收(缺省清全部节点),幂等 |
POST | /fleet/mitigations/peer-disable | 断开对端:{asn} 的全部 eBGP 会话置 disabled(BIRD 摘协议,WG 隧道保留),幂等;无该 AS 会话 404 |
POST | /fleet/mitigations/peer-enable | 恢复对端全部会话,幂等 |
blocked_prefixes需要 fleet agent ≥ 1.0.187:更早的 agent 对 DesiredState 的严格校验不识别该字段。
自动对等供给策略
源码:apps/control-server/app/api/v1/admin/autopeering.py。自动对等免审批——运维控制点是供给策略;已落成对等的处置(禁用、拆除)走上面的标准 Peering 管理 API。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /autopeering/nodes | 全部节点策略 + 已落成数 |
PUT | /autopeering/nodes/{node_id} | upsert 供给策略(全量替换,未给的字段回默认) |
PATCH | /autopeering/nodes/{node_id} | 部分更新,只改给出的字段 |
GET | /autopeering/requests?status= | 请求清单(审计与排障视图,可按状态过滤) |
策略字段:{ enabled, endpoint_host?, max_sessions?, notes?, region?, country_code?, description?, bandwidth_mbps?, monthly_quota_gb?, notice? }。
enabled=false后该节点从用户面清单消失、新提交404,已落成的对等不受影响。前端的「该节点是否接受自动对等」开关用PATCH {"enabled": false}即可。max_sessions是可选的额外人工上限;null表示只受端口池约束,可用连接数由free_ports派生。endpoint_host为null的节点只能由本网向外拨出(NAT 后或跨境专线形态),不出现在可拨入清单里。- 其余字段是用户面列表页的展示元数据。
运维视角见 自动对等运维。
fleet 收权运维
源码:apps/control-server/app/api/v1/admin/fleet.py。密钥与端口收权批留下的 fleet 级运维端点。两个迁移端点幂等、支持 dry_run;非 dry-run 时对全部受影响节点在同一事务内 materialize,提交后逐节点广播。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /fleet/port-pools | 端口池视图:{ "pools": { "<scope>": { "start": …, "end": … } } }。现役段位为 external 51800–51830、internal 52800–52820 |
POST | /fleet/normalize-peer-names | 存量对等命名规范化 + 空壳 Peering 清理(需 registry 副本可用) |
POST | /fleet/migrate-keys | 存量密钥统一:典范钥入库,spec 改由 materializer 无条件注入 |
POST | /fleet/migrate-internal-ports | 内部链路端口换段 + base_template 内残留段位清除 |
三个 POST 的请求体为 { "dry_run": true }(默认 true)。响应:{ dry_run, changed_nodes[], generations{node: generation}, details }。
执行顺序:先对各节点跑 peering backfill 修 orphan 关联 → migrate-keys → migrate-internal-ports。设计见 fleet 供给收权。