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..5ad0bfbd68e --- /dev/null +++ b/conf/db/upgrade/V5.5.18__schema.sql @@ -0,0 +1,31 @@ +-- 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 `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/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..8531f8537c9 --- /dev/null +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -0,0 +1,449 @@ += 对接ZNS SDN控制器 + +== 总体描述 + +在考虑对接 ZNS SDN 控制器时,首先需要考虑如何做好 Cloud 资源对象和 ZNS 资源映射。 + +* ZNS segments → L2Network + L3Network + IpRange +* ZNS segments port → VmNic + UsedIp +* ZNS segments + transport zone → L2NetworkClusterRefVO + +=== 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控制器 + +ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`SugonSdnController`、`OvnController`、`HuaweiIMasterSdnController` 等实现。 + +新定义 `ZnsControllerVO`,继承 `SdnControllerVO`,不添加新的字段: + +* 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 信息。 + +=== 创建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` + +* 调用 `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` 枚举字段。 + +* 根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则: ++ +[cols="2,2"] +|=== +|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType + +|`dpdk` +|`OvsDpdk` + +|`kernel` +|`OvsKernel` + +|其它未知值,或者 profile 查询失败 +|`ZNS` + +|=== +* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,同时持有两类 transport zone 缓存: +** transport zone 元数据缓存:保存到建议新增的 `ZnsTransportZone` +** `transportZoneUuid → Set` 反向缓存:用于 segment 和 cluster 的关联 +* 根据 computerManagerUuid 从 ZNS 获取 segments 列表。 +** 根据 segment 信息创建 L2Network、L3Network +** 根据 segment.ipam 信息创建 IpRange + +ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: + +[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` + +|=== + +补充说明: + +* `L2Network.vSwitchType` 固定写入 `ZNS` +* `L2Network.virtualNetworkId` 取 `segment.virtual_network_id`(Geneve/VLAN) +* `L3Network.category` 当前初始化逻辑固定为 `Private` + +5. 根据 `segment.transport_zone_uuid` 查询前面缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 + +`L2NetworkClusterRefVO` 的映射关系如下: + +[cols="2,3,2"] +|=== +|字段 |来源 |说明 + +|`l2NetworkUuid` +|当前 segment 创建出的 `L2NetworkVO.uuid` +|当前 L2 网络 UUID + +|`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: + +[cols="3,2"] +|=== +|情况 |操作 + +|`SdnControllerHostRefVO` 不存在(新主机) +|INSERT 新记录 + +|已存在但 `vSwitchType` 或 `vtepIp` 发生变化 +|UPDATE + +|已存在且字段未变 +|跳过 + +|=== + +[NOTE] +创建时只做 INSERT;重连时使用 upsert,可处理主机上下线及 vSwitchType 变更的情况。 + +==== 2. Segment 协调(以 Cloud 为基准) + +重连以 Cloud 数据库中所有 `vSwitchType = ZNS` 的 `L2NetworkVO` 为基准,与 ZNS 侧属于本 Computer Manager 的 segment 做三路对比。 +ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关联。 + +[cols="2,2,3"] +|=== +|Cloud 侧 L2NetworkVO |ZNS 侧 segment |操作 + +|不存在(`l2Uuid` 指向已删除 L2,或 segment 无 `l2Uuid`) +|存在 +|调用 `DELETE /zns/api/v1/segments` 删除孤儿 segment + +|存在 +|不存在 +|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3 信息 + +|存在 +|存在但参数不一致(如 CIDR) +|调用 `PATCH /zns/api/v1/segments/{uuid}` 更新 + +|存在 +|存在且参数一致 +|无操作 + +|=== + +[NOTE] +重连 *不会* 根据 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 + +=== 基础信息 + +`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 + +|=== + +=== 创建 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。 + +==== APIChangeL2NetworkVlanIdMsg + +* L2GeneveNetwork 类型不支持修改 VlanId,需要在 `L2NetworkApiInterceptor` 中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` +* L2VlanNetwork、L2NoVlanNetwork 类型支持 +* 仅需要修改 L2NetworkVO 数据库,不需要下发到物理机,需要调用修改 ZNS segment API + +== 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 L3Network 不添加网络服务。 + +=== 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 的物理机 + +=== 网卡创建过程 + +创建虚拟机或给虚拟机添加网卡时,会调用 `VmAllocateNicFlow` 创建网卡。 + +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` + +=== 网卡删除过程 + +* `VmReturnReleaseNicFlow`:在 `destroyVmWorkFlowElements` 中被调用,用于虚拟机销毁时释放网卡资源 +* `VmDetachNicFlow`:在云主机删除网卡时调用 + +两个 Flow 中都需要: + +1. 调用 ZNS 删除 segment port API +2. 删除 `VmNicVO`、`UsedIpVO` + +=== DPDK 网卡的特殊处理 + +由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 +这个逻辑与 OVN DPDK 虚拟网卡一致。 + +=== ChangeVmNicNetwork(换网操作) + +`APIChangeVmNicNetworkMsg` 涉及 detach 旧网络 + attach 新网络: + +* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络 +* 从 ZNS 网络变换成 ZNS 网络的场景,需要调用 ZNS API 删除旧的 segment port,调用 API 创建新的 segment port,并更新 `VmNicVO`/`UsedIpVO` 等相关数据对象 + +=== FilterAttachableL3NetworkExtensionPoint + +* `APIGetVmAttachableL3NetworkMsg` 必须能获取到 ZNS L3 网络。 + + 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/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/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()); } 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/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 25b24d7f412..593684408d4 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,148 @@ 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/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"]] + } + + // 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", type: "kernel", switchType: "OVS"]] + } + return [success: true, data: [uuid: "hsp-dpdk", name: "hsp-dpdk", type: "dpdk", switchType: "OVS"]] + } + + // /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 [:] + } } }