From fec131c6ee3b7c940d819c50fe0e7d6b45b0f151 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 26 Mar 2026 20:52:33 +0800 Subject: [PATCH 1/6] [rest]: fix bug Resolves: ZCF-1365 Change-Id: I696f6a6a6e6d7867746e70736d6d646861686166 --- .../rest/webhook/WebhookCallbackClient.java | 17 + .../pages/networkResource/ZnsIntegration.adoc | 614 +++++++++++++++--- .../SdnControllerManagerImpl.java | 11 +- 3 files changed, 564 insertions(+), 78 deletions(-) diff --git a/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java b/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java index 2cacfef27b..f719bb8445 100644 --- a/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java +++ b/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java @@ -131,6 +131,15 @@ public String getCallbackUrl() { return callbackUrl; } + /** + * Override the callback URL. Use this when the callback is handled by a + * dedicated HTTP endpoint (e.g. a Spring Controller) rather than the + * sendCommand channel. + */ + public void setCallbackUrl(String callbackUrl) { + this.callbackUrl = callbackUrl; + } + /** * @return the protocol adapter */ @@ -138,6 +147,14 @@ public WebhookProtocol getProtocol() { return protocol; } + /** + * Deliver a callback that was received outside the sendCommand channel + * (e.g. from a dedicated Spring Controller endpoint for external systems). + */ + public void deliverCallback(T cmd) { + onCallback(cmd); + } + /** * Callback handler invoked by the RESTFacade sendCommand channel. */ diff --git a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc index da7bb2e737..b536d69410 100644 --- a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -15,7 +15,7 @@ Cloud L2 和 ZNS segment 之间的资源如何一一对应? [source,go] ---- type Cms struct { - CmsUuid string + CmsUuid string Type string ### cloud/zsv/zaku/zns IP string ### cloud mn vip Role string ###owner, user @@ -52,52 +52,166 @@ ZNS API 定义需要包含 Cms 数据: 不是所有资源对象都需要保持 cms 信息,但是由 cms 创建的资源,或者需要按 cms 查询的资源,都需要保存 cms 信息。 一个资源可能有多个 cms 信息,表示资源可在多个 cms 之间共享。 +=== UUID 格式约定 + +ZNS 使用带连字符的 UUID 格式(`550e8400-e29b-41d4-a716-446655440000`),Cloud 使用不带连字符的紧凑 UUID 格式(`550e8400e29b41d4a716446655440000`)。 + +Cloud 在调用 ZNS API 时需要将 Cloud UUID 转换为 ZNS UUID 格式,在接收 ZNS 响应时需要将 ZNS UUID 转换回 Cloud 格式。 + +== Cloud 与 ZNS 通信协议 + +=== 传输层 + +Cloud 通过 HTTP REST API 与 ZNS 通信。所有请求/响应体均为 JSON 格式。 + +全局配置参数: + +[cols="2,2,2"] +|=== +|配置项 |默认值 |说明 + +|`zns.controller.scheme` +|`http` +|HTTP 或 HTTPS + +|`zns.controller.port` +|`7278` +|ZNS API 端口 + +|`zns.controller.timeout` +|`300000`(5分钟) +|请求超时时间(毫秒) + +|=== + +=== 异步回调模式 + +所有写操作(POST/PATCH/DELETE)采用异步回调模式: + +.... +Cloud ZNS + │ POST/PATCH/DELETE + headers │ + │ x-web-hook: │ + │ x-job-uuid: │ + │─────────────────────────────────────────►│ + │ ◄── 202 Accepted │ (ZNS 立即返回) + │ │ + │ (ZNS 异步处理完成后回调 Cloud) │ + │ ◄── POST │ + │ { taskUuid, success, status, data } │ + │ │ + │ Cloud 根据 taskUuid 匹配到 │ + │ 对应的等待中的调用并完成 │ +.... + +所有读操作(GET)为同步调用,直接返回 200 + JSON body。 + +=== 请求串行化 + +对同一个 ZNS IP 的所有 API 调用通过任务链串行执行,避免并发操作导致的冲突。 == ZNS SDN控制器 ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`SugonSdnController`、`OvnController`、`HuaweiIMasterSdnController` 等实现。 -新定义 `ZnsControllerVO`,继承 `SdnControllerVO`,不添加新的字段: +新定义 `ZnsControllerVO`,继承 `SdnControllerVO`: * vendorType:ZNS -* vendorVersion:1.0 +* vendorVersion:用于记录当前连接的 ZNS 版本(详见 <> 章节) +* transportZones:关联的 `ZnsTransportZoneVO` 列表(一对多) [NOTE] 必须先在 ZNS 完成添加 Computer Manager 的操作,然后在 Cloud 侧创建对应的 SdnController。 ZNS SDN Controller 保持 SystemTags:`computerManagerUuid::xxxx`,这里的 xxxx 是 ZNS 创建的 Computer Manager UUID。 后续 Cloud 调用 ZNS API 创建 segment、segment port 时,Cloud 会根据 computerManagerUuid 组装 cms 信息。 +Cloud 还通过 SystemTag 记录以下映射关系: + +* `znsSegmentUuid::{segmentUuid}` 在 `L2NetworkVO` 上 — 映射 Cloud L2 → ZNS segment +* `znsSegmentPortUuid::{portUuid}` 在 `VmNicVO` 上 — 映射 Cloud NIC → ZNS segment port + === 创建SDN控制器 -* 根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于当前 Computer Manager 管理的 cluster,且 `managementIp != null`: +==== 1. 验证 Computer Manager + +根据 systemTag 中提供的 `computerManagerUuid`,调用 `GET /zns/api/v1/fabric/compute-managers/{uuid}` 验证 Computer Manager 在 ZNS 上存在且可用。 + +==== 2. 同步 Compute Collections(集群映射) + +根据 `GET /zns/api/v1/fabric/compute-collections` 获取 compute collection 列表,过滤出属于当前 Computer Manager 的条目。 + +ZNS 侧直接使用 Cloud 的 cluster UUID,因此可以直接按 UUID 匹配 Cloud 侧的 `ClusterVO`,无需 name 匹配。 + +构建以下映射关系: +* `znsClusterUuids`:属于该 Computer Manager 的 ZNS cluster UUID 集合 +* `znsClusterToZstackCluster`:ZNS cluster UUID → Cloud cluster UUID + +==== 3. 获取 Discovered Nodes(主机发现) + +根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于步骤 2 中确定的 cluster 集合,且 `managementIp != null`: + ** `HostData.managementIp`:匹配 Cloud `HostVO.managementIp`,找到对应 `HostVO.uuid` -** `HostData.clusterId`:即 Cloud `ClusterVO.uuid`(ZNS 侧直接使用 Cloud 的 cluster UUID,无需 name 匹配) +** `HostData.clusterId`:即 Cloud `ClusterVO.uuid` ** `HostData.transportNodeProfileId`:用于后续推导 vSwitchType 和建立 transport zone → cluster 的反向映射 [NOTE] ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` 相同。 -* 根据 `HostData.transportNodeProfileId` 调用 `GET /zns/api/v1/fabric/transport-node-profiles/{uuid}` 获取 `TransportNodeProfileData`,取 `hostSwitchProfiles[0]` 调用 `GET /zns/api/v1/fabric/host-switch-profiles/{uuid}` 获取 `HostSwitchProfileData`: +==== 4. 推导 vSwitchType 并创建 Host Ref + +根据 `HostData.transportNodeProfileId` 调用 `GET /zns/api/v1/fabric/transport-node-profiles/{uuid}` 获取 `TransportNodeProfileData`,取 `hostSwitchProfiles[0]` 调用 `GET /zns/api/v1/fabric/host-switch-profiles/{uuid}` 获取 `HostSwitchProfileData`: + ** `HostSwitchProfileData.type`:枚举值 `dpdk` 或 `kernel`,用于推导 `SdnControllerHostRefVO.vSwitchType` ** `HostSwitchProfileData.transportZoneIds`:关联的 transport zone UUID 列表,建立反向缓存 `transportZoneUuid → Set` -* 调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,并缓存到当前 `ZnsSdnControllerVO` 关联的数据中。建议新增 `ZnsTransportZone` 数据表,主要字段如下: -+ +[NOTE] +OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel`)和 `switchType`(字符串描述,如 `"OVS"`)两个字段。 +推导 `vSwitchType` 使用的是 `type` 枚举字段。 + +根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则: + +[cols="2,2"] +|=== +|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType + +|`dpdk` +|`OvsDpdk` + +|`kernel` +|`OvsKernel` + +|其它未知值,或者 profile 查询失败 +|`ZNS` + +|=== + +==== 5. 同步 Transport Zones + +调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,持久化到 `ZnsTransportZoneVO`。主要字段如下: + [cols="2,3,2"] |=== |字段 |来源 |说明 +|`uuid` +|transport zone UUID(转换为 Cloud 格式) +|主键 + |`isDefault` -|transport zone 返回字段 +|每种 type 的第一个 transport zone 设为 true |是否为默认 transport zone +|`name` +|transport zone 返回字段 +|transport zone 名称 + |`description` |transport zone 返回字段 |描述信息 -|`name` +|`type` |transport zone 返回字段 -|transport zone 名称 +|类型,典型值为 `vlan` 或 `overlay` |`physicalNetwork` |transport zone 返回字段 @@ -111,10 +225,6 @@ ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` |transport zone 返回字段 |标签信息 -|`type` -|transport zone 返回字段 -|类型,典型值为 `vlan` 或 `overlay` - |`znsSdnControllerUuid` |当前 `ZnsSdnControllerVO.uuid` |外键,关联到所属 ZNS SDN Controller @@ -122,35 +232,17 @@ ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` |=== [NOTE] -这里的缓存不只是 `transportZoneUuid → Set` 反向索引,还包括 transport zone 自身的元数据。 -前者用于根据 host/profile 关系反查 cluster,后者用于后续创建 Cloud L2Network 时选择默认 transport zone,并为 `segment.transport_zone_uuid` 的解释提供基础数据。 - -[NOTE] -OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel`)和 `switchType`(字符串描述,如 `"OVS"`)两个字段。 -推导 `vSwitchType` 使用的是 `type` 枚举字段。 - -* 根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则: -+ -[cols="2,2"] -|=== -|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType +`isDefault` 的设置规则:每种 type(vlan、overlay)的第一个 transport zone 自动设为默认。重连时保留已有的默认设置。 -|`dpdk` -|`OvsDpdk` +==== 6. 同步 Segments 并创建 L2/L3 -|`kernel` -|`OvsKernel` +根据 computerManagerUuid 从 ZNS 获取 segments 列表(带 cms 过滤)。 -|其它未知值,或者 profile 查询失败 -|`ZNS` +对每个 segment: -|=== -* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,同时持有两类 transport zone 缓存: -** transport zone 元数据缓存:保存到建议新增的 `ZnsTransportZone` -** `transportZoneUuid → Set` 反向缓存:用于 segment 和 cluster 的关联 -* 根据 computerManagerUuid 从 ZNS 获取 segments 列表。 -** 根据 segment 信息创建 L2Network、L3Network -** 根据 segment.ipam 信息创建 IpRange +* 根据 segment 信息创建 L2Network、L3Network +* 根据 segment.ipam 信息创建 IpRange +* 通过 PATCH 将 Cloud L2 UUID 回写到 ZNS segment 的 cms.cms_resource_uuid 中 ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: @@ -178,10 +270,11 @@ ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: 补充说明: * `L2Network.vSwitchType` 固定写入 `ZNS` +* `L2Network.physicalInterface` 固定写入空字符串 * `L2Network.virtualNetworkId` 取 `segment.virtual_network_id`(Geneve/VLAN) * `L3Network.category` 当前初始化逻辑固定为 `Private` -5. 根据 `segment.transport_zone_uuid` 查询前面缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 +根据 `segment.transport_zone_uuid` 查询步骤 4 缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 `L2NetworkClusterRefVO` 的映射关系如下: @@ -212,7 +305,7 @@ ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: ==== 1. 刷新 SdnControllerHostRefVO(upsert) -重连仍需重新扫描 host,以处理新加入的主机或 vSwitchType 发生变化的主机。 +重连仍需重新扫描 host(包括 compute collections → discovered nodes → derive vSwitchType),以处理新加入的主机或 vSwitchType 发生变化的主机。 映射关系与创建时完全相同(`HostData.managementIp` → `HostVO`,`HostSwitchProfileData.type` → `vSwitchType`), 区别仅在于写库操作改为 upsert: @@ -234,30 +327,34 @@ ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: [NOTE] 创建时只做 INSERT;重连时使用 upsert,可处理主机上下线及 vSwitchType 变更的情况。 -==== 2. Segment 协调(以 Cloud 为基准) +==== 2. 刷新 Transport Zones + +调用 `GET /zns/api/v1/fabric/transport-zones` 重新拉取 transport zone 列表,对 `ZnsTransportZoneVO` 做 upsert,同时删除 ZNS 侧已不存在的旧记录。重连时保留已有的 `isDefault` 设置。 + +==== 3. Segment 协调(以 Cloud 为基准) 重连以 Cloud 数据库中所有 `vSwitchType = ZNS` 的 `L2NetworkVO` 为基准,与 ZNS 侧属于本 Computer Manager 的 segment 做三路对比。 -ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关联。 +ZNS 侧 segment 通过 cms 元数据中的 `cms_resource_uuid` 与 Cloud L2 关联。 [cols="2,2,3"] |=== |Cloud 侧 L2NetworkVO |ZNS 侧 segment |操作 -|不存在(`l2Uuid` 指向已删除 L2,或 segment 无 `l2Uuid`) +|不存在(`cms_resource_uuid` 指向已删除 L2,或 segment 无 `cms_resource_uuid`) |存在 |调用 `DELETE /zns/api/v1/segments` 删除孤儿 segment |存在 |不存在 -|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3 信息 +|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3/IpRange 信息 |存在 -|存在但参数不一致(如 CIDR) +|存在但参数不一致(名称、描述、CIDR 等) |调用 `PATCH /zns/api/v1/segments/{uuid}` 更新 |存在 |存在且参数一致 -|无操作 +|无操作(仅同步 systemTag 确保 segment UUID 映射正确) |=== @@ -265,10 +362,10 @@ ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关 重连 *不会* 根据 ZNS segment 在 Cloud 侧创建新的 L2Network / L3Network / IpRange / `L2NetworkClusterRefVO`。 如果 ZNS 存在但 Cloud 不存在,视为孤儿 segment 并删除(与创建阶段的单向导入方向相反)。 -==== 3. Segment Port 协调 +==== 4. Segment Port 协调 完成 segment 协调后,对每个已与 Cloud L2 匹配的 segment,逐一协调其 port。 -Port 通过 cms 元数据中的 `ExternalIds.vmNicUuid` 与 Cloud `VmNicVO` 关联。 +Port 通过 cms 元数据中的 `cms_resource_uuid` 与 Cloud `VmNicVO` 关联。 [cols="2,2,3"] |=== @@ -283,13 +380,30 @@ Port 通过 cms 元数据中的 `ExternalIds.vmNicUuid` 与 Cloud `VmNicVO` 关 |调用 `DELETE /zns/api/v1/segments/{uuid}/ports` 删除孤儿 port |两侧均存在 -|(当前实现不做参数比对更新) +|(当前实现不做参数比对更新,仅同步 systemTag) |无操作 |=== +=== 心跳探活(Ping) + +Cloud 定期发送 `SdnControllerPingMsg`,通过调用 `GET /zns/api/v1/fabric/compute-managers/{uuid}` 验证 Computer Manager 连接是否正常。 + +* 验证成功 → 控制器保持 Connected 状态 +* 验证失败 → 控制器状态变为 Disconnected + === 删除SDN控制器 -删除 ZNS SDN Controller 时,会级联删除 ZNS 侧的 Computer Manager 和 Segment、Segment Port 等资源。Cloud 侧的 L2Network、L3Network、VmNic 等也会一起删除。 + +删除 ZNS SDN Controller 时的清理流程: + +. 根据 computerManagerUuid 从 ZNS 查询属于本 Controller 的所有 segments +. 批量调用 `DELETE /zns/api/v1/segments` 删除这些 segments(force = true),连带删除 port +. 删除 Cloud 本地的 `ZnsTransportZoneVO` 记录 +. 调用 `DELETE /zns/api/v1/fabric/compute-managers/{uuid}` 删除 Computer Manager + +[NOTE] +删除 segments 和 compute manager 过程中如果 ZNS 调用失败,仅打印告警日志,不阻断删除流程。 +Cloud 侧的 L2Network、L3Network、IpRange、VmNic 等资源由 `SdnControllerVO` 级联删除机制清理。 == L2Network @@ -315,7 +429,7 @@ ZNS L2Network 的类型定义: |ZNS(固定值,不区分 kernel 和 dpdk) |physicalInterface -|null +|空字符串 |virtualNetworkId |Vlan Id 或 Geneve Id @@ -324,9 +438,7 @@ ZNS L2Network 的类型定义: === 创建 L2Network -处理逻辑类似 OVN Controller,但调用 ZNS API 创建 segment。 - -创建 ZNS 二层网络时,Cloud 需要先根据 L2 类型从已缓存的 transport zone 中选择默认 transport zone: +创建 ZNS 二层网络时,Cloud 根据 L2 类型从已缓存的 `ZnsTransportZoneVO` 中选择默认 transport zone(`isDefault = true`): [cols="2,2,3"] |=== @@ -346,6 +458,13 @@ ZNS L2Network 的类型定义: |=== +调用 `POST /zns/api/v1/segments` 创建 segment,请求体包含:name、description、transport_type(转为大写)、transport_zone_uuid、virtual_network_id、cms 信息。 + +创建成功后,将 ZNS 返回的 segment UUID 通过 systemTag 记录到 L2NetworkVO 上。 + +[NOTE] +创建 L2Network 时,Cloud 会自动将 `vSwitchType` 设为 `ZNS`,`physicalInterface` 设为空字符串。这通过 API 拦截器(`ZnsApiInterceptor`)在 `APICreateL2NetworkMsg` 处理前自动完成。 + [NOTE] ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。 因此 Cloud 创建的 L2Network 在 OVN 侧默认可在该 transport zone 覆盖的全部物理机上使用,而不是由 OVN 提供 cluster 级隔离。 @@ -354,15 +473,19 @@ ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。 Cloud 侧会额外施加一层调度约束:只有二层网络实际加载了某个 cluster 后,该 cluster 中的物理机才允许用于创建虚拟机。 也就是说,transport zone 决定的是底层网络可达范围,`L2NetworkClusterRefVO` 决定的是 Cloud 侧可调度范围。 -==== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg +=== 删除 L2Network -处理逻辑类似 OVN Controller,根据 ZNS Host 和 transport zone 的关系,把 ZNS segment 关联到 transport zone。 +调用 `DELETE /zns/api/v1/segments` 删除对应的 ZNS segment(force = true),并清理 systemTag。 +如果 ZNS 侧删除失败(例如 segment 已不存在),仅打印告警日志,不阻断 Cloud 侧的删除流程。 -==== APIChangeL2NetworkVlanIdMsg +=== APIChangeL2NetworkVlanIdMsg -* L2GeneveNetwork 类型不支持修改 VlanId,需要在 `L2NetworkApiInterceptor` 中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` +* L2GeneveNetwork 类型不支持修改 VlanId,需要在 API 拦截器中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` * L2VlanNetwork、L2NoVlanNetwork 类型支持 -* 仅需要修改 L2NetworkVO 数据库,不需要下发到物理机,需要调用修改 ZNS segment API + +=== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg + +当前实现为空操作(no-op)。ZNS 通过 transport zone 管理网络覆盖范围,attach/detach cluster 仅影响 Cloud 侧的调度约束。 == L3Network @@ -392,11 +515,18 @@ ZNS L3 的定义: ZNS L3Network 不添加网络服务。 -=== SetVmStaticIp / ChangeVmIp 操作 +=== 创建/删除 IpRange + +创建 IpRange 时,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `gateway_address` 设为 IpRange 的 `networkCidr`,同步 CIDR 信息到 ZNS。 + +删除最后一个 IpRange 时,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `gateway_address` 设为空字符串,清除 ZNS 侧的 CIDR 信息。 + +[NOTE] +创建/删除 L3Network 本身不触发 ZNS API 调用,仅 IpRange 的变化才同步到 ZNS。 -由于 ZNS 网络的 IP 由 ZNS 管理,`APISetVmStaticIpMsg` 和 `APIChangeVmIpMsg` 需要特殊处理: +=== MTU 同步 -在 `VmInstanceApiInterceptor` 中增加校验:如果目标 L3Network 关联的 L2Network 的 vSwitchType 为 ZNS,需要将用户指定的 IP 传给 ZNS segment port API 进行更新,而非走 Cloud 侧的 IP 分配流程。 +当用户修改 L3Network MTU 时,如果该 L3 属于 ZNS 网络,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `mtu` 字段同步到 ZNS segment。 == VmNic @@ -415,10 +545,11 @@ VmNicType 的值有:VNIC、VF、`dpdkvhostuserclient`。ZNS 可能是 dpdk 模 ZNS 网络创建过程: -1. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType -2. 调用 ZNS 创建 segment port API,返回 ip/掩码/网关、ip6/前缀/网关 -3. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 -4. 根据获取的参数创建 `VmNicVO`、`UsedIpVO` +. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType +. 调用 ZNS 创建 segment port API(`POST /zns/api/v1/segments/{uuid}/ports`),请求体包含:name、mac、ip、vm_uuid、cms 信息。ZNS 返回分配的 IP 地址 +. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 +. 根据获取的参数创建/更新 `VmNicVO`、`UsedIpVO` +. 将 ZNS 返回的 port UUID 通过 systemTag 记录到 VmNicVO 上 === 网卡删除过程 @@ -427,23 +558,354 @@ ZNS 网络创建过程: 两个 Flow 中都需要: -1. 调用 ZNS 删除 segment port API -2. 删除 `VmNicVO`、`UsedIpVO` +. 调用 ZNS 删除 segment port API(`DELETE /zns/api/v1/segments/{uuid}/ports`) +. 清理 systemTag +. 删除 `VmNicVO`、`UsedIpVO` -=== DPDK 网卡的特殊处理 +[NOTE] +如果 ZNS 侧删除失败,仅打印告警日志,不阻断 Cloud 侧的删除流程。 -由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 -这个逻辑与 OVN DPDK 虚拟网卡一致。 +=== IP 变更(VmIpChanged) + +当虚拟机 IP 发生变化时(`SetVmStaticIp`、`ChangeVmIp` 等操作),Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}/ports/{portUuid}` 更新 ZNS 侧的 port IP。 === ChangeVmNicNetwork(换网操作) `APIChangeVmNicNetworkMsg` 涉及 detach 旧网络 + attach 新网络: -* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络 -* 从 ZNS 网络变换成 ZNS 网络的场景,需要调用 ZNS API 删除旧的 segment port,调用 API 创建新的 segment port,并更新 `VmNicVO`/`UsedIpVO` 等相关数据对象 +* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络(API 拦截器阻断) +* 不支持在不同 ZNS 控制器之间换网(API 拦截器阻断) +* 从 ZNS 网络变换成同一控制器下的 ZNS 网络: +** `beforeUpdateNic`:记录旧 port 上下文(znsIp、segmentUuid、portUuid) +** `afterUpdateNic`:先删除旧 segment port,再在新 segment 上创建新 port + +=== DPDK 网卡的特殊处理 + +由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 +这个逻辑与 OVN DPDK 虚拟网卡一致。 === FilterAttachableL3NetworkExtensionPoint -* `APIGetVmAttachableL3NetworkMsg` 必须能获取到 ZNS L3 网络。 +获取虚拟机可挂载的 L3 网络列表时,ZNS 网络有额外的过滤规则: + +* 如果虚拟机尚未挂载 ZNS 网络:ZNS 类型的 L3 仅当虚拟机所在物理机被该 ZNS 控制器管理时才可挂载 +* 如果虚拟机已挂载 ZNS 网络:只允许挂载同一 ZNS 控制器下的 L3 或非 ZNS 的 L3 + +== API 拦截器 + +`ZnsApiInterceptor` 在以下场景进行拦截: + +* `APICreateL2NetworkMsg`:如果 systemTag 指定了 ZNS SDN 控制器,自动设置 `vSwitchType = ZNS`、`physicalInterface = ""` +* `APIChangeL2NetworkVlanIdMsg`:禁止修改 L2GeneveNetwork 的 VLAN ID +* `APIChangeVmNicNetworkMsg`:禁止 ZNS 与非 ZNS 网络之间换网,禁止不同 ZNS 控制器之间换网 +* `APISdnControllerAddHostMsg` / `RemoveHostMsg` / `ChangeHostMsg`:禁止对 ZNS 控制器手动管理主机(由 ZNS 自动管理) + +[[api-version]] +== API 版本兼容性 + +=== 问题背景 + +Cloud 和 ZNS 是独立部署的两个组件,可能出现版本不一致: + +* **场景A**:Cloud 升级了,ZNS 没升级 — Cloud 发出 ZNS 不认识的 API 格式 +* **场景B**:ZNS 升级了,Cloud 没升级 — Cloud 用旧格式调新 API,参数缺失或语义变化 +* **场景C**:运行中 ZNS 被升级/降级 — Cloud 缓存的版本信息过期 + +=== 设计思路 + +.... +1. 每个 API 独立版本 + 不是给 ZNS 一个整体版本号,而是给每个 API 独立的版本号。 + ZNS 一年 2-3 个版本,但不是每个 API 都会变。 + +2. ZNS 是自己能力的真相源 + 每个 API 通过 HTTP OPTIONS 方法声明自己支持哪些版本。 + ZNS 各 API 各管各的版本,不集中到一个大接口里。 + +3. Cloud 通过 HTTP Header 声明自己发送的版本 + 每次请求带 X-Api-Version header,ZNS 可据此选择处理逻辑。 +.... + +=== 版本模型 + +==== 每个 API 有独立版本 + +.... +API 路径 + 方法 Cloud 发送的版本 ZNS 支持的版本 +────────────────────────────────────── ──────────────── ─────────────── +POST /zns/api/v1/segments 1.1 [1.0, 1.1] +PATCH /zns/api/v1/segments/{uuid} 1.1 [1.0, 1.1, 1.2] +DELETE /zns/api/v1/segments 1.0 [1.0] +GET /zns/api/v1/segments 1.1 [1.0, 1.1] +POST /zns/api/v1/segments/{id}/ports 1.0 [1.0, 1.1] +PATCH /zns/api/v1/segments/{id}/ports 1.1 [1.0, 1.1] +DELETE /zns/api/v1/segments/{id}/ports 1.0 [1.0] +.... + +* *Cloud 侧*:每个 API 有一个确定的版本,表示"我发出去的请求是什么格式" +* *ZNS 侧*:每个 API 支持一组版本,表示"我能理解哪些格式" +* *兼容条件*:Cloud 发送的版本 ∈ ZNS 支持的版本集合 + +==== API 版本变更举例 + +.... +ZNS v1.0 发布: + PATCH /segments 支持 [1.0] + POST /ports 支持 [1.0] + +ZNS v1.1 发布(PATCH segment 新增 mtu 字段): + PATCH /segments 支持 [1.0, 1.1] ← 新增 1.1,但仍兼容 1.0 + POST /ports 支持 [1.0] ← 没变 + +ZNS v1.2 发布(PATCH segment 删除了某个旧字段): + PATCH /segments 支持 [1.1, 1.2] ← 不再支持 1.0 + POST /ports 支持 [1.0, 1.1] ← 新增 1.1 +.... + +=== 版本查询:HTTP OPTIONS 方法 + +每个 API 路径通过标准的 HTTP OPTIONS 方法返回自己支持的版本。 + +.... +Cloud ZNS + │ │ + │ OPTIONS /zns/api/v1/segments │ + │───────────────────────────────────────────────────►│ + │ │ + │ ◄── 204 No Content │ + │ Headers: │ + │ Allow: GET, POST, DELETE │ ← 标准头 + │ X-Api-Versions: POST=1.0,1.1; │ ← 自定义头 + │ DELETE=1.0; │ + │ GET=1.0,1.1 │ + │ │ + │ OPTIONS /zns/api/v1/segments/{uuid} │ + │───────────────────────────────────────────────────►│ + │ │ + │ ◄── 204 No Content │ + │ Headers: │ + │ Allow: GET, PATCH │ + │ X-Api-Versions: PATCH=1.0,1.1,1.2; │ + │ GET=1.0,1.1 │ + │ │ +.... + +`X-Api-Versions` Header 格式: +.... +X-Api-Versions: {METHOD1}={ver1},{ver2};{METHOD2}={ver1},{ver2} + +示例:POST=1.0,1.1;DELETE=1.0;GET=1.0,1.1 +.... + +=== 版本声明:X-Api-Version 请求头 + +Cloud 每次发送 API 请求时,通过 Header 声明自己发送的是哪个版本的格式: + +.... +Cloud ZNS + │ │ + │ PATCH /zns/api/v1/segments/{uuid} │ + │ Headers: │ + │ X-Api-Version: 1.1 ← 声明版本 │ + │ Content-Type: application/json │ + │ x-web-hook: ... │ + │ x-job-uuid: ... │ + │ Body: │ + │ { "name": "...", "mtu": 9000 } ← 1.1 格式 │ + │───────────────────────────────────────────────────►│ + │ │ + │ ZNS 收到请求: │ + │ 1. 检查 X-Api-Version = 1.1 │ + │ 2. PATCH segments 支持 1.1? → 用 1.1 处理逻辑 │ + │ 不支持? → 返回 400 │ + │ │ +.... + +==== ZNS 处理不支持的版本 + +.... +ZNS 返回: + HTTP 400 Bad Request + { + "error": "unsupported_api_version", + "message": "PATCH /segments does not support version 1.3", + "supported_versions": ["1.0", "1.1", "1.2"] + } +.... + +==== ZNS 处理没有版本 Header 的请求(兼容旧版 Cloud) + +没有 `X-Api-Version` header 时,ZNS 按该 API 支持的最低版本处理,确保旧版 Cloud 仍能正常使用。 + +=== 版本检查时机 + +.... +核心原则: + 不在每次 API 调用前询问 ZNS(会导致每次操作延迟翻倍) + 而是在 3 个时机批量拉取版本信息,缓存在内存中 + + ZNS 版本一年才变 2-3 次,变版本必然有运维动作(升级), + 升级后通常会触发 reconnect。Ping 间隔内版本突然变化的概率极低。 +.... + +[cols="2,3,3"] +|=== +|时机 |触发条件 |行为 + +|添加控制器(preInit) +|用户首次添加 ZNS 控制器到 Cloud +|对每个 API 路径发 OPTIONS → 检查兼容性 → 缓存。不兼容则拒绝添加 + +|重连控制器(reconnect) +|管理员手动触发重连,或网络恢复后自动重连 +|清除旧缓存 → 重新发 OPTIONS → 检查 → 缓存。不兼容则重连失败 + +|心跳探活(ping) +|Cloud 定时 Ping ZNS +|对每个 API 路径发 OPTIONS → 与缓存对比。版本变化 → 刷新缓存 → 重新检查。不兼容 → Ping 失败 → 标记 Disconnected + +|=== + +=== 双层防御 + +版本兼容性在两个层面保证: + +.... +第 1 层: Cloud 提前检查(发请求之前) + Cloud 在 preInit/reconnect/ping 时通过 OPTIONS 拉取版本 + 缓存到内存,每次发请求前本地查缓存 + 不兼容 → 不发请求,直接报错或降级 + 作用: 避免发出注定失败的请求 + +第 2 层: ZNS 兜底校验(收到请求时) + ZNS 收到请求后检查 X-Api-Version header + 不支持 → 返回 400 + supported_versions + 作用: 即使 Cloud 缓存过期,ZNS 也能拒绝不兼容的请求 +.... + +=== 不兼容时的处理策略 + +不同 API 不兼容时,处理方式不同: + +[cols="3,1,3"] +|=== +|API (路径 + 方法) |重要性 |不兼容时行为 + +|POST /segments(创建网络) +|关键 +|阻断: 拒绝添加/重连控制器 + +|DELETE /segments(删除网络) +|关键 +|阻断 + +|POST /segments/{id}/ports(创建端口) +|关键 +|阻断 + +|DELETE /segments/{id}/ports(删除端口) +|关键 +|阻断 + +|GET /segments(列表,对账用) +|关键 +|阻断 + +|GET /segments/{id}/ports(列表,对账用) +|关键 +|阻断 + +|PATCH /segments/{uuid}(更新 mtu 等) +|非关键 +|降级: 跳过 + 打印告警日志 + +|PATCH /segments/{id}/ports/{id}(更新端口) +|非关键 +|降级: 跳过 + 打印告警日志 + +|=== + +=== 兼容旧版 + +==== Cloud 遇到旧版 ZNS(不支持 OPTIONS 版本响应) + +如果 OPTIONS 返回 404 或 204 但没有 `X-Api-Versions` header,Cloud 假设所有 API 只支持 `["1.0"]`。 + +==== ZNS 遇到旧版 Cloud(不带 X-Api-Version header) + +没有 `X-Api-Version` header 时,ZNS 按最低支持版本处理。 + +=== 错误信息设计 + +==== Cloud 新,ZNS 旧 + +.... +ZNS controller [192.168.1.10] does not support the following APIs +required by this Cloud version: + - PATCH /segments: Cloud sends v1.1, ZNS supports [1.0] + - POST /ports: Cloud sends v1.1, ZNS supports [1.0] +Please upgrade ZNS to a compatible version. +.... + +==== ZNS 新,Cloud 旧 + +.... +This Cloud version is not compatible with ZNS [192.168.1.10]: + - PATCH /segments: Cloud sends v1.0, ZNS supports [1.1, 1.2] (v1.0 dropped) +Please upgrade Cloud to a compatible version. +.... + +==== 非关键 API 降级 + +.... +WARN: ZNS [192.168.1.10] does not support PATCH /segments v1.1 +(ZNS supports [1.0]). MTU sync will be skipped. +This does not affect core network operations. Upgrade ZNS to enable MTU sync. +.... + +=== 版本维护规范 + +==== 什么时候需要升版本 + +需要升版本: + +* 请求体新增必填字段 +* 请求体删除字段 +* 字段类型变化(string → int) +* 字段语义变化(单位从 MB 变成 KB) +* 响应体结构变化(影响 Cloud 解析) + +不需要升版本: + +* 请求体新增可选字段(ZNS 忽略未知字段即可) +* 纯内部实现变化,接口不变 +* 性能优化,接口不变 + +==== ZNS 的向后兼容策略 + +建议每个 API 至少兼容前 2 个版本。 + +.... +示例: PATCH /segments + v1.2 发布时 → supported: [1.0, 1.1, 1.2] (兼容 3 个版本) + v1.3 发布时 → supported: [1.1, 1.2, 1.3] (下线 1.0) + +这样可以给 Cloud 升级留出足够的时间窗口。 +.... + +=== 双方职责总结 + +==== ZNS 侧 + +. 每个 API 路径实现 OPTIONS 方法,返回 `Allow` + `X-Api-Versions` header +. 收到请求时检查 `X-Api-Version` header:支持则用对应版本处理;不支持则返回 400;缺失则按最低版本处理 +. 每次 API 格式变化时在该 API 的 OPTIONS 响应中新增版本号,保留旧版本兼容 +. 新增 API 时实现 OPTIONS,初始版本为 `1.0` +==== Cloud 侧 +. 维护 `CLOUD_API_VERSIONS` 表,记录当前发出的每个 API 的格式版本 +. 在 preInit、reconnect、ping 三个时机通过 OPTIONS 拉取版本并缓存 +. 每次发请求带上 `X-Api-Version` header +. 每次修改 API 请求体格式时同步更新版本号 +. 标记每个 API 的重要性(关键/非关键),决定不兼容时是阻断还是降级 diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java index 7a1967e244..daa78b0f58 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java @@ -271,8 +271,15 @@ private void handle(APIAddSdnControllerMsg msg) { @Override public void run(MessageReply reply) { if (reply.isSuccess()) { - tagMgr.createTagsFromAPICreateMessage(msg, vo.getUuid(), SdnControllerVO.class.getSimpleName()); - event.setInventory(SdnControllerInventory.valueOf(dbf.findByUuid(vo.getUuid(), SdnControllerVO.class))); + try { + tagMgr.createTagsFromAPICreateMessage(msg, vo.getUuid(), SdnControllerVO.class.getSimpleName()); + event.setInventory(SdnControllerInventory.valueOf(dbf.findByUuid(vo.getUuid(), SdnControllerVO.class))); + } catch (Exception e) { + logger.warn(String.format("failed to load SdnControllerVO[uuid:%s] after init: %s", + vo.getUuid(), e.getMessage()), e); + event.setError(operr("failed to load SdnController[uuid:%s] after successful init: %s", + vo.getUuid(), e.getMessage())); + } } else { event.setError(reply.getError()); } From bfc85ab90701ce24b81e2beb84f6b4762562940c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 27 Mar 2026 18:40:15 +0800 Subject: [PATCH 2/6] [sdnController]: ususu Resolves: ZCF-1365 Change-Id: I726b776f6b73637a7379677566706e64667a7161 --- .../compute/vm/VmAllocateNicIpFlow.java | 37 +++ .../org/zstack/compute/vm/VmSystemTags.java | 5 + conf/springConfigXml/sdnController.xml | 1 - .../AfterAllocateVmNicIpExtensionPoint.java | 19 ++ .../java/org/zstack/kvm/KVMAgentCommands.java | 24 ++ .../src/main/java/org/zstack/kvm/KVMHost.java | 10 + .../SdnControllerManagerImpl.java | 305 ++---------------- 7 files changed, 127 insertions(+), 274 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java index b7a10d05ec..db9de1d9ae 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java @@ -10,7 +10,9 @@ import org.zstack.core.cloudbus.CloudBusListCallBack; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; +import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.errorcode.ErrorFacade; +import org.zstack.header.core.Completion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.core.workflow.Flow; import org.zstack.header.core.workflow.FlowRollback; @@ -47,6 +49,8 @@ public class VmAllocateNicIpFlow implements Flow { @Autowired private VmNicManager nicManager; @Autowired + private PluginRegistry pluginRgty; + @Autowired protected VmInstanceManager vmMgr; @Override @@ -186,6 +190,39 @@ public void done(ErrorCodeList errorCodeList) { } else { dbf.updateCollection(nicsWithIp); dbf.updateCollection(ipVOS); + callAfterAllocateVmNicIpExtensions(spec, trigger); + } + } + }); + } + + private void callAfterAllocateVmNicIpExtensions(VmInstanceSpec spec, FlowTrigger trigger) { + List exts = + pluginRgty.getExtensionList(AfterAllocateVmNicIpExtensionPoint.class); + if (exts.isEmpty()) { + trigger.next(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.afterAllocateVmNicIp(spec, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + wcomp.addError(errorCode); + wcomp.allDone(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (!errorCodeList.getCauses().isEmpty()) { + trigger.fail(errorCodeList.getCauses().get(0)); + } else { trigger.next(); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java index 713c64890e..01d63c4046 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java @@ -5,6 +5,7 @@ import org.zstack.header.tag.AdminOnlyTag; import org.zstack.header.tag.TagDefinition; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmNicVO; import org.zstack.tag.PatternedSystemTag; import org.zstack.tag.SensitiveTagOutputHandler; import org.zstack.tag.SensitiveTag; @@ -314,4 +315,8 @@ public String desensitizeTag(SystemTag systemTag, String tag) { public static PatternedSystemTag VM_STATE_PAUSED_AFTER_MIGRATE = new PatternedSystemTag(("vmPausedAfterMigrate"), VmInstanceVO.class); public static PatternedSystemTag VM_MEMORY_ACCESS_MODE_SHARED = new PatternedSystemTag(("vmMemoryAccessModeShared"), VmInstanceVO.class); + + public static String IFACE_ID_TOKEN = "ifaceId"; + public static PatternedSystemTag IFACE_ID = new PatternedSystemTag( + String.format("ifaceId::{%s}", IFACE_ID_TOKEN), VmNicVO.class); } diff --git a/conf/springConfigXml/sdnController.xml b/conf/springConfigXml/sdnController.xml index 8cf5684384..af8b134fd3 100644 --- a/conf/springConfigXml/sdnController.xml +++ b/conf/springConfigXml/sdnController.xml @@ -34,7 +34,6 @@ - diff --git a/header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java new file mode 100644 index 0000000000..0afba727a4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java @@ -0,0 +1,19 @@ +package org.zstack.header.vm; + +import org.zstack.header.core.Completion; + +/** + * Extension point called after IP address(es) have been successfully allocated + * and flushed to the database for VmNics in VmAllocateNicIpFlow. + * + * At the time this fires: + * - VmNicVO rows exist in the database (created by VmAllocateNicFlow) + * - UsedIpVO rows are committed (allocated by VmAllocateNicIpFlow) + * - spec.getDestNics() contains up-to-date NIC inventories with IP info + * + * If the implementation fails, the flow chain rolls back: + * VmAllocateNicIpFlow.rollback (returns IPs) → VmAllocateNicFlow.rollback (deletes NICs). + */ +public interface AfterAllocateVmNicIpExtensionPoint { + void afterAllocateVmNicIp(VmInstanceSpec spec, Completion completion); +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index a8a1378288..6ed711cb25 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -1242,6 +1242,14 @@ public static class NicTO extends BaseVirtualDeviceTO { private Boolean isolated; + // bridge sub-type: null for Linux bridge, "openvswitch" for OVS bridge + // generates in libvirt XML + private String bridgePortType; + + // OVS external_ids:iface-id, used by SDN controller to identify the port + // generates + private String interfaceId; + public List getIps() { return ips; } @@ -1434,6 +1442,22 @@ public void setL2NetworkUuid(String l2NetworkUuid) { this.l2NetworkUuid = l2NetworkUuid; } + public String getBridgePortType() { + return bridgePortType; + } + + public void setBridgePortType(String bridgePortType) { + this.bridgePortType = bridgePortType; + } + + public String getInterfaceId() { + return interfaceId; + } + + public void setInterfaceId(String interfaceId) { + this.interfaceId = interfaceId; + } + public static NicTO fromVmNicInventory(VmNicInventory nic) { KVMAgentCommands.NicTO to = new KVMAgentCommands.NicTO(); to.setMac(nic.getMac()); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index a245757517..3872012b6c 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -4179,6 +4179,16 @@ private NicTO completeNicInfo(VmNicInventory nic) { KVMCompleteNicInformationExtensionPoint extp = factory.getCompleteNicInfoExtension(L2NetworkType.valueOf(l2inv.getType())); NicTO to = extp.completeNicInformation(l2inv, l3Inv, nic); + if (L2NetworkConstant.VSWITCH_TYPE_ZNS.equals(l2inv.getvSwitchType())) { + to.setBridgeName("br-int"); + to.setBridgePortType("openvswitch"); + String ifaceId = VmSystemTags.IFACE_ID.getTokenByResourceUuid( + nic.getUuid(), VmSystemTags.IFACE_ID_TOKEN); + if (ifaceId != null) { + to.setInterfaceId(ifaceId); + } + } + if (to.getUseVirtio() == null) { to.setUseVirtio(VmSystemTags.VIRTIO.hasTag(nic.getVmInstanceUuid())); to.setIps(getCleanTrafficIp(nic)); diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java index daa78b0f58..4d7b83e13a 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java @@ -47,11 +47,10 @@ import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class SdnControllerManagerImpl extends AbstractService implements SdnControllerManager, - L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, InstantiateResourceOnAttachingNicExtensionPoint, - PreVmInstantiateResourceExtensionPoint, VmReleaseResourceExtensionPoint, - ReleaseNetworkServiceOnDetachingNicExtensionPoint, SecurityGroupGetSdnBackendExtensionPoint, + L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, + SecurityGroupGetSdnBackendExtensionPoint, AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint, - BeforeAllocateVmNicExtensionPoint, AfterReleaseVmNicExtensionPoint { + AfterAllocateVmNicIpExtensionPoint, AfterReleaseVmNicExtensionPoint { private static final CLogger logger = Utils.getLogger(SdnControllerManagerImpl.class); private static final Logger log = LoggerFactory.getLogger(SdnControllerManagerImpl.class); @@ -462,262 +461,11 @@ public void done(ErrorCodeList errorCodeList) { }); } - @Override - public void releaseVmResource(VmInstanceSpec spec, Completion completion) { - if (VmInstanceConstant.VmOperation.DetachNic != spec.getCurrentVmOperation() && - VmInstanceConstant.VmOperation.Destroy != spec.getCurrentVmOperation()) { - completion.success(); - return; - } - - if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) { - completion.success(); - return; - } - - // we run into this situation when VM nics are all detached and the - // VM is being rebooted - if (spec.getDestNics().isEmpty()) { - completion.success(); - return; - } - - Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { - continue; - } - - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null) { - continue; - } - - if (shouldSkipSdnForNic(l2VO)) { - continue; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10005, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); - return; - } - - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); - } - - if (nicMaps.isEmpty()) { - completion.success(); - return; - } - - removeLogicalPort(nicMaps, completion); - } - - @Override - public void instantiateResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, Completion completion) { - L2NetworkVO l2NetworkVO = dbf.findByUuid(l3.getL2NetworkUuid(), L2NetworkVO.class); - if (shouldSkipSdnForNic(l2NetworkVO)) { - completion.success(); - return; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid())); - return; - } - - Map> nicMaps = new HashMap<>(); - List nics = new ArrayList<>(); - nics.add(spec.getDestNics().get(0)); - nicMaps.put(controllerUuid, nics); - sdnAddVmNics(nicMaps, completion); - } - - @Override - public void releaseResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, NoErrorCompletion completion) { - L2NetworkVO l2NetworkVO = dbf.findByUuid(l3.getL2NetworkUuid(), L2NetworkVO.class); - if (shouldSkipSdnForNic(l2NetworkVO)) { - completion.done(); - return; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - logger.warn(String.format("sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid())); - completion.done(); - return; - } - - Map> nicMaps = new HashMap<>(); - List nics = new ArrayList<>(); - nics.add(spec.getDestNics().get(0)); - nicMaps.put(controllerUuid, nics); - - removeLogicalPort(nicMaps, new Completion(completion) { - @Override - public void success() { - completion.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.info(String.format("failed to remove logical port for vm[uuid:%s] nic[internalName:%s], because: %s", - spec.getVmInventory().getUuid(), spec.getDestNics().get(0).getInternalName(), errorCode.getDetails())); - completion.done(); - } - }); - } - - @Override - public void releaseResourceOnDetachingNic(VmInstanceSpec spec, VmNicInventory nic, NoErrorCompletion completion) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - L2NetworkVO l2NetworkVO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (shouldSkipSdnForNic(l2NetworkVO)) { - completion.done(); - return; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - logger.warn(String.format("sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid())); - completion.done(); - return; - } - - Map> nicMaps = new HashMap<>(); - List nics = new ArrayList<>(); - nics.add(spec.getDestNics().get(0)); - nicMaps.put(controllerUuid, nics); - - removeLogicalPort(nicMaps, new Completion(completion) { - @Override - public void success() { - completion.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.info(String.format("failed to remove logical port for vm[uuid:%s] nic[internalName:%s], because: %s", - spec.getVmInventory().getUuid(), spec.getDestNics().get(0).getInternalName(), errorCode.getDetails())); - completion.done(); - } - }); - } - - @Override - public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstantiateResourceException { - - } - - @Override - public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { - if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) { - completion.success(); - return; - } - - // we run into this situation when VM nics are all detached and the - // VM is being rebooted - if (spec.getDestNics().isEmpty()) { - completion.success(); - return; - } - - Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { - continue; - } - - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null) { - continue; - } - - if (shouldSkipSdnForNic(l2VO)) { - continue; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10007, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); - return; - } - - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); - } - - if (nicMaps.isEmpty()) { - completion.success(); - return; - } - - sdnAddVmNics(nicMaps, completion); - } - - @Override - public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { - // create/start/reboot vm failed, code will go here VmInstantiateResourcePreFlow.rollack() - // vm change image failed, - if (VmInstanceConstant.VmOperation.NewCreate != spec.getCurrentVmOperation()) { - completion.success(); - return; - } - - if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) { - completion.success(); - return; - } - - // we run into this situation when VM nics are all detached and the - // VM is being rebooted - if (spec.getDestNics().isEmpty()) { - completion.success(); - return; - } - - Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { - continue; - } - - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null) { - continue; - } - - if (shouldSkipSdnForNic(l2VO)) { - continue; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10008, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); - return; - } - - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); - } - - if (nicMaps.isEmpty()) { - completion.success(); - return; - } - - removeLogicalPort(nicMaps, completion); - } + // VM lifecycle hooks (PreVmInstantiateResourceExtensionPoint, VmReleaseResourceExtensionPoint, + // InstantiateResourceOnAttachingNicExtensionPoint, ReleaseNetworkServiceOnDetachingNicExtensionPoint) + // have been removed. SDN port creation/deletion is now unified through VmNic lifecycle hooks: + // - beforeAllocateVmNic (BeforeAllocateVmNicExtensionPoint) for port creation + // - afterReleaseVmNic (AfterReleaseVmNicExtensionPoint) for port deletion /** * Returns true if the L2 network should be skipped for SDN port management: @@ -912,28 +660,39 @@ private SdnControllerVO getSdnControllerVO(L3NetworkInventory l3Network) { } @Override - public void beforeAllocateVmNic(VmNicInventory nic, VmInstanceSpec spec, Completion completion) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { + public void afterAllocateVmNicIp(VmInstanceSpec spec, Completion completion) { + if (spec.getDestNics() == null || spec.getDestNics().isEmpty()) { completion.success(); return; } - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null || shouldSkipSdnForNic(l2VO)) { - completion.success(); - return; + Map> nicMaps = new HashMap<>(); + for (VmNicInventory nic : spec.getDestNics()) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + continue; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + continue; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); + return; + } + + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); } - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); + if (nicMaps.isEmpty()) { + completion.success(); return; } - Map> nicMaps = new HashMap<>(); - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); sdnAddVmNics(nicMaps, completion); } From 99dcdb7cac9989af5b26b0ed61fb0b44a3df4200 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Mon, 30 Mar 2026 10:00:46 +0800 Subject: [PATCH 3/6] [compute]: ususu Resolves: ZCF-1365 Change-Id: I6c7a7777656f6268697267686e636b6f6a66626e --- .../zstack/compute/vm/VmAllocateNicIpFlow.java | 17 +++++++++++++++-- conf/springConfigXml/sdnController.xml | 5 +---- .../sdnController/SdnControllerManagerImpl.java | 8 -------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java index db9de1d9ae..95a1565150 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java @@ -190,13 +190,13 @@ public void done(ErrorCodeList errorCodeList) { } else { dbf.updateCollection(nicsWithIp); dbf.updateCollection(ipVOS); - callAfterAllocateVmNicIpExtensions(spec, trigger); + callAfterAllocateVmNicIpExtensions(spec, nicsWithIp, trigger); } } }); } - private void callAfterAllocateVmNicIpExtensions(VmInstanceSpec spec, FlowTrigger trigger) { + private void callAfterAllocateVmNicIpExtensions(VmInstanceSpec spec, List nicsWithIp, FlowTrigger trigger) { List exts = pluginRgty.getExtensionList(AfterAllocateVmNicIpExtensionPoint.class); if (exts.isEmpty()) { @@ -204,6 +204,17 @@ private void callAfterAllocateVmNicIpExtensions(VmInstanceSpec spec, FlowTrigger return; } + // Scope destNics to only those that actually received IPs so that + // extension-point implementations (e.g. SdnControllerManagerImpl) do not + // accidentally operate on NICs without allocated addresses. + List originalDestNics = spec.getDestNics(); + Set nicWithIpUuids = nicsWithIp.stream() + .map(VmNicVO::getUuid) + .collect(Collectors.toSet()); + spec.setDestNics(originalDestNics.stream() + .filter(n -> nicWithIpUuids.contains(n.getUuid())) + .collect(Collectors.toList())); + new While<>(exts).each((ext, wcomp) -> { ext.afterAllocateVmNicIp(spec, new Completion(wcomp) { @Override @@ -220,6 +231,8 @@ public void fail(ErrorCode errorCode) { }).run(new WhileDoneCompletion(trigger) { @Override public void done(ErrorCodeList errorCodeList) { + // Restore full destNics list so subsequent flows see all NICs + spec.setDestNics(originalDestNics); if (!errorCodeList.getCauses().isEmpty()) { trigger.fail(errorCodeList.getCauses().get(0)); } else { diff --git a/conf/springConfigXml/sdnController.xml b/conf/springConfigXml/sdnController.xml index af8b134fd3..426397f882 100644 --- a/conf/springConfigXml/sdnController.xml +++ b/conf/springConfigXml/sdnController.xml @@ -26,14 +26,11 @@ - - - - + diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java index 4d7b83e13a..db006bb973 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java @@ -276,8 +276,6 @@ public void run(MessageReply reply) { } catch (Exception e) { logger.warn(String.format("failed to load SdnControllerVO[uuid:%s] after init: %s", vo.getUuid(), e.getMessage()), e); - event.setError(operr("failed to load SdnController[uuid:%s] after successful init: %s", - vo.getUuid(), e.getMessage())); } } else { event.setError(reply.getError()); @@ -461,12 +459,6 @@ public void done(ErrorCodeList errorCodeList) { }); } - // VM lifecycle hooks (PreVmInstantiateResourceExtensionPoint, VmReleaseResourceExtensionPoint, - // InstantiateResourceOnAttachingNicExtensionPoint, ReleaseNetworkServiceOnDetachingNicExtensionPoint) - // have been removed. SDN port creation/deletion is now unified through VmNic lifecycle hooks: - // - beforeAllocateVmNic (BeforeAllocateVmNicExtensionPoint) for port creation - // - afterReleaseVmNic (AfterReleaseVmNicExtensionPoint) for port deletion - /** * Returns true if the L2 network should be skipped for SDN port management: * it has no SDN controller type configured on its VSwitchType. From 75c9096990fb153dd5c9c782feda410cc661e240 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Mon, 30 Mar 2026 10:56:42 +0800 Subject: [PATCH 4/6] [testlib,compute]: fix SdnController cast and NicIp ext filter Resolves: ZCF-1365 Change-Id: I7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b Co-Authored-By: Claude Opus 4.6 --- .../main/java/org/zstack/testlib/SdnControllerSpec.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy index 8a5f488c11..cb05661c4f 100644 --- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy @@ -4,6 +4,7 @@ import org.springframework.http.HttpEntity import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.zstack.sdk.SdnControllerInventory +import org.zstack.utils.gson.JSONObjectUtil import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.LoginReply import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.LoginRsp @@ -58,9 +59,9 @@ class SdnControllerSpec extends Spec implements HasSession { } postCreate { - inventory = querySdnController { + inventory = JSONObjectUtil.rehashObject(querySdnController { conditions=["uuid=${inventory.uuid}".toString()] - }[0] + }[0], SdnControllerInventory.class) } return id(name, inventory.uuid) From dfa1f8972046c3f6788f2207f488632786aa3bb3 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Mon, 30 Mar 2026 17:37:30 +0800 Subject: [PATCH 5/6] [compute]: ususu Resolves: ZCF-1365 Change-Id: I64786961746d626b76707466767975636475736e --- .../zstack/compute/vm/VmAllocateNicFlow.java | 49 +------ .../compute/vm/VmAllocateNicIpFlow.java | 50 ------- .../compute/vm/VmAllocateSdnNicFlow.java | 122 ++++++++++++++++++ .../zstack/compute/vm/VmDetachNicFlow.java | 13 +- .../org/zstack/compute/vm/VmInstanceBase.java | 1 + .../compute/vm/VmReturnReleaseNicFlow.java | 32 ++--- conf/springConfigXml/VmInstanceManager.xml | 1 + conf/springConfigXml/sdnController.xml | 3 +- .../vm/AfterAllocateSdnNicExtensionPoint.java | 49 +++++++ .../SdnControllerManagerImpl.java | 76 ++++++++--- 10 files changed, 254 insertions(+), 142 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java create mode 100644 header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java index 2e1634421a..b572bf0ea1 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java @@ -300,41 +300,6 @@ public void done(ErrorCodeList errorCodeList) { }); } - private void callAfterReleaseVmNicExtensions(List nics, Completion completion) { - List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); - if (exts.isEmpty() || nics.isEmpty()) { - completion.success(); - return; - } - - new While<>(nics).each((nic, wcomp) -> { - new While<>(exts).each((ext, wcomp2) -> { - ext.afterReleaseVmNic(nic, new Completion(wcomp2) { - @Override - public void success() { - wcomp2.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.warn(String.format("failed to call afterReleaseVmNic for nic[uuid:%s], %s", nic.getUuid(), errorCode)); - wcomp2.done(); - } - }); - }).run(new WhileDoneCompletion(wcomp) { - @Override - public void done(ErrorCodeList errorCodeList) { - wcomp.done(); - } - }); - }).run(new WhileDoneCompletion(completion) { - @Override - public void done(ErrorCodeList errorCodeList) { - completion.success(); - } - }); - } - @Override public void rollback(final FlowRollback chain, Map data) { final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); @@ -355,18 +320,6 @@ public void rollback(final FlowRollback chain, Map data) { } dbf.removeByPrimaryKeys(destNics.stream().map(VmNicInventory::getUuid).collect(Collectors.toList()), VmNicVO.class); - callAfterReleaseVmNicExtensions(destNics, new Completion(chain) { - @Override - public void success() { - chain.rollback(); - } - - @Override - public void fail(ErrorCode errorCode) { - // best-effort: log and continue rollback even if SDN cleanup fails - logger.warn(String.format("afterReleaseVmNic extensions failed during rollback: %s", errorCode)); - chain.rollback(); - } - }); + chain.rollback(); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java index 95a1565150..b7a10d05ec 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicIpFlow.java @@ -10,9 +10,7 @@ import org.zstack.core.cloudbus.CloudBusListCallBack; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; -import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.errorcode.ErrorFacade; -import org.zstack.header.core.Completion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.core.workflow.Flow; import org.zstack.header.core.workflow.FlowRollback; @@ -49,8 +47,6 @@ public class VmAllocateNicIpFlow implements Flow { @Autowired private VmNicManager nicManager; @Autowired - private PluginRegistry pluginRgty; - @Autowired protected VmInstanceManager vmMgr; @Override @@ -190,52 +186,6 @@ public void done(ErrorCodeList errorCodeList) { } else { dbf.updateCollection(nicsWithIp); dbf.updateCollection(ipVOS); - callAfterAllocateVmNicIpExtensions(spec, nicsWithIp, trigger); - } - } - }); - } - - private void callAfterAllocateVmNicIpExtensions(VmInstanceSpec spec, List nicsWithIp, FlowTrigger trigger) { - List exts = - pluginRgty.getExtensionList(AfterAllocateVmNicIpExtensionPoint.class); - if (exts.isEmpty()) { - trigger.next(); - return; - } - - // Scope destNics to only those that actually received IPs so that - // extension-point implementations (e.g. SdnControllerManagerImpl) do not - // accidentally operate on NICs without allocated addresses. - List originalDestNics = spec.getDestNics(); - Set nicWithIpUuids = nicsWithIp.stream() - .map(VmNicVO::getUuid) - .collect(Collectors.toSet()); - spec.setDestNics(originalDestNics.stream() - .filter(n -> nicWithIpUuids.contains(n.getUuid())) - .collect(Collectors.toList())); - - new While<>(exts).each((ext, wcomp) -> { - ext.afterAllocateVmNicIp(spec, new Completion(wcomp) { - @Override - public void success() { - wcomp.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - wcomp.addError(errorCode); - wcomp.allDone(); - } - }); - }).run(new WhileDoneCompletion(trigger) { - @Override - public void done(ErrorCodeList errorCodeList) { - // Restore full destNics list so subsequent flows see all NICs - spec.setDestNics(originalDestNics); - if (!errorCodeList.getCauses().isEmpty()) { - trigger.fail(errorCodeList.getCauses().get(0)); - } else { trigger.next(); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java new file mode 100644 index 0000000000..1279f91245 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java @@ -0,0 +1,122 @@ +package org.zstack.compute.vm; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.core.asyncbatch.While; +import org.zstack.core.componentloader.PluginRegistry; +import org.zstack.header.core.Completion; +import org.zstack.header.core.WhileDoneCompletion; +import org.zstack.header.core.workflow.Flow; +import org.zstack.header.core.workflow.FlowRollback; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.vm.*; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.List; +import java.util.Map; + +import static org.zstack.core.progress.ProgressReportService.taskProgress; + +/** + * Placed after VmAllocateNicIpFlow in the flow chain. + * + * For each NIC belonging to an SDN-managed L2 network, this flow delegates + * to the registered {@link AfterAllocateSdnNicExtensionPoint} implementations + * (typically {@code SdnControllerManagerImpl}) which: + * + * - OVN: calls addLogicalPorts() with already-allocated IPs + * - ZNS: calls createSegmentPort(), receives IP back from ZNS, writes to DB + * - H3C/Sugon: default no-op + * + * Non-SDN NICs are skipped by the extension implementation internally. + */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VmAllocateSdnNicFlow implements Flow { + private static final CLogger logger = Utils.getLogger(VmAllocateSdnNicFlow.class); + + @Autowired + private PluginRegistry pluginRgty; + + @Override + public void run(final FlowTrigger trigger, final Map data) { + taskProgress("create SDN ports for nics"); + + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + final List nics = spec.getDestNics(); + + if (nics == null || nics.isEmpty()) { + trigger.next(); + return; + } + + List exts = + pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); + if (exts.isEmpty()) { + trigger.next(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.afterAllocateSdnNic(spec, nics, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + wcomp.addError(errorCode); + wcomp.allDone(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (!errorCodeList.getCauses().isEmpty()) { + trigger.fail(errorCodeList.getCauses().get(0)); + } else { + trigger.next(); + } + } + }); + } + + @Override + public void rollback(final FlowRollback chain, Map data) { + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + final List nics = spec.getDestNics(); + + List exts = + pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); + + if (exts.isEmpty() || nics == null || nics.isEmpty()) { + chain.rollback(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.rollbackSdnNic(spec, nics, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + // best-effort: log and continue + logger.warn(String.format("failed to rollback SDN nic: %s", errorCode)); + wcomp.done(); + } + }); + }).run(new WhileDoneCompletion(chain) { + @Override + public void done(ErrorCodeList errorCodeList) { + chain.rollback(); + } + }); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java index 149fe4451b..f05f783cc9 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java @@ -100,7 +100,7 @@ public void run(MessageReply reply) { public void done(ErrorCodeList errorCodeList) { dbf.removeByPrimaryKey(nic.getUuid(), VmNicVO.class); - callAfterReleaseVmNicExtensions(nic, new Completion(trigger) { + callReleaseSdnNics(List.of(nic), new Completion(trigger) { @Override public void success() { trigger.next(); @@ -108,7 +108,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extensions failed for nic[uuid:%s]: %s, continue", + logger.warn(String.format("releaseSdnNics failed for nic[uuid:%s]: %s, continue", nic.getUuid(), errorCode)); trigger.next(); } @@ -117,15 +117,15 @@ public void fail(ErrorCode errorCode) { }); } - private void callAfterReleaseVmNicExtensions(VmNicInventory nic, Completion completion) { - List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + private void callReleaseSdnNics(List nics, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); if (exts.isEmpty()) { completion.success(); return; } new While<>(exts).each((ext, wcomp) -> { - ext.afterReleaseVmNic(nic, new Completion(wcomp) { + ext.releaseSdnNics(nics, new Completion(wcomp) { @Override public void success() { wcomp.done(); @@ -133,8 +133,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extension failed for nic[uuid:%s]: %s, continue", - nic.getUuid(), errorCode)); + logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode)); wcomp.done(); } }); diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java index 73f3e98b0d..4e904d6dbf 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -2287,6 +2287,7 @@ void rollback() { flowChain.then(new VmAllocateNicFlow()); flowChain.then(new VmAllocateNicIpFlow()); + flowChain.then(new VmAllocateSdnNicFlow()); flowChain.then(new VmSetDefaultL3NetworkOnAttachingFlow()); setAdditionalFlow(flowChain, spec); if (self.getState() == VmInstanceState.Running) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java index 6e928b635c..2aa8a45994 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java @@ -98,7 +98,7 @@ public void done(ErrorCodeList errorCodeList) { releasedNics.add(nic); } - callAfterReleaseVmNicExtensions(releasedNics, new Completion(chain) { + callReleaseSdnNics(releasedNics, new Completion(chain) { @Override public void success() { chain.next(); @@ -106,7 +106,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extensions failed: %s, continue anyway", errorCode)); + logger.warn(String.format("releaseSdnNics failed: %s, continue anyway", errorCode)); chain.next(); } }); @@ -114,31 +114,23 @@ public void fail(ErrorCode errorCode) { }); } - private void callAfterReleaseVmNicExtensions(List nics, Completion completion) { - List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + private void callReleaseSdnNics(List nics, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); if (exts.isEmpty() || nics.isEmpty()) { completion.success(); return; } - new While<>(nics).each((nic, wcomp) -> { - new While<>(exts).each((ext, wcomp2) -> { - ext.afterReleaseVmNic(nic, new Completion(wcomp2) { - @Override - public void success() { - wcomp2.done(); - } + new While<>(exts).each((ext, wcomp) -> { + ext.releaseSdnNics(nics, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } - @Override - public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extension failed for nic[uuid:%s]: %s, continue", - nic.getUuid(), errorCode)); - wcomp2.done(); - } - }); - }).run(new WhileDoneCompletion(wcomp) { @Override - public void done(ErrorCodeList errorCodeList) { + public void fail(ErrorCode errorCode) { + logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode)); wcomp.done(); } }); diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index ef3d5a7cc9..0ab2073d58 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -37,6 +37,7 @@ org.zstack.compute.vm.VmAllocateVolumeFlow org.zstack.compute.vm.VmAllocateNicFlow org.zstack.compute.vm.VmAllocateNicIpFlow + org.zstack.compute.vm.VmAllocateSdnNicFlow org.zstack.compute.vm.VmAllocateCdRomFlow org.zstack.compute.vm.VmInstantiateResourcePreFlow org.zstack.compute.vm.VmCreateOnHypervisorFlow diff --git a/conf/springConfigXml/sdnController.xml b/conf/springConfigXml/sdnController.xml index 426397f882..e76be084ac 100644 --- a/conf/springConfigXml/sdnController.xml +++ b/conf/springConfigXml/sdnController.xml @@ -30,8 +30,7 @@ - - + diff --git a/header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java new file mode 100644 index 0000000000..97606fddcb --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java @@ -0,0 +1,49 @@ +package org.zstack.header.vm; + +import org.zstack.header.core.Completion; + +import java.util.List; + +/** + * Extension point called after VmNicVO is persisted and non-SDN NIC IPs have + * been allocated (i.e. after VmAllocateNicIpFlow), but before the VM is + * instantiated on the hypervisor. + * + * Implementations should: + * 1. Filter NICs that belong to SDN-managed L2 networks (via VSwitchType). + * 2. Send REST API calls to the SDN controller to create ports (e.g. + * OVN logical switch ports, ZNS segment ports). + * 3. For controllers that return IP addresses (e.g. ZNS), write the + * returned IPs back into UsedIpVO and VmNicVO. + * + * If the implementation fails, VmAllocateSdnNicFlow will trigger a rollback + * via {@link #rollbackSdnNic}. + */ +public interface AfterAllocateSdnNicExtensionPoint { + /** + * Create SDN ports for the given NICs. + * + * @param spec the VM instance spec + * @param nics all NICs from spec.getDestNics() — implementation filters SDN NICs internally + * @param completion success/fail callback + */ + void afterAllocateSdnNic(VmInstanceSpec spec, List nics, Completion completion); + + /** + * Rollback: remove SDN ports and clean up any IPs allocated by the SDN controller. + * + * @param spec the VM instance spec + * @param nics all NICs from spec.getDestNics() + * @param completion success/fail callback (best-effort — failures should be logged but not block rollback) + */ + void rollbackSdnNic(VmInstanceSpec spec, List nics, Completion completion); + + /** + * Release SDN ports for NICs being detached or destroyed. + * Used by VmDetachNicFlow and VmReturnReleaseNicFlow. + * + * @param nics NICs to release (implementation filters SDN NICs internally) + * @param completion success/fail callback (best-effort) + */ + void releaseSdnNics(List nics, Completion completion); +} diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java index db006bb973..e879d43647 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java @@ -50,7 +50,7 @@ public class SdnControllerManagerImpl extends AbstractService implements SdnCont L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, SecurityGroupGetSdnBackendExtensionPoint, AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint, - AfterAllocateVmNicIpExtensionPoint, AfterReleaseVmNicExtensionPoint { + AfterAllocateSdnNicExtensionPoint { private static final CLogger logger = Utils.getLogger(SdnControllerManagerImpl.class); private static final Logger log = LoggerFactory.getLogger(SdnControllerManagerImpl.class); @@ -652,14 +652,14 @@ private SdnControllerVO getSdnControllerVO(L3NetworkInventory l3Network) { } @Override - public void afterAllocateVmNicIp(VmInstanceSpec spec, Completion completion) { - if (spec.getDestNics() == null || spec.getDestNics().isEmpty()) { + public void afterAllocateSdnNic(VmInstanceSpec spec, List nics, Completion completion) { + if (nics == null || nics.isEmpty()) { completion.success(); return; } Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { + for (VmNicInventory nic : nics) { L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); if (l3Vo == null) { continue; @@ -673,7 +673,7 @@ public void afterAllocateVmNicIp(VmInstanceSpec spec, Completion completion) { String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); + completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] has not attached controller", l2VO.getUuid())); return; } @@ -689,28 +689,74 @@ public void afterAllocateVmNicIp(VmInstanceSpec spec, Completion completion) { } @Override - public void afterReleaseVmNic(VmNicInventory nic, Completion completion) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { + public void rollbackSdnNic(VmInstanceSpec spec, List nics, Completion completion) { + if (nics == null || nics.isEmpty()) { completion.success(); return; } - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + Map> nicMaps = new HashMap<>(); + for (VmNicInventory nic : nics) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + continue; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + continue; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + continue; + } + + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + } + + if (nicMaps.isEmpty()) { completion.success(); return; } - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); + removeLogicalPort(nicMaps, completion); + } + + @Override + public void releaseSdnNics(List nics, Completion completion) { + if (nics == null || nics.isEmpty()) { + completion.success(); return; } Map> nicMaps = new HashMap<>(); - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + for (VmNicInventory nic : nics) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + continue; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + continue; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + continue; + } + + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + } + + if (nicMaps.isEmpty()) { + completion.success(); + return; + } + removeLogicalPort(nicMaps, completion); } } From 4e774605944091459c9842d1e33044bcb1644e80 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Mon, 30 Mar 2026 17:45:34 +0800 Subject: [PATCH 6/6] [compute]: usuus Resolves: ZCF-1365 Change-Id: I6c76786265676977656c6461616369647a777968 --- .../src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java index f05f783cc9..d59339bf3b 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java @@ -100,7 +100,7 @@ public void run(MessageReply reply) { public void done(ErrorCodeList errorCodeList) { dbf.removeByPrimaryKey(nic.getUuid(), VmNicVO.class); - callReleaseSdnNics(List.of(nic), new Completion(trigger) { + callReleaseSdnNics(java.util.Collections.singletonList(nic), new Completion(trigger) { @Override public void success() { trigger.next();