From ca4ab2faec7fd03017553626e2c2881014a5072c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 13 Mar 2026 20:06:42 +0800 Subject: [PATCH 1/4] [sdnController]: add doc Resolves: ZCF-1365 Change-Id: I73687962636e7871626e687761626d6661716668 --- conf/db/upgrade/V5.5.18__schema.sql | 15 + docs/modules/network/nav.adoc | 1 + .../pages/networkResource/ZnsIntegration.adoc | 322 ++++++++++++++++++ .../networkResource/networkResource.adoc | 3 +- .../header/network/l2/L2NetworkConstant.java | 4 + .../kvm/KVMRealizeL2NoVlanNetworkBackend.java | 3 +- .../kvm/KVMRealizeL2VlanNetworkBackend.java | 3 +- 7 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 conf/db/upgrade/V5.5.18__schema.sql create mode 100644 docs/modules/network/pages/networkResource/ZnsIntegration.adoc diff --git a/conf/db/upgrade/V5.5.18__schema.sql b/conf/db/upgrade/V5.5.18__schema.sql new file mode 100644 index 00000000000..6f362aeab68 --- /dev/null +++ b/conf/db/upgrade/V5.5.18__schema.sql @@ -0,0 +1,15 @@ +-- ZNS SDN Controller support + +CREATE TABLE IF NOT EXISTS `ZnsControllerVO` ( + `uuid` varchar(32) NOT NULL, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkZnsControllerVOSdnControllerVO` FOREIGN KEY (`uuid`) REFERENCES `SdnControllerVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `L2GeneveNetworkVO` ( + `uuid` varchar(32) NOT NULL, + `geneveId` int(10) unsigned NOT NULL, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkL2GeneveNetworkVOL2NetworkVO` FOREIGN KEY (`uuid`) REFERENCES `L2NetworkVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/docs/modules/network/nav.adoc b/docs/modules/network/nav.adoc index 53e42d44f22..189d4db5edb 100644 --- a/docs/modules/network/nav.adoc +++ b/docs/modules/network/nav.adoc @@ -3,4 +3,5 @@ *** xref:networkResource/L2Network.adoc[] *** xref:networkResource/L3Network.adoc[] *** xref:networkResource/VpcRouter.adoc[] + *** xref:networkResource/ZnsIntegration.adoc[] ** xref:networkService/networkService.adoc[] \ No newline at end of file diff --git a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc new file mode 100644 index 00000000000..12be07488d0 --- /dev/null +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -0,0 +1,322 @@ += 对接ZNS SDN控制器 + +== 总体描述 + +在考虑对接ZNS SDN控制器时,首先需要考虑如何做好Cloud资源对象和ZNS资源映射。 + +* ZNS segments → L2Network + L3Network + IpRange +* ZNS segments port → VmNic + UsedIp +* ZNS segments + transport zone → L2NetworkClusterRefVO + +因为最终会把ZNS UI嵌套到Cloud UI中,其他资源对象不需要映射。 + +== ZNS SDN控制器 + +ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`SugonSdnController`、`OvnController`、`HuaweiIMasterSdnController` 等实现。 + +新定义 `ZnsControllerVO`,继承 `SdnControllerVO`,不添加新的字段: + +* vendorType:ZNS +* vendorVersion:1.0 + +=== 新增类说明 + +[cols="1,2"] +|=== +|类名 |说明 + +|`ZnsControllerVO` +|继承 `SdnControllerVO`,无额外字段,需要建表SQL(仅uuid主键关联) + +|`ZnsSdnControllerFactory` +|实现 `SdnControllerFactory` 接口,注册 vendorType 为 "ZNS";`getSdnControllerSecurityGroup()` 返回null(ZNS不需要安全组SDN后端);`getSdnControllerDhcp()` 返回null(ZNS不配置DHCP) + +|`ZnsSdnController` +|实现 `SdnController` 接口,处理控制器生命周期(创建/删除/重连);addHost/removeHost 仅操作数据库,不修改物理机配置 + +|`ZnsSdnControllerL2` +|实现 `SdnControllerL2` 接口;createL2Network 调用ZNS API创建segment;deleteL2Network 调用ZNS API删除segment;attachL2NetworkToCluster/detachL2NetworkFromCluster 调用ZNS API修改segment的transport zone关联;addVmNics() 调用ZNS API创建segment port;removeVmNics() 调用ZNS API删除segment port;其它函数空实现 + +|`ZnsSdnControllerL3` +|实现 `SdnControllerL3` 接口;createIpRange() 调用ZNS API修改segment的cidr;deleteIpRange() 调用ZNS API修改segment的cidr;createL3Network()/deleteL3Network() 空实现 + +|`ZnsSdnControllerConstant` +|定义常量 `ZNS_CONTROLLER = "ZNS"` + +|=== + +=== 创建SDN控制器 + +必须先在ZNS完成添加 Computer Manager 的操作,才能在Cloud侧创建对应的 SdnController。 + +UI调用 `APIAddSdnControllerMsg`,同时携带一个 SystemTags:`computerManagerUuid::xxxx`,Cloud侧根据这个tag把 Computer Manager 和 SdnController 关联起来。Cloud后续API操作,会把 computerManagerUuid 作为 cms uuid 传给ZNS,用来区分ZNS的segment、segment port是哪个cloud创建的。 + +在添加 SdnController 的过程中,`initSdnController` 函数实现如下逻辑: + +1. 根据 Computer Manager 的uuid获取ZNS Host列表,配置 `SdnControllerHostRefVO`,关联到 `SdnControllerVO` +2. 根据 computerManagerUuid 获取ZNS segment列表,配置 `L2NetworkVO`、`L3NetworkVO`、`IpRangeVO` +3. 根据ZNS segment和transport zone的关系,配置 `L2NetworkClusterRefVO`,关联 `L2NetworkVO` 和 `ClusterVO` + +[NOTE] +ZNS 为 Cloud 生成一个 Computer Manager,Cloud 通过这个 Computer Manager 的 uuid 来区分不同 Cloud 的 ZNS 资源。 +ZNS 的资源(如 segment、segment port 等)都必须携带 cms 信息,一个资源可以管理多个 cms 信息,实现同一个资源在多个 Cloud 共享。 + +=== 删除SDN控制器 + +和其它类型的 SdnController 一样,删除 `SdnControllerVO`、`SdnControllerHostRefVO`、`L2NetworkVO`、`L3NetworkVO`、`IpRangeVO`、`L2NetworkClusterRefVO` 等相关数据对象,但不需要执行删除物理机 OVS DPDK 操作。 + +=== 同步 + +Cloud 的 API 不是声明式 API,不能保证 Cloud 的数据库和 ZNS 数据的一致性,因此 Cloud 的资源可能和 ZNS 的资源不一致。 +为此,`APIReconnectSdnControllerMsg` 用来同步 Cloud 和 ZNS 的资源状态,确保 Cloud 数据库和 ZNS 的资源状态一致,动作包含: + +1. Cloud 读取属于当前 Cloud 的 Segment 列表(查询时携带 Computer Manager 的 uuid): +** 如果 ZNS 存在,但 Cloud 不存在,且 ZNS 的 segment 也没有其它 cms 使用,调用 API 删除 ZNS segment +** 如果 ZNS 存在,Cloud 也存在,需要比较两侧的参数,如果不一致,更新 ZNS 侧的参数(如 cidr) +** 如果 ZNS 不存在,但 Cloud 存在,调用 API 添加 ZNS segment,根据 L3 信息添加 segment cidr + +2. 完成 Segment 同步后,Cloud 读取属于当前 Cloud 的 Segment port 列表(查询时携带 Computer Manager 的 uuid): +** 如果 ZNS 不存在,但 Cloud 存在,调用 ZNS segment port API 创建 segment port +** 如果 ZNS 存在,但 Cloud 不存在,调用 ZNS segment port API 删除 segment port +** 如果 ZNS 存在,Cloud 也存在,需要比较两侧的参数,如果不一致,更新 ZNS 侧参数为 Cloud 侧参数 + +=== Cloud调用ZNS API + +本章节仅列 Cloud 主动调用 ZNS 的接口,不包含 callback 接口。 + +==== Segment + +* `GET /zns/api/v1/segments` +* `GET /zns/api/v1/segments/{uuid}` +* `POST /zns/api/v1/segments` +* `PATCH /zns/api/v1/segments/{uuid}` +* `DELETE /zns/api/v1/segments` + +==== Segment Port + +* `GET /zns/api/v1/segments/{uuid}/ports` +* `GET /zns/api/v1/segments/{uuid}/ports/{portUuid}` +* `POST /zns/api/v1/segments/{uuid}/ports` +* `PATCH /zns/api/v1/segments/{uuid}/ports/{portUuid}` +* `DELETE /zns/api/v1/segments/{uuid}/ports` + +==== Segment Used IP + +* `GET /zns/api/v1/segments/{uuid}/used-ips` + +[NOTE] +查询某个 segment 下全部 segment port 时,Cloud 使用:`GET /zns/api/v1/segments/{uuid}/ports?offset=0&limit=-1` + +== L2Network + +=== 基础信息 + +`L2NetworkVO` 的重要字段: + +* `type`:L2NetworkType.types,有:L2NoVlanNetwork、L2VlanNetwork、VxlanNetworkPool、VxlanNetwork、TfL2Network、HardwareVxlanNetworkPool、HardwareVxlanNetwork +* `vSwitchType`:VSwitchType.types,有:LinuxBridge、TfL2Network、MacVlan、OvnDpdk、OvsDpdk +* `virtualNetworkId`:vlanId 或 vxlanID +* `physicalInterface`:物理网卡名称 + +ZNS L2Network 的类型定义: + +[cols="1,2"] +|=== +|属性 |值 + +|type +|L2NoVlanNetwork、L2VlanNetwork、L2GeneveNetwork(新增类型,类似于L2VlanNetwork) + +|vSwitchType +|ZNS(固定值,不区分 kernel 和 dpdk) + +|physicalInterface +|null + +|virtualNetworkId +|Vlan Id 或 Geneve Id + +|=== + +[IMPORTANT] +新增的 `VSwitchType("ZNS")` 必须设置 sdnControllerType 为 "ZNS",即 `new VSwitchType("ZNS").setSdnControllerType("ZNS")`。 +这是 `SdnControllerManagerImpl` 判断 L2 网络是否归 SDN Controller 管理的关键属性,影响 `preInstantiateVmResource`、`releaseVmResource`、`instantiateResourceOnAttachingNic`、`releaseResourceOnDetachingNic` 等所有扩展点的正确路由。 + +=== 新增L2GeneveNetwork注册 + +需要新增以下类: + +[cols="1,2"] +|=== +|类名 |说明 + +|`L2GeneveNetworkVO` +|继承 `L2NetworkVO`,增加 `geneveId` 字段(类似 `L2VlanNetworkVO` 的 vlan 字段),需要建表SQL + +|`L2GeneveNetworkInventory` +|对应的 Inventory 类 + +|`L2GeneveNetworkFactory` +|实现 `L2NetworkFactory` 接口,注册 `new L2NetworkType("L2GeneveNetwork")` + +|`L2GeneveNetwork` +|继承 `L2NoVlanNetwork`,处理 L2GeneveNetwork 的消息路由 + +|`L2NetworkConstant` 新增常量 +|`L2_GENEVE_NETWORK_TYPE = "L2GeneveNetwork"` + +|`ZnsVmNicFactory` +|注册 `new VSwitchType("ZNS")`,绑定对应的 VmNicType + +|=== + +=== KVM Realize Backend + +需要为 L2GeneveNetwork 注册 KVM 后端实现: + +`KVMRealizeL2GeneveNetworkBackend`:实现 `KVMCompleteNicInformationExtensionPoint` 接口 + +* 按 `L2NetworkType("L2GeneveNetwork")` 注册到 `KVMHostFactory` 的 `completeNicInfoExtensions` 映射中 +* `completeNicInformation()` 方法:填充 NicTO 的 `bridgeName`、`physicalInterface`、`mtu` 等信息;对于 OvnDpdk 模式需要设置 `srcPath`(与 `KVMRealizeL2NoVlanNetworkBackend` 中 OvnDpdk 的处理逻辑一致) +* `realize/check/delete` 方法:由于 ZNS/OVS 管理 bridge,这些方法可以做空实现,但必须注册,否则 attach L2 到 cluster 或 host reconnect 时会因找不到 backend 而失败 + +[NOTE] +如果 L2NoVlanNetwork 和 L2VlanNetwork 类型的 ZNS 网络复用现有 Backend,需确认这些 backend 能正确处理 vSwitchType 为 ZNS 的情况。 + +=== 创建L2Network + +处理逻辑类似 OVN Controller,但调用 ZNS API 创建 segment。 + +==== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg + +处理逻辑类似 OVN Controller,根据 ZNS Host 和 transport zone 的关系,把 ZNS segment 关联到 transport zone。 + +==== APIChangeL2NetworkVlanIdMsg + +* L2GeneveNetwork 类型不支持修改 VlanId,需要在 `L2NetworkApiInterceptor` 中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` +* L2VlanNetwork、L2NoVlanNetwork 类型支持 +* 仅需要修改 L2NetworkVO 数据库,不需要下发到物理机,需要调用修改 ZNS segment API + +[NOTE] +`AttachedL2NetworkAllocatorFlow` 根据虚拟机选择的 L2 网络选择候选的物理机,它根据 `L2NetworkClusterRefVO` 找到 L2Network 关联的 cluster,从而找到 cluster 内的物理机器作为候选机器。 + +== L3Network + +=== 基础信息 + +`L3NetworkVO` 的重要字段: + +* `type`:L3BasicNetwork、L3VpcNetwork +* `category`:Public、Private、System + +ZNS L3 的定义: + +[cols="1,2"] +|=== +|属性 |值/规则 + +|type +|L3ZnsNetwork + +|category(L2GeneveNetwork 类型) +|只能是 Private + +|category(L2NoVlanNetwork、L2VlanNetwork 类型) +|可以是 Public 或 Private + +|=== + +ZNS L3 不配置 DHCP 网络服务,因此 `enableIpAddressAllocation()` 为 false。 + +[IMPORTANT] +当前 `enableIpAddressAllocation()` 实现中,L3VpcNetwork 类型会返回 true(因为 type != L3BasicNetwork)。 +需要调整该逻辑:当 L3Network 关联的 L2Network 的 vSwitchType 为 ZNS 时返回 false。 + +ZNS L3 API 不需要调用 SDN backend。`ZnsSdnControllerFactory.getSdnControllerL3()` 返回 null,`SdnControllerManagerImpl` 中 `getSdnControllerL3()` 在 controllerUuid 为 null 或 factory 返回 null 时会自然跳过,不影响 L3 CRUD 操作。 + +ZNS L3 不需要配置任何网络服务(无 DHCP、无 DNS、无 UserData、无 EIP、无 PortForwarding 等)。 + +Cloud 侧的 IpRange 只做记录,不参与 IP 分配,IP 由 ZNS 负责管理和分配。`SdnControllerManagerImpl` 的 `afterAddIpRange`/`afterDeleteIpRange` 会调用 `SdnControllerL2` 的 `addL3NetworkIpRange`/`deleteL3NetworkIpRange`,`ZnsSdnControllerL2` 中这两个方法做空实现。 + +=== SetVmStaticIp / ChangeVmIp 操作 + +由于 ZNS 网络的 IP 由 ZNS 管理,`APISetVmStaticIpMsg` 和 `APIChangeVmIpMsg` 需要特殊处理: + +在 `VmInstanceApiInterceptor` 中增加校验:如果目标 L3Network 关联的 L2Network 的 vSwitchType 为 ZNS,需要将用户指定的 IP 传给 ZNS segment port API 进行更新,而非走 Cloud 侧的 IP 分配流程。 + +== VmNic + +VmNicType 的值有:VNIC、VF、dpdkvhostuserclient。ZNS 可能是 dpdk 模式,也可能是 kernel 模式,在 UI 选择 ZNS 网络以后,用户可以选择网卡类型:VNIC 或 dpdkvhostuserclient。 + +=== 虚拟机的物理机分配 + +创建虚拟机选择了 ZNS 网络时: + +* 默认网卡类型是 VNIC,需要选择到部署了 OvnKernel 的物理机 +* 如果选择了 dpdkvhostuserclient,需要选择到部署了 OvnDpdk 的物理机 + +`AttachedL2NetworkAllocatorFlow` 会调用 `AttachedL2NetworkAllocatorExtensionPoint` 扩展点进一步选择物理机,该扩展点根据 L2 找到 ZNS 控制器,根据 `SdnControllerHostRefVO` 找到物理机: + +* 如果网卡是 VNIC,选择 vSwitchType 是 OvnKernel 的物理机 +* 如果网卡是 dpdkvhostuserclient,选择 vSwitchType 是 OvnDpdk 的物理机 + +=== 网卡创建过程 + +`VmAllocateNicFlow`/`ApplianceVmAllocateNicFlow` 分别是创建虚拟机、ApplianceVm 过程中创建网卡的过程。ApplianceVm 可能使用 ZNS 网络,因此 `ApplianceVmAllocateNicFlow` 也需要适配 ZNS 流程。 + +ZNS 网络创建过程: + +1. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType +2. 调用 ZNS 创建 segment port API,获取 ip/掩码/网关、ip6/前缀/网关,cms 信息中携带 computerManagerUuid +3. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 +4. 根据获取的参数创建 `VmNicVO`、`UsedIpVO` + +[NOTE] +`VmAllocateNicIpFlow`(在 `VmAllocateNicFlow` 之后执行)负责给已创建的 Nic 分配 IP。 +对于 ZNS 网络,由于 `enableIpAddressAllocation()` 为 false 且 IP 已在 `VmAllocateNicFlow` 中通过 ZNS API 获取并保存,`VmAllocateNicIpFlow` 会跳过这些 Nic,不会重复处理。 + +==== 网卡创建失败回滚 + +如果调用 ZNS segment port API 成功获取到 IP,但后续创建 `VmNicVO`/`UsedIpVO` 失败,需要回滚:在 `VmAllocateNicFlow` 的 rollback 方法中检查是否已调用 ZNS 分配了 segment port,如果是则调用 ZNS 删除 segment port。 + +=== 网卡删除过程 + +* `VmReturnReleaseNicFlow`:在 `destroyVmWorkFlowElements` 中被调用,用于虚拟机销毁时释放网卡资源 +* `VmDetachNicFlow`:在云主机删除网卡时调用 + +两个 Flow 中都需要: + +1. 调用 ZNS 删除 segment port API +2. 删除 `VmNicVO`、`UsedIpVO` + +=== VM Start/Reboot 时的资源管理 + +`SdnControllerManagerImpl` 实现了 `PreVmInstantiateResourceExtensionPoint` 和 `VmReleaseResourceExtensionPoint`: + +* `preInstantiateVmResource()`:VM 启动/重启时,通过 vSwitchType 查找 sdnControllerType,调用 `SdnControllerL2.addVmNics()`。 + ZNS 场景下:dpdkvhostuserclient 网卡与 OVN 逻辑端口处理一致;VNIC 网卡无需额外操作(addVmNics 中按网卡类型判断即可)。 +* `releaseVmResource()`:VM 销毁/detachNic 时,调用 `SdnControllerL2.removeVmNics()`。 + ZNS 场景下:dpdkvhostuserclient 网卡与 OVN 逻辑端口处理一致;VNIC 网卡无需额外操作。 + +=== VM迁移 + +迁移流程(`VmMigrationCheckL2NetworkOnHostFlow` → `VmAllocateHostForMigrateVmFlow` → `VmMigrateOnHypervisorFlow`)中: + +* `VmMigrationCheckL2NetworkOnHostFlow`:检查目标主机是否关联了 VM 所需的 L2 网络(通过 `L2NetworkClusterRefVO`),ZNS 网络无需额外处理 +* 迁移时 ZNS segment port 不需要做操作,ZNS 的 segment port 不绑定特定物理机信息 +* dpdkvhostuserclient 类型网卡:与 OVN 端口一样,如果 OVN 在迁移时有特殊处理(如 `postMigrateVm` 扩展点),ZNS 也需要相同处理 + +=== ChangeVmNicNetwork(换网操作) + +`APIChangeVmNicNetworkMsg` 涉及 detach 旧网络 + attach 新网络: + +* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络 +* 从 ZNS 网络变换成 ZNS 网络的场景,需要调用 ZNS API 删除旧的 segment port,调用 API 创建新的 segment port,并更新 `VmNicVO`/`UsedIpVO` 等相关数据对象 + +=== FilterAttachableL3NetworkExtensionPoint + +OVN 实现了此扩展点用于过滤可挂载的 L3 网络。ZNS 也需要实现此扩展点: + +* 过滤逻辑:确保只有 ZNS SDN Controller 关联的物理机上的 VM 才能挂载 ZNS L3 网络 +* 在 `ZnsSdnControllerFactory` 或独立的扩展类中实现 + diff --git a/docs/modules/network/pages/networkResource/networkResource.adoc b/docs/modules/network/pages/networkResource/networkResource.adoc index 9aa66ce7341..b0fbf015282 100644 --- a/docs/modules/network/pages/networkResource/networkResource.adoc +++ b/docs/modules/network/pages/networkResource/networkResource.adoc @@ -2,4 +2,5 @@ * xref:networkResource/L2Network.adoc[] * xref:networkResource/L3Network.adoc[] -* xref:networkResource/VpcRouter.adoc[] \ No newline at end of file +* xref:networkResource/VpcRouter.adoc[] +* xref:networkResource/ZnsIntegration.adoc[] diff --git a/header/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.java b/header/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.java index e72b0b91396..8ac6141da69 100755 --- a/header/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.java +++ b/header/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.java @@ -22,6 +22,8 @@ public interface L2NetworkConstant { public static final String HARDWARE_VXLAN_NETWORK_TYPE = "HardwareVxlanNetwork"; public static final String L2_TF_NETWORK_TYPE = "TfL2Network"; @PythonClass + public static final String L2_GENEVE_NETWORK_TYPE = "L2GeneveNetwork"; + @PythonClass public static final String VXLAN_NETWORK_TYPE = "VxlanNetwork"; @PythonClass public static final String VXLAN_NETWORK_POOL_TYPE = "VxlanNetworkPool"; @@ -37,6 +39,8 @@ public interface L2NetworkConstant { @PythonClass public static final String VSWITCH_TYPE_OVN_DPDK = "OvnDpdk"; + @PythonClass + public static final String VSWITCH_TYPE_ZNS = "ZNS"; public static final String OVN_DPDK_VNIC_SRC_PATH = "/var/run/openvswitch/"; public static final String DETACH_L2NETWORK_CODE = "l2Network.detach"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.java index 9ec57078f6e..2872218ffe4 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.java @@ -235,7 +235,8 @@ public NicTO completeNicInformation(L2NetworkInventory l2Network, L3NetworkInven to.setBridgeName(makeBridgeName(l2Network.getUuid())); to.setPhysicalInterface(l2Network.getPhysicalInterface()); to.setMtu(new MtuGetter().getMtu(l3Network.getUuid())); - if (l2Network.getvSwitchType().equals(L2NetworkConstant.VSWITCH_TYPE_OVN_DPDK)) { + if (l2Network.getvSwitchType().equals(L2NetworkConstant.VSWITCH_TYPE_OVN_DPDK) + || l2Network.getvSwitchType().equals(L2NetworkConstant.VSWITCH_TYPE_ZNS)) { to.setSrcPath(L2NetworkConstant.OVN_DPDK_VNIC_SRC_PATH + nic.getInternalName()); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java index 15c0214c538..ee77877951a 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java @@ -248,7 +248,8 @@ public NicTO completeNicInformation(L2NetworkInventory l2Network, L3NetworkInven to.setMetaData(String.valueOf(vlanId)); to.setMtu(new MtuGetter().getMtu(l3Network.getUuid())); to.setVlanId(String.valueOf(vlanId)); - if (l2Network.getvSwitchType().equals(L2NetworkConstant.VSWITCH_TYPE_OVN_DPDK)) { + if (l2Network.getvSwitchType().equals(L2NetworkConstant.VSWITCH_TYPE_OVN_DPDK) + || l2Network.getvSwitchType().equals(L2NetworkConstant.VSWITCH_TYPE_ZNS)) { to.setSrcPath(L2NetworkConstant.OVN_DPDK_VNIC_SRC_PATH + nic.getInternalName()); } From d4c53181d0bf6418d0692bdef107e93621e770f9 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 19 Mar 2026 21:06:40 +0800 Subject: [PATCH 2/4] [sdnController]: ususu Resolves: ZCF-1365 Change-Id: I626968656f6c6966796a616b6e736d696e647966 --- conf/db/upgrade/V5.5.18__schema.sql | 2 +- .../pages/networkResource/ZnsIntegration.adoc | 357 ++++++++++-------- .../sdnController/SdnControllerFactory.java | 2 - .../h3cVcfc/H3cVcfcSdnControllerFactory.java | 6 - .../controller/SugonSdnControllerFactory.java | 5 - .../zstack/testlib/SdnControllerSpec.groovy | 144 +++++++ 6 files changed, 350 insertions(+), 166 deletions(-) diff --git a/conf/db/upgrade/V5.5.18__schema.sql b/conf/db/upgrade/V5.5.18__schema.sql index 6f362aeab68..30c59276d8d 100644 --- a/conf/db/upgrade/V5.5.18__schema.sql +++ b/conf/db/upgrade/V5.5.18__schema.sql @@ -10,6 +10,6 @@ CREATE TABLE IF NOT EXISTS `L2GeneveNetworkVO` ( `uuid` varchar(32) NOT NULL, `geneveId` int(10) unsigned NOT NULL, PRIMARY KEY (`uuid`), - CONSTRAINT `fkL2GeneveNetworkVOL2NetworkVO` FOREIGN KEY (`uuid`) REFERENCES `L2NetworkVO` (`uuid`) ON DELETE CASCADE + CONSTRAINT `fkL2GeneveNetworkVOL2NetworkEO` FOREIGN KEY (`uuid`) REFERENCES `L2NetworkEO` (`uuid`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc index 12be07488d0..dbf38959d8f 100644 --- a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -2,13 +2,56 @@ == 总体描述 -在考虑对接ZNS SDN控制器时,首先需要考虑如何做好Cloud资源对象和ZNS资源映射。 +在考虑对接 ZNS SDN 控制器时,首先需要考虑如何做好 Cloud 资源对象和 ZNS 资源映射。 * ZNS segments → L2Network + L3Network + IpRange * ZNS segments port → VmNic + UsedIp * ZNS segments + transport zone → L2NetworkClusterRefVO -因为最终会把ZNS UI嵌套到Cloud UI中,其他资源对象不需要映射。 +=== CMS +Cloud L2 和 ZNS segment 之间的资源如何一一对应? +引入一个 CMS 数据结构。 + +[source,go] +---- + type Cms struct { + CmsUuid string + Type string ### cloud/zsv/zaku/zns + IP string ### cloud mn vip + Role string ###owner, user + CmsResourceUuid string ###owner, user +} + +type Segment { + ... + CmsMetaDatas []Cms `json:"cms"` +} +---- +* Cms::Type 定义 cms 类型:Cloud、ZSV、ZAKU、ZNS +* Cms::IP 定义 cms 的 IP 地址,该字段主要为了让维护人员更友好地识别资源,cms 的唯一标识仍是 CmsUuid +* Cms::CmsUuid 定义 cmsUuid。该值是 ZNS 上创建的 Computer Manager UUID,Cloud 在创建 SdnController 时会把这个 UUID 记录到 SdnController 的 systemTag 中 +* Cms::Role 定义 cms 是这个资源的 owner 还是 user。owner 表示该 cms 创建了资源,user 表示该 cms 使用了资源。一个资源可以在多个 cms 之间共享,比如一个 segment 可以被多个 Cloud 使用。资源删除有两种策略: +** 只有当所有 cms 都不使用该资源时,才可以删除(以 CmsMetaDatas 作为资源引用基数); +** 只要 owner 删除了该资源,就删除该资源。必须等待 user 删除后,owner 才能删除 +* Cms::CmsResourceUuid 定义该 cms 系统对应的资源 UUID,在数据同步过程中用于映射 Cms 资源和 ZNS 资源。 + +=== ZNS API + +ZNS API 定义需要包含 Cms 数据: + +* 创建 API:cms 根据自己的资源创建 cms 数据,ZNS 存入数据库 +* 删除 API:cms 根据资源 UUID 删除对应的 cms 数据,ZNS 根据结果执行删除操作 +* 查询 API:需要支持根据 cmsUuid 过滤资源 +* 修改操作者 API:ZNS 可以提供 API 添加/删除资源 user + +[NOTE] +不是所有 API 都需要以上全部操作。 + +=== ZNS 数据库对象 + +不是所有资源对象都需要保持 cms 信息,但是由 cms 创建的资源,或者需要按 cms 查询的资源,都需要保存 cms 信息。 +一个资源可能有多个 cms 信息,表示资源可在多个 cms 之间共享。 + == ZNS SDN控制器 @@ -19,93 +62,188 @@ ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`S * vendorType:ZNS * vendorVersion:1.0 -=== 新增类说明 +[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 信息。 -[cols="1,2"] +=== 创建SDN控制器 + +* 根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于当前 Computer Manager 管理的 cluster,且 `managementIp != null`: +** `HostData.managementIp`:匹配 Cloud `HostVO.managementIp`,找到对应 `HostVO.uuid` +** `HostData.clusterId`:即 Cloud `ClusterVO.uuid`(ZNS 侧直接使用 Cloud 的 cluster UUID,无需 name 匹配) +** `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`: +** `HostSwitchProfileData.type`:枚举值 `dpdk` 或 `kernel`,用于推导 `SdnControllerHostRefVO.vSwitchType` +** `HostSwitchProfileData.transportZoneIds`:关联的 transport zone UUID 列表,建立反向缓存 `transportZoneUuid → Set` + +[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 -|`ZnsControllerVO` -|继承 `SdnControllerVO`,无额外字段,需要建表SQL(仅uuid主键关联) +|`dpdk` +|`OvsDpdk` -|`ZnsSdnControllerFactory` -|实现 `SdnControllerFactory` 接口,注册 vendorType 为 "ZNS";`getSdnControllerSecurityGroup()` 返回null(ZNS不需要安全组SDN后端);`getSdnControllerDhcp()` 返回null(ZNS不配置DHCP) +|`kernel` +|`OvsKernel` -|`ZnsSdnController` -|实现 `SdnController` 接口,处理控制器生命周期(创建/删除/重连);addHost/removeHost 仅操作数据库,不修改物理机配置 +|其它未知值,或者 profile 查询失败 +|`ZNS` -|`ZnsSdnControllerL2` -|实现 `SdnControllerL2` 接口;createL2Network 调用ZNS API创建segment;deleteL2Network 调用ZNS API删除segment;attachL2NetworkToCluster/detachL2NetworkFromCluster 调用ZNS API修改segment的transport zone关联;addVmNics() 调用ZNS API创建segment port;removeVmNics() 调用ZNS API删除segment port;其它函数空实现 +|=== +* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,建立了 ZNS Host 和 Cloud Host 的映射关系。 +* 根据 computerManagerUuid 从 ZNS 获取 segments 列表。 +** 根据 segment 信息创建 L2Network、L3Network +** 根据 segment.ipam 信息创建 IpRange -|`ZnsSdnControllerL3` -|实现 `SdnControllerL3` 接口;createIpRange() 调用ZNS API修改segment的cidr;deleteIpRange() 调用ZNS API修改segment的cidr;createL3Network()/deleteL3Network() 空实现 +ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: -|`ZnsSdnControllerConstant` -|定义常量 `ZNS_CONTROLLER = "ZNS"` +[cols="1,2,2,2"] +|=== +|ZNS字段 |条件 |Cloud L2Network.type |Cloud L3Network.type + +|`transport_type` +|`overlay` +|`L2GeneveNetwork` +|`L3ZnsNetwork` + +|`transport_type` +|`vlan` 且 `virtual_network_id > 0` +|`L2VlanNetwork` +|`L3ZnsNetwork` + +|`transport_type` +|`vlan` 且 `virtual_network_id == 0` +|`L2NoVlanNetwork` +|`L3ZnsNetwork` |=== -=== 创建SDN控制器 +补充说明: -必须先在ZNS完成添加 Computer Manager 的操作,才能在Cloud侧创建对应的 SdnController。 +* `L2Network.vSwitchType` 固定写入 `ZNS` +* `L2Network.virtualNetworkId` 取 `segment.virtual_network_id`(Geneve/VLAN) +* `L3Network.category` 当前初始化逻辑固定为 `Private` -UI调用 `APIAddSdnControllerMsg`,同时携带一个 SystemTags:`computerManagerUuid::xxxx`,Cloud侧根据这个tag把 Computer Manager 和 SdnController 关联起来。Cloud后续API操作,会把 computerManagerUuid 作为 cms uuid 传给ZNS,用来区分ZNS的segment、segment port是哪个cloud创建的。 +5. 根据 `segment.transport_zone_uuid` 查询前面缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 -在添加 SdnController 的过程中,`initSdnController` 函数实现如下逻辑: +`L2NetworkClusterRefVO` 的映射关系如下: -1. 根据 Computer Manager 的uuid获取ZNS Host列表,配置 `SdnControllerHostRefVO`,关联到 `SdnControllerVO` -2. 根据 computerManagerUuid 获取ZNS segment列表,配置 `L2NetworkVO`、`L3NetworkVO`、`IpRangeVO` -3. 根据ZNS segment和transport zone的关系,配置 `L2NetworkClusterRefVO`,关联 `L2NetworkVO` 和 `ClusterVO` +[cols="2,3,2"] +|=== +|字段 |来源 |说明 -[NOTE] -ZNS 为 Cloud 生成一个 Computer Manager,Cloud 通过这个 Computer Manager 的 uuid 来区分不同 Cloud 的 ZNS 资源。 -ZNS 的资源(如 segment、segment port 等)都必须携带 cms 信息,一个资源可以管理多个 cms 信息,实现同一个资源在多个 Cloud 共享。 +|`l2NetworkUuid` +|当前 segment 创建出的 `L2NetworkVO.uuid` +|当前 L2 网络 UUID -=== 删除SDN控制器 +|`clusterUuid` +|`HostData.clusterId`(经 transport zone 反向缓存查找) +|`segment.transport_zone_uuid` → 缓存 → `Set` + +|`l2ProviderType` +|常量 `ZNS` +|固定值 + +|=== + +=== 重连SDN控制器 + +重连(`APIReconnectSdnControllerMsg`)与创建的核心区别: + +* **创建**:ZNS 是数据源,Cloud 单向从 ZNS 读取资源,并在 Cloud 侧新建 L2Network / L3Network / IpRange / `SdnControllerHostRefVO` / `L2NetworkClusterRefVO` 等对象。 +* **重连**:Cloud 数据库是基准,*不新建* Cloud 资源;仅对 `SdnControllerHostRefVO` 做 upsert,并将 ZNS 侧的 segment / segment port 状态对齐到 Cloud 侧。 + +==== 1. 刷新 SdnControllerHostRefVO(upsert) + +重连仍需重新扫描 host,以处理新加入的主机或 vSwitchType 发生变化的主机。 +映射关系与创建时完全相同(`HostData.managementIp` → `HostVO`,`HostSwitchProfileData.type` → `vSwitchType`), +区别仅在于写库操作改为 upsert: -和其它类型的 SdnController 一样,删除 `SdnControllerVO`、`SdnControllerHostRefVO`、`L2NetworkVO`、`L3NetworkVO`、`IpRangeVO`、`L2NetworkClusterRefVO` 等相关数据对象,但不需要执行删除物理机 OVS DPDK 操作。 +[cols="3,2"] +|=== +|情况 |操作 + +|`SdnControllerHostRefVO` 不存在(新主机) +|INSERT 新记录 -=== 同步 +|已存在但 `vSwitchType` 或 `vtepIp` 发生变化 +|UPDATE -Cloud 的 API 不是声明式 API,不能保证 Cloud 的数据库和 ZNS 数据的一致性,因此 Cloud 的资源可能和 ZNS 的资源不一致。 -为此,`APIReconnectSdnControllerMsg` 用来同步 Cloud 和 ZNS 的资源状态,确保 Cloud 数据库和 ZNS 的资源状态一致,动作包含: +|已存在且字段未变 +|跳过 -1. Cloud 读取属于当前 Cloud 的 Segment 列表(查询时携带 Computer Manager 的 uuid): -** 如果 ZNS 存在,但 Cloud 不存在,且 ZNS 的 segment 也没有其它 cms 使用,调用 API 删除 ZNS segment -** 如果 ZNS 存在,Cloud 也存在,需要比较两侧的参数,如果不一致,更新 ZNS 侧的参数(如 cidr) -** 如果 ZNS 不存在,但 Cloud 存在,调用 API 添加 ZNS segment,根据 L3 信息添加 segment cidr +|=== -2. 完成 Segment 同步后,Cloud 读取属于当前 Cloud 的 Segment port 列表(查询时携带 Computer Manager 的 uuid): -** 如果 ZNS 不存在,但 Cloud 存在,调用 ZNS segment port API 创建 segment port -** 如果 ZNS 存在,但 Cloud 不存在,调用 ZNS segment port API 删除 segment port -** 如果 ZNS 存在,Cloud 也存在,需要比较两侧的参数,如果不一致,更新 ZNS 侧参数为 Cloud 侧参数 +[NOTE] +创建时只做 INSERT;重连时使用 upsert,可处理主机上下线及 vSwitchType 变更的情况。 -=== Cloud调用ZNS API +==== 2. Segment 协调(以 Cloud 为基准) -本章节仅列 Cloud 主动调用 ZNS 的接口,不包含 callback 接口。 +重连以 Cloud 数据库中所有 `vSwitchType = ZNS` 的 `L2NetworkVO` 为基准,与 ZNS 侧属于本 Computer Manager 的 segment 做三路对比。 +ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关联。 -==== Segment +[cols="2,2,3"] +|=== +|Cloud 侧 L2NetworkVO |ZNS 侧 segment |操作 -* `GET /zns/api/v1/segments` -* `GET /zns/api/v1/segments/{uuid}` -* `POST /zns/api/v1/segments` -* `PATCH /zns/api/v1/segments/{uuid}` -* `DELETE /zns/api/v1/segments` +|不存在(`l2Uuid` 指向已删除 L2,或 segment 无 `l2Uuid`) +|存在 +|调用 `DELETE /zns/api/v1/segments` 删除孤儿 segment -==== Segment Port +|存在 +|不存在 +|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3 信息 -* `GET /zns/api/v1/segments/{uuid}/ports` -* `GET /zns/api/v1/segments/{uuid}/ports/{portUuid}` -* `POST /zns/api/v1/segments/{uuid}/ports` -* `PATCH /zns/api/v1/segments/{uuid}/ports/{portUuid}` -* `DELETE /zns/api/v1/segments/{uuid}/ports` +|存在 +|存在但参数不一致(如 CIDR) +|调用 `PATCH /zns/api/v1/segments/{uuid}` 更新 -==== Segment Used IP +|存在 +|存在且参数一致 +|无操作 -* `GET /zns/api/v1/segments/{uuid}/used-ips` +|=== [NOTE] -查询某个 segment 下全部 segment port 时,Cloud 使用:`GET /zns/api/v1/segments/{uuid}/ports?offset=0&limit=-1` +重连 *不会* 根据 ZNS segment 在 Cloud 侧创建新的 L2Network / L3Network / IpRange / `L2NetworkClusterRefVO`。 +如果 ZNS 存在但 Cloud 不存在,视为孤儿 segment 并删除(与创建阶段的单向导入方向相反)。 + +==== 3. Segment Port 协调 + +完成 segment 协调后,对每个已与 Cloud L2 匹配的 segment,逐一协调其 port。 +Port 通过 cms 元数据中的 `ExternalIds.vmNicUuid` 与 Cloud `VmNicVO` 关联。 + +[cols="2,2,3"] +|=== +|Cloud 侧 VmNicVO |ZNS 侧 segment port |操作 + +|存在 +|不存在 +|调用 `POST /zns/api/v1/segments/{uuid}/ports` 补建 port + +|不存在 +|存在 +|调用 `DELETE /zns/api/v1/segments/{uuid}/ports` 删除孤儿 port + +|两侧均存在 +|(当前实现不做参数比对更新) +|无操作 + +|=== + +=== 删除SDN控制器 +删除 ZNS SDN Controller 时,会级联删除 ZNS 侧的 Computer Manager 和 Segment、Segment Port 等资源。Cloud 侧的 L2Network、L3Network、VmNic 等也会一起删除。 == L2Network @@ -138,52 +276,7 @@ ZNS L2Network 的类型定义: |=== -[IMPORTANT] -新增的 `VSwitchType("ZNS")` 必须设置 sdnControllerType 为 "ZNS",即 `new VSwitchType("ZNS").setSdnControllerType("ZNS")`。 -这是 `SdnControllerManagerImpl` 判断 L2 网络是否归 SDN Controller 管理的关键属性,影响 `preInstantiateVmResource`、`releaseVmResource`、`instantiateResourceOnAttachingNic`、`releaseResourceOnDetachingNic` 等所有扩展点的正确路由。 - -=== 新增L2GeneveNetwork注册 - -需要新增以下类: - -[cols="1,2"] -|=== -|类名 |说明 - -|`L2GeneveNetworkVO` -|继承 `L2NetworkVO`,增加 `geneveId` 字段(类似 `L2VlanNetworkVO` 的 vlan 字段),需要建表SQL - -|`L2GeneveNetworkInventory` -|对应的 Inventory 类 - -|`L2GeneveNetworkFactory` -|实现 `L2NetworkFactory` 接口,注册 `new L2NetworkType("L2GeneveNetwork")` - -|`L2GeneveNetwork` -|继承 `L2NoVlanNetwork`,处理 L2GeneveNetwork 的消息路由 - -|`L2NetworkConstant` 新增常量 -|`L2_GENEVE_NETWORK_TYPE = "L2GeneveNetwork"` - -|`ZnsVmNicFactory` -|注册 `new VSwitchType("ZNS")`,绑定对应的 VmNicType - -|=== - -=== KVM Realize Backend - -需要为 L2GeneveNetwork 注册 KVM 后端实现: - -`KVMRealizeL2GeneveNetworkBackend`:实现 `KVMCompleteNicInformationExtensionPoint` 接口 - -* 按 `L2NetworkType("L2GeneveNetwork")` 注册到 `KVMHostFactory` 的 `completeNicInfoExtensions` 映射中 -* `completeNicInformation()` 方法:填充 NicTO 的 `bridgeName`、`physicalInterface`、`mtu` 等信息;对于 OvnDpdk 模式需要设置 `srcPath`(与 `KVMRealizeL2NoVlanNetworkBackend` 中 OvnDpdk 的处理逻辑一致) -* `realize/check/delete` 方法:由于 ZNS/OVS 管理 bridge,这些方法可以做空实现,但必须注册,否则 attach L2 到 cluster 或 host reconnect 时会因找不到 backend 而失败 - -[NOTE] -如果 L2NoVlanNetwork 和 L2VlanNetwork 类型的 ZNS 网络复用现有 Backend,需确认这些 backend 能正确处理 vSwitchType 为 ZNS 的情况。 - -=== 创建L2Network +=== 创建 L2Network 处理逻辑类似 OVN Controller,但调用 ZNS API 创建 segment。 @@ -197,9 +290,6 @@ ZNS L2Network 的类型定义: * L2VlanNetwork、L2NoVlanNetwork 类型支持 * 仅需要修改 L2NetworkVO 数据库,不需要下发到物理机,需要调用修改 ZNS segment API -[NOTE] -`AttachedL2NetworkAllocatorFlow` 根据虚拟机选择的 L2 网络选择候选的物理机,它根据 `L2NetworkClusterRefVO` 找到 L2Network 关联的 cluster,从而找到 cluster 内的物理机器作为候选机器。 - == L3Network === 基础信息 @@ -226,17 +316,7 @@ ZNS L3 的定义: |=== -ZNS L3 不配置 DHCP 网络服务,因此 `enableIpAddressAllocation()` 为 false。 - -[IMPORTANT] -当前 `enableIpAddressAllocation()` 实现中,L3VpcNetwork 类型会返回 true(因为 type != L3BasicNetwork)。 -需要调整该逻辑:当 L3Network 关联的 L2Network 的 vSwitchType 为 ZNS 时返回 false。 - -ZNS L3 API 不需要调用 SDN backend。`ZnsSdnControllerFactory.getSdnControllerL3()` 返回 null,`SdnControllerManagerImpl` 中 `getSdnControllerL3()` 在 controllerUuid 为 null 或 factory 返回 null 时会自然跳过,不影响 L3 CRUD 操作。 - -ZNS L3 不需要配置任何网络服务(无 DHCP、无 DNS、无 UserData、无 EIP、无 PortForwarding 等)。 - -Cloud 侧的 IpRange 只做记录,不参与 IP 分配,IP 由 ZNS 负责管理和分配。`SdnControllerManagerImpl` 的 `afterAddIpRange`/`afterDeleteIpRange` 会调用 `SdnControllerL2` 的 `addL3NetworkIpRange`/`deleteL3NetworkIpRange`,`ZnsSdnControllerL2` 中这两个方法做空实现。 +ZNS L3Network 不添加网络服务。 === SetVmStaticIp / ChangeVmIp 操作 @@ -246,39 +326,26 @@ Cloud 侧的 IpRange 只做记录,不参与 IP 分配,IP 由 ZNS 负责管 == VmNic -VmNicType 的值有:VNIC、VF、dpdkvhostuserclient。ZNS 可能是 dpdk 模式,也可能是 kernel 模式,在 UI 选择 ZNS 网络以后,用户可以选择网卡类型:VNIC 或 dpdkvhostuserclient。 +VmNicType 的值有:VNIC、VF、`dpdkvhostuserclient`。ZNS 可能是 dpdk 模式,也可能是 kernel 模式。在 UI 选择 ZNS 网络后,用户可以选择网卡类型:VNIC 或 `dpdkvhostuserclient`。 === 虚拟机的物理机分配 创建虚拟机选择了 ZNS 网络时: * 默认网卡类型是 VNIC,需要选择到部署了 OvnKernel 的物理机 -* 如果选择了 dpdkvhostuserclient,需要选择到部署了 OvnDpdk 的物理机 - -`AttachedL2NetworkAllocatorFlow` 会调用 `AttachedL2NetworkAllocatorExtensionPoint` 扩展点进一步选择物理机,该扩展点根据 L2 找到 ZNS 控制器,根据 `SdnControllerHostRefVO` 找到物理机: - -* 如果网卡是 VNIC,选择 vSwitchType 是 OvnKernel 的物理机 -* 如果网卡是 dpdkvhostuserclient,选择 vSwitchType 是 OvnDpdk 的物理机 +* 如果选择了 `dpdkvhostuserclient`,需要选择到部署了 OvnDpdk 的物理机 === 网卡创建过程 -`VmAllocateNicFlow`/`ApplianceVmAllocateNicFlow` 分别是创建虚拟机、ApplianceVm 过程中创建网卡的过程。ApplianceVm 可能使用 ZNS 网络,因此 `ApplianceVmAllocateNicFlow` 也需要适配 ZNS 流程。 +创建虚拟机或给虚拟机添加网卡时,会调用 `VmAllocateNicFlow` 创建网卡。 ZNS 网络创建过程: 1. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType -2. 调用 ZNS 创建 segment port API,获取 ip/掩码/网关、ip6/前缀/网关,cms 信息中携带 computerManagerUuid +2. 调用 ZNS 创建 segment port API,返回 ip/掩码/网关、ip6/前缀/网关 3. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 4. 根据获取的参数创建 `VmNicVO`、`UsedIpVO` -[NOTE] -`VmAllocateNicIpFlow`(在 `VmAllocateNicFlow` 之后执行)负责给已创建的 Nic 分配 IP。 -对于 ZNS 网络,由于 `enableIpAddressAllocation()` 为 false 且 IP 已在 `VmAllocateNicFlow` 中通过 ZNS API 获取并保存,`VmAllocateNicIpFlow` 会跳过这些 Nic,不会重复处理。 - -==== 网卡创建失败回滚 - -如果调用 ZNS segment port API 成功获取到 IP,但后续创建 `VmNicVO`/`UsedIpVO` 失败,需要回滚:在 `VmAllocateNicFlow` 的 rollback 方法中检查是否已调用 ZNS 分配了 segment port,如果是则调用 ZNS 删除 segment port。 - === 网卡删除过程 * `VmReturnReleaseNicFlow`:在 `destroyVmWorkFlowElements` 中被调用,用于虚拟机销毁时释放网卡资源 @@ -289,22 +356,10 @@ ZNS 网络创建过程: 1. 调用 ZNS 删除 segment port API 2. 删除 `VmNicVO`、`UsedIpVO` -=== VM Start/Reboot 时的资源管理 - -`SdnControllerManagerImpl` 实现了 `PreVmInstantiateResourceExtensionPoint` 和 `VmReleaseResourceExtensionPoint`: - -* `preInstantiateVmResource()`:VM 启动/重启时,通过 vSwitchType 查找 sdnControllerType,调用 `SdnControllerL2.addVmNics()`。 - ZNS 场景下:dpdkvhostuserclient 网卡与 OVN 逻辑端口处理一致;VNIC 网卡无需额外操作(addVmNics 中按网卡类型判断即可)。 -* `releaseVmResource()`:VM 销毁/detachNic 时,调用 `SdnControllerL2.removeVmNics()`。 - ZNS 场景下:dpdkvhostuserclient 网卡与 OVN 逻辑端口处理一致;VNIC 网卡无需额外操作。 - -=== VM迁移 - -迁移流程(`VmMigrationCheckL2NetworkOnHostFlow` → `VmAllocateHostForMigrateVmFlow` → `VmMigrateOnHypervisorFlow`)中: +=== DPDK 网卡的特殊处理 -* `VmMigrationCheckL2NetworkOnHostFlow`:检查目标主机是否关联了 VM 所需的 L2 网络(通过 `L2NetworkClusterRefVO`),ZNS 网络无需额外处理 -* 迁移时 ZNS segment port 不需要做操作,ZNS 的 segment port 不绑定特定物理机信息 -* dpdkvhostuserclient 类型网卡:与 OVN 端口一样,如果 OVN 在迁移时有特殊处理(如 `postMigrateVm` 扩展点),ZNS 也需要相同处理 +由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 +这个逻辑与 OVN DPDK 虚拟网卡一致。 === ChangeVmNicNetwork(换网操作) @@ -315,8 +370,6 @@ ZNS 网络创建过程: === FilterAttachableL3NetworkExtensionPoint -OVN 实现了此扩展点用于过滤可挂载的 L3 网络。ZNS 也需要实现此扩展点: +* `APIGetVmAttachableL3NetworkMsg` 必须能获取到 ZNS L3 网络。 -* 过滤逻辑:确保只有 ZNS SDN Controller 关联的物理机上的 VM 才能挂载 ZNS L3 网络 -* 在 `ZnsSdnControllerFactory` 或独立的扩展类中实现 diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerFactory.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerFactory.java index 6c25dcaddb9..95604b61646 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerFactory.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerFactory.java @@ -10,8 +10,6 @@ public interface SdnControllerFactory { SdnControllerType getVendorType(); - SdnControllerVO persistSdnController(SdnControllerVO vo); - SdnController getSdnController(SdnControllerVO vo); default SdnController getSdnController(String l2NetworkUuid) {return null;}; diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/h3cVcfc/H3cVcfcSdnControllerFactory.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/h3cVcfc/H3cVcfcSdnControllerFactory.java index 2d297ca518c..7647a086824 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/h3cVcfc/H3cVcfcSdnControllerFactory.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/h3cVcfc/H3cVcfcSdnControllerFactory.java @@ -20,12 +20,6 @@ public SdnControllerType getVendorType() { return sdnControllerType; } - @Override - public SdnControllerVO persistSdnController(SdnControllerVO vo) { - vo = dbf.persistAndRefresh(vo); - return vo; - } - @Override public SdnController getSdnController(SdnControllerVO vo) { diff --git a/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/controller/SugonSdnControllerFactory.java b/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/controller/SugonSdnControllerFactory.java index b277a336c3d..fad3e58f886 100644 --- a/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/controller/SugonSdnControllerFactory.java +++ b/plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/controller/SugonSdnControllerFactory.java @@ -21,11 +21,6 @@ public SdnControllerType getVendorType() { return sdnControllerType; } - @Override - public SdnControllerVO persistSdnController(SdnControllerVO vo) { - vo = dbf.persistAndRefresh(vo); - return vo; - } @Override public SdnController getSdnController(SdnControllerVO vo) { diff --git a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy index 25b24d7f412..f2b042d3fe7 100644 --- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy @@ -1,6 +1,9 @@ package org.zstack.testlib +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.zstack.sdk.SdnControllerInventory import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands @@ -25,6 +28,9 @@ import org.zstack.sugonSdnController.controller.api.types.Project import org.zstack.sugonSdnController.controller.api.types.VirtualMachine import org.zstack.sugonSdnController.controller.api.types.VirtualMachineInterface import org.zstack.sugonSdnController.controller.api.types.VirtualNetwork +import org.zstack.utils.gson.JSONObjectUtil + +import javax.servlet.http.HttpServletRequest /** * Created by shixin.ruan on 2019/09/26. @@ -289,6 +295,144 @@ class SdnControllerSpec extends Spec implements HasSession { ResponseEntity response = new ResponseEntity(json, HttpStatus.OK); return response.getBody() } + + // ===================== ZNS Simulators ===================== + + // Helper: trigger ZNS async callback in a separate thread + def triggerZnsCallback = { HttpEntity entity, EnvSpec spec, Object data -> + String jobUuid = entity.headers.getFirst("x-job-uuid") + String webhook = entity.headers.getFirst("x-web-hook") + if (!jobUuid || !webhook) return + + Thread.start { + Thread.sleep(100) + def cmd = [taskUuid: jobUuid, success: true, status: "completed", data: data] + def headers = new HttpHeaders() + headers.setContentType(MediaType.APPLICATION_JSON) + def body = JSONObjectUtil.toJsonString(cmd) + headers.set("commandpath", "/zns/callback") + spec.restTemplate.exchange(webhook, HttpMethod.POST, + new HttpEntity(body, headers), String.class) + } + } + + // GET /zns/api/v1/fabric/compute-managers/{uuid} + simulator("/zns/api/v1/fabric/compute-managers/[^/]+") { + return [success: true, data: [uuid: "cm-uuid-1", name: "cm-1", connectionStatus: "connected"]] + } + + // GET /zns/api/v1/fabric/compute-collections + simulator("/zns/api/v1/fabric/compute-collections") { + return [success: true, data: [ + [uuid: "cc-1", name: "cluster-1", computeManagerId: "cm-uuid-1"], + [uuid: "cc-2", name: "cluster-2", computeManagerId: "cm-uuid-1"] + ], total: 2] + } + + // GET /zns/api/v1/fabric/discovered-nodes + simulator("/zns/api/v1/fabric/discovered-nodes") { + return [success: true, data: [ + [uuid: "dn-1", name: "kvm-1", managementIp: "127.0.0.1", clusterId: "cc-1", transportNodeProfileId: "tnp-dpdk"], + [uuid: "dn-2", name: "kvm-2", managementIp: "127.0.0.2", clusterId: "cc-1", transportNodeProfileId: "tnp-dpdk"] + ], total: 2] + } + + // GET /zns/api/v1/fabric/transport-zones + simulator("/zns/api/v1/fabric/transport-zones") { + return [success: true, data: [ + [uuid: "tz-1", name: "tz-overlay", type: "overlay"] + ], total: 1] + } + + // GET /zns/api/v1/fabric/transport-node-profiles/{uuid} + simulator("/zns/api/v1/fabric/transport-node-profiles/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def profileUuid = uri.tokenize("/").last() + if (profileUuid == "tnp-kernel") { + return [success: true, data: [uuid: "tnp-kernel", name: "tnp-kernel", hostSwitchProfiles: ["hsp-kernel"]]] + } + return [success: true, data: [uuid: "tnp-dpdk", name: "tnp-dpdk", hostSwitchProfiles: ["hsp-dpdk"]]] + } + + // GET /zns/api/v1/fabric/host-switch-profiles/{uuid} + simulator("/zns/api/v1/fabric/host-switch-profiles/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def hspUuid = uri.tokenize("/").last() + if (hspUuid == "hsp-kernel") { + return [success: true, data: [uuid: "hsp-kernel", name: "hsp-kernel", switchType: "kernel"]] + } + return [success: true, data: [uuid: "hsp-dpdk", name: "hsp-dpdk", switchType: "dpdk"]] + } + + // /zns/api/v1/segments — GET(list) / POST(create) / DELETE(batch) + simulator("/zns/api/v1/segments") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + if (req.method == "GET") { + return [success: true, data: [ + [uuid: "zns-sg-1", name: "sg-1", transport_type: "overlay", virtual_network_id: 100, + transport_zone_uuid: "tz-1", + cms: [cmsUuid: "cm-uuid-1", ExternalIds: [:]], + ipams: [[ip_cidr: "10.0.1.0/24", gateway_ip: "10.0.1.1", prefix: 24, + ip_ranges: [[StartIp: "10.0.1.10", EndIp: "10.0.1.100"]]]]], + [uuid: "zns-sg-2", name: "sg-2", transport_type: "overlay", virtual_network_id: 200, + transport_zone_uuid: "tz-1", + cms: [cmsUuid: "cm-uuid-1", ExternalIds: [:]], + ipams: [[ip_cidr: "10.0.2.0/24", gateway_ip: "10.0.2.1", prefix: 24, + ip_ranges: [[StartIp: "10.0.2.10", EndIp: "10.0.2.100"]]]]] + ], total: 2] + } + // POST(create) or DELETE(batch) → async callback + def data = [uuid: "zns-sg-new", name: "new-seg", transport_type: "overlay"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid} — GET(single) / PATCH(update) + simulator("/zns/api/v1/segments/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def segUuid = uri.tokenize("/").last() + if (req.method == "GET") { + return [success: true, data: [uuid: segUuid, name: "seg-" + segUuid, transport_type: "overlay"]] + } + // PATCH → async callback + def data = [uuid: segUuid, name: "seg-" + segUuid, transport_type: "overlay"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid}/ports — GET(list) / POST(create) / DELETE(batch) + simulator("/zns/api/v1/segments/[^/]+/ports") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + if (req.method == "GET") { + return [success: true, data: [], total: 0] + } + // POST(create) or DELETE(batch) → async callback + def data = [uuid: "zns-port-new", segment_uuid: "zns-sg-1", mac: "00:00:00:00:00:01"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid}/ports/{portUuid} — GET / PATCH + simulator("/zns/api/v1/segments/[^/]+/ports/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def portUuid = uri.tokenize("/").last() + if (req.method == "GET") { + return [success: true, data: [uuid: portUuid, segment_uuid: "zns-sg-1"]] + } + // PATCH → async callback + def data = [uuid: portUuid, segment_uuid: "zns-sg-1"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid}/used-ips — GET + simulator("/zns/api/v1/segments/[^/]+/used-ips") { + return [success: true, data: [], total: 0] + } + + // POST /zns/api/v1/fabric/compute-managers/{uuid}/sync-jobs + simulator("/zns/api/v1/fabric/compute-managers/[^/]+/sync-jobs") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + triggerZnsCallback(entity, spec, [:]) + return [:] + } } } From 83a21f4bc5c6a09d373d02bdb3aef6064b81480b Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sun, 22 Mar 2026 13:02:39 +0800 Subject: [PATCH 3/4] [docs]: usuu Resolves: ZCF-1365 Change-Id: I716b787976776e7979736e617864726d786b756b --- .../pages/networkResource/ZnsIntegration.adoc | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc index dbf38959d8f..8531f8537c9 100644 --- a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -81,6 +81,50 @@ ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` ** `HostSwitchProfileData.type`:枚举值 `dpdk` 或 `kernel`,用于推导 `SdnControllerHostRefVO.vSwitchType` ** `HostSwitchProfileData.transportZoneIds`:关联的 transport zone UUID 列表,建立反向缓存 `transportZoneUuid → Set` +* 调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,并缓存到当前 `ZnsSdnControllerVO` 关联的数据中。建议新增 `ZnsTransportZone` 数据表,主要字段如下: ++ +[cols="2,3,2"] +|=== +|字段 |来源 |说明 + +|`isDefault` +|transport zone 返回字段 +|是否为默认 transport zone + +|`description` +|transport zone 返回字段 +|描述信息 + +|`name` +|transport zone 返回字段 +|transport zone 名称 + +|`physicalNetwork` +|transport zone 返回字段 +|物理网络标识 + +|`status` +|transport zone 返回字段 +|当前状态 + +|`tags` +|transport zone 返回字段 +|标签信息 + +|`type` +|transport zone 返回字段 +|类型,典型值为 `vlan` 或 `overlay` + +|`znsSdnControllerUuid` +|当前 `ZnsSdnControllerVO.uuid` +|外键,关联到所属 ZNS SDN Controller + +|=== + +[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` 枚举字段。 @@ -101,7 +145,9 @@ OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel` |`ZNS` |=== -* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,建立了 ZNS Host 和 Cloud Host 的映射关系。 +* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,同时持有两类 transport zone 缓存: +** transport zone 元数据缓存:保存到建议新增的 `ZnsTransportZone` +** `transportZoneUuid → Set` 反向缓存:用于 segment 和 cluster 的关联 * 根据 computerManagerUuid 从 ZNS 获取 segments 列表。 ** 根据 segment 信息创建 L2Network、L3Network ** 根据 segment.ipam 信息创建 IpRange @@ -280,6 +326,34 @@ ZNS L2Network 的类型定义: 处理逻辑类似 OVN Controller,但调用 ZNS API 创建 segment。 +创建 ZNS 二层网络时,Cloud 需要先根据 L2 类型从已缓存的 transport zone 中选择默认 transport zone: + +[cols="2,2,3"] +|=== +|Cloud L2 类型 |默认 transport zone.type |说明 + +|`L2VlanNetwork` +|`vlan` +|LAN 网络默认使用 vlan 类型的 transport zone + +|`L2NoVlanNetwork` +|`vlan` +|NoVlan 网络默认使用 vlan 类型的 transport zone + +|`L2GeneveNetwork` +|`overlay` +|Geneve 网络默认使用 overlay 类型的 transport zone + +|=== + +[NOTE] +ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。 +因此 Cloud 创建的 L2Network 在 OVN 侧默认可在该 transport zone 覆盖的全部物理机上使用,而不是由 OVN 提供 cluster 级隔离。 + +[NOTE] +Cloud 侧会额外施加一层调度约束:只有二层网络实际加载了某个 cluster 后,该 cluster 中的物理机才允许用于创建虚拟机。 +也就是说,transport zone 决定的是底层网络可达范围,`L2NetworkClusterRefVO` 决定的是 Cloud 侧可调度范围。 + ==== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg 处理逻辑类似 OVN Controller,根据 ZNS Host 和 transport zone 的关系,把 ZNS segment 关联到 transport zone。 From 6f59e4ab9fa41c26794c6fc381d143cf2b08f9ad Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sun, 22 Mar 2026 18:39:47 +0800 Subject: [PATCH 4/4] [network]: usus DBImpact Resolves: ZCF-1365 Change-Id: I6c746e6a777a6a62726b7a687070736b65766b6a --- conf/db/upgrade/V5.5.18__schema.sql | 16 +++ .../AfterSetL3NetworkMtuExtensionPoint.java | 7 + .../header/network/l3/L3NetworkInventory.java | 3 + .../header/network/l3/L3NetworkType.java | 9 ++ .../zstack/header/network/l3/L3NetworkVO.java | 3 + .../network/l3/L3NetworkManagerImpl.java | 35 +++++ sdk/src/main/java/SourceClassMap.java | 2 + .../sdk/CreateL2GeneveNetworkAction.java | 131 ++++++++++++++++++ .../zstack/sdk/L2GeneveNetworkInventory.java | 15 ++ .../java/org/zstack/testlib/ApiHelper.groovy | 27 ++++ .../zstack/testlib/SdnControllerSpec.groovy | 12 +- 11 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/network/l3/AfterSetL3NetworkMtuExtensionPoint.java create mode 100644 sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java diff --git a/conf/db/upgrade/V5.5.18__schema.sql b/conf/db/upgrade/V5.5.18__schema.sql index 30c59276d8d..5ad0bfbd68e 100644 --- a/conf/db/upgrade/V5.5.18__schema.sql +++ b/conf/db/upgrade/V5.5.18__schema.sql @@ -13,3 +13,19 @@ CREATE TABLE IF NOT EXISTS `L2GeneveNetworkVO` ( CONSTRAINT `fkL2GeneveNetworkVOL2NetworkEO` FOREIGN KEY (`uuid`) REFERENCES `L2NetworkEO` (`uuid`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE IF NOT EXISTS `ZnsTransportZoneVO` ( + `uuid` varchar(32) NOT NULL, + `name` varchar(255) DEFAULT NULL, + `description` varchar(2048) DEFAULT NULL, + `type` varchar(64) DEFAULT NULL, + `physicalNetwork` varchar(255) DEFAULT NULL, + `status` varchar(64) DEFAULT NULL, + `isDefault` tinyint(1) NOT NULL DEFAULT 0, + `tags` text DEFAULT NULL, + `znsSdnControllerUuid` varchar(32) NOT NULL, + `createDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkZnsTransportZoneVOSdnControllerVO` FOREIGN KEY (`znsSdnControllerUuid`) REFERENCES `SdnControllerVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/header/src/main/java/org/zstack/header/network/l3/AfterSetL3NetworkMtuExtensionPoint.java b/header/src/main/java/org/zstack/header/network/l3/AfterSetL3NetworkMtuExtensionPoint.java new file mode 100644 index 00000000000..c884c697b00 --- /dev/null +++ b/header/src/main/java/org/zstack/header/network/l3/AfterSetL3NetworkMtuExtensionPoint.java @@ -0,0 +1,7 @@ +package org.zstack.header.network.l3; + +import org.zstack.header.core.Completion; + +public interface AfterSetL3NetworkMtuExtensionPoint { + void afterSetL3NetworkMtu(L3NetworkInventory l3, int mtu, Completion completion); +} diff --git a/header/src/main/java/org/zstack/header/network/l3/L3NetworkInventory.java b/header/src/main/java/org/zstack/header/network/l3/L3NetworkInventory.java index f1662f6421f..b40295ea289 100755 --- a/header/src/main/java/org/zstack/header/network/l3/L3NetworkInventory.java +++ b/header/src/main/java/org/zstack/header/network/l3/L3NetworkInventory.java @@ -423,6 +423,9 @@ public boolean enableIpAddressAllocation() { } if (!getType().equals(L3NetworkConstant.L3_BASIC_NETWORK_TYPE)) { + if (L3NetworkType.hasType(getType())) { + return L3NetworkType.valueOf(getType()).isIpAddressAllocationEnabled(); + } return true; } diff --git a/header/src/main/java/org/zstack/header/network/l3/L3NetworkType.java b/header/src/main/java/org/zstack/header/network/l3/L3NetworkType.java index b60fbb45969..e759a88b57c 100755 --- a/header/src/main/java/org/zstack/header/network/l3/L3NetworkType.java +++ b/header/src/main/java/org/zstack/header/network/l3/L3NetworkType.java @@ -6,6 +6,7 @@ public class L3NetworkType { private static Map types = Collections.synchronizedMap(new HashMap()); private final String typeName; private boolean exposed = true; + private boolean ipAddressAllocationEnabled = true; public L3NetworkType(String typeName) { this.typeName = typeName; @@ -25,6 +26,14 @@ public void setExposed(boolean exposed) { this.exposed = exposed; } + public boolean isIpAddressAllocationEnabled() { + return ipAddressAllocationEnabled; + } + + public void setIpAddressAllocationEnabled(boolean ipAddressAllocationEnabled) { + this.ipAddressAllocationEnabled = ipAddressAllocationEnabled; + } + public static boolean hasType(String typeName) { return types.containsKey(typeName); } diff --git a/header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java b/header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java index 0df379851dd..0ea342bd7a5 100755 --- a/header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java +++ b/header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java @@ -115,6 +115,9 @@ public boolean enableIpAddressAllocation() { } if (!getType().equals(L3NetworkConstant.L3_BASIC_NETWORK_TYPE)) { + if (L3NetworkType.hasType(getType())) { + return L3NetworkType.valueOf(getType()).isIpAddressAllocationEnabled(); + } return true; } diff --git a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java index 7c1ee6da361..cd57611ab9a 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java @@ -192,6 +192,41 @@ public void fail(ErrorCode errorCode) { } }); } + }).then(new NoRollbackFlow() { + @Override + public void run(FlowTrigger trigger, Map data) { + List exts = + pluginRgty.getExtensionList(AfterSetL3NetworkMtuExtensionPoint.class); + if (exts.isEmpty()) { + trigger.next(); + return; + } + + L3NetworkInventory l3Inv = L3NetworkInventory.valueOf(l3Vo); + new While<>(exts).each((ext, wcompl) -> { + ext.afterSetL3NetworkMtu(l3Inv, msg.getMtu(), new Completion(wcompl) { + @Override + public void success() { + wcompl.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + wcompl.addError(errorCode); + wcompl.allDone(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (errorCodeList.getCauses().isEmpty()) { + trigger.next(); + } else { + trigger.fail(errorCodeList.getCauses().get(0)); + } + } + }); + } }).done(new FlowDoneHandler(msg) { @Override public void handle(Map data) { diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 6ebcb3ee5e2..ebf074633d3 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -587,6 +587,7 @@ public class SourceClassMap { put("org.zstack.network.service.virtualrouter.VirtualRouterOfferingInventory", "org.zstack.sdk.VirtualRouterOfferingInventory"); put("org.zstack.network.service.virtualrouter.VirtualRouterSoftwareVersionInventory", "org.zstack.sdk.VirtualRouterSoftwareVersionInventory"); put("org.zstack.network.service.virtualrouter.VirtualRouterVmInventory", "org.zstack.sdk.VirtualRouterVmInventory"); + put("org.zstack.network.zns.L2GeneveNetworkInventory", "org.zstack.sdk.L2GeneveNetworkInventory"); put("org.zstack.observabilityServer.ObservabilityServerOfferingInventory", "org.zstack.sdk.ObservabilityServerOfferingInventory"); put("org.zstack.observabilityServer.ObservabilityServerVmInventory", "org.zstack.sdk.ObservabilityServerVmInventory"); put("org.zstack.observabilityServer.service.ObservabilityServerServiceDataInventory", "org.zstack.sdk.ObservabilityServerServiceDataInventory"); @@ -1156,6 +1157,7 @@ public class SourceClassMap { put("org.zstack.sdk.KvmCephIsoTO", "org.zstack.storage.ceph.primary.KvmCephIsoTO"); put("org.zstack.sdk.KvmHostHypervisorMetadataInventory", "org.zstack.kvm.hypervisor.datatype.KvmHostHypervisorMetadataInventory"); put("org.zstack.sdk.KvmHypervisorInfoInventory", "org.zstack.kvm.hypervisor.datatype.KvmHypervisorInfoInventory"); + put("org.zstack.sdk.L2GeneveNetworkInventory", "org.zstack.network.zns.L2GeneveNetworkInventory"); put("org.zstack.sdk.L2NetworkData", "org.zstack.header.network.l2.L2NetworkData"); put("org.zstack.sdk.L2NetworkInventory", "org.zstack.header.network.l2.L2NetworkInventory"); put("org.zstack.sdk.L2PortGroupNetworkInventory", "org.zstack.network.l2.virtualSwitch.header.L2PortGroupNetworkInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java b/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java new file mode 100644 index 00000000000..2d6cceeb9d9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java @@ -0,0 +1,131 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CreateL2GeneveNetworkAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.CreateL2NetworkResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, numberRange = {1L,16777214L}, noTrim = false) + public java.lang.Integer geneveId; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String zoneUuid; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String physicalInterface; + + @Param(required = false) + public java.lang.String type; + + @Param(required = false, validValues = {"LinuxBridge","OvsDpdk","MacVlan","OvnDpdk"}, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vSwitchType = "LinuxBridge"; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean isolated = false; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String pvlan; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.CreateL2NetworkResult value = res.getResult(org.zstack.sdk.CreateL2NetworkResult.class); + ret.value = value == null ? new org.zstack.sdk.CreateL2NetworkResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/l2-networks/geneve"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java b/sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java new file mode 100644 index 00000000000..bbf314e095c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java @@ -0,0 +1,15 @@ +package org.zstack.sdk; + + + +public class L2GeneveNetworkInventory extends org.zstack.sdk.L2NetworkInventory { + + public java.lang.Integer geneveId; + public void setGeneveId(java.lang.Integer geneveId) { + this.geneveId = geneveId; + } + public java.lang.Integer getGeneveId() { + return this.geneveId; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 7e79da5f4e1..1bdddba8006 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -9899,6 +9899,33 @@ abstract class ApiHelper { } + def createL2GeneveNetwork(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CreateL2GeneveNetworkAction.class) Closure c) { + def a = new org.zstack.sdk.CreateL2GeneveNetworkAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def createL2HardwareVxlanNetwork(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CreateL2HardwareVxlanNetworkAction.class) Closure c) { def a = new org.zstack.sdk.CreateL2HardwareVxlanNetworkAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy index f2b042d3fe7..593684408d4 100644 --- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy @@ -316,8 +316,12 @@ class SdnControllerSpec extends Spec implements HasSession { } } - // GET /zns/api/v1/fabric/compute-managers/{uuid} - simulator("/zns/api/v1/fabric/compute-managers/[^/]+") { + // GET/DELETE /zns/api/v1/fabric/compute-managers/{uuid} + simulator("/zns/api/v1/fabric/compute-managers/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + if (req.method == "DELETE") { + triggerZnsCallback(entity, spec, [:]) + return [:] + } return [success: true, data: [uuid: "cm-uuid-1", name: "cm-1", connectionStatus: "connected"]] } @@ -359,9 +363,9 @@ class SdnControllerSpec extends Spec implements HasSession { def uri = req.getRequestURI() def hspUuid = uri.tokenize("/").last() if (hspUuid == "hsp-kernel") { - return [success: true, data: [uuid: "hsp-kernel", name: "hsp-kernel", switchType: "kernel"]] + return [success: true, data: [uuid: "hsp-kernel", name: "hsp-kernel", type: "kernel", switchType: "OVS"]] } - return [success: true, data: [uuid: "hsp-dpdk", name: "hsp-dpdk", switchType: "dpdk"]] + return [success: true, data: [uuid: "hsp-dpdk", name: "hsp-dpdk", type: "dpdk", switchType: "OVS"]] } // /zns/api/v1/segments — GET(list) / POST(create) / DELETE(batch)