exts = pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class);
+ if (exts.isEmpty() || nics.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ new While<>(exts).each((ext, wcomp) -> {
+ ext.releaseSdnNics(nics, new Completion(wcomp) {
+ @Override
+ public void success() {
+ wcomp.done();
+ }
+
+ @Override
+ public void fail(ErrorCode errorCode) {
+ logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode));
+ wcomp.done();
+ }
+ });
+ }).run(new WhileDoneCompletion(completion) {
+ @Override
+ public void done(ErrorCodeList errorCodeList) {
+ completion.success();
}
});
}
diff --git a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
index 713c64890ee..01d63c40464 100755
--- a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
+++ b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
@@ -5,6 +5,7 @@
import org.zstack.header.tag.AdminOnlyTag;
import org.zstack.header.tag.TagDefinition;
import org.zstack.header.vm.VmInstanceVO;
+import org.zstack.header.vm.VmNicVO;
import org.zstack.tag.PatternedSystemTag;
import org.zstack.tag.SensitiveTagOutputHandler;
import org.zstack.tag.SensitiveTag;
@@ -314,4 +315,8 @@ public String desensitizeTag(SystemTag systemTag, String tag) {
public static PatternedSystemTag VM_STATE_PAUSED_AFTER_MIGRATE = new PatternedSystemTag(("vmPausedAfterMigrate"), VmInstanceVO.class);
public static PatternedSystemTag VM_MEMORY_ACCESS_MODE_SHARED = new PatternedSystemTag(("vmMemoryAccessModeShared"), VmInstanceVO.class);
+
+ public static String IFACE_ID_TOKEN = "ifaceId";
+ public static PatternedSystemTag IFACE_ID = new PatternedSystemTag(
+ String.format("ifaceId::{%s}", IFACE_ID_TOKEN), VmNicVO.class);
}
diff --git a/conf/db/upgrade/V5.5.18__schema.sql b/conf/db/upgrade/V5.5.18__schema.sql
new file mode 100644
index 00000000000..84d643113a4
--- /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 '0000-00-00 00:00:00',
+ `lastOpDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' 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/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml
index ef3d5a7cc9e..0ab2073d583 100755
--- a/conf/springConfigXml/VmInstanceManager.xml
+++ b/conf/springConfigXml/VmInstanceManager.xml
@@ -37,6 +37,7 @@
org.zstack.compute.vm.VmAllocateVolumeFlow
org.zstack.compute.vm.VmAllocateNicFlow
org.zstack.compute.vm.VmAllocateNicIpFlow
+ org.zstack.compute.vm.VmAllocateSdnNicFlow
org.zstack.compute.vm.VmAllocateCdRomFlow
org.zstack.compute.vm.VmInstantiateResourcePreFlow
org.zstack.compute.vm.VmCreateOnHypervisorFlow
diff --git a/conf/springConfigXml/sdnController.xml b/conf/springConfigXml/sdnController.xml
index a9be9ec8795..e76be084ac6 100644
--- a/conf/springConfigXml/sdnController.xml
+++ b/conf/springConfigXml/sdnController.xml
@@ -26,14 +26,11 @@
-
-
-
-
+
diff --git a/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java b/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java
new file mode 100644
index 00000000000..f719bb8445e
--- /dev/null
+++ b/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java
@@ -0,0 +1,189 @@
+package org.zstack.core.rest.webhook;
+
+import org.zstack.core.Platform;
+import org.zstack.core.thread.ThreadFacade;
+import org.zstack.core.thread.ThreadFacadeImpl;
+import org.zstack.header.core.ReturnValueCompletion;
+import org.zstack.header.errorcode.ErrorCode;
+import org.zstack.header.rest.RESTFacade;
+import org.zstack.utils.Utils;
+import org.zstack.utils.logging.CLogger;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import static org.zstack.core.Platform.operr;
+
+/**
+ * Generic async callback client for external systems that use a webhook pattern:
+ *
+ * - Send an HTTP request to the external system
+ * - External system returns immediately (e.g. 202 Accepted)
+ * - External system later POSTs back the result to a callback URL
+ * - This client matches the callback to the original request and completes it
+ *
+ *
+ * Callback flow:
+ *
+ * External Controller
+ * │ POST /asyncrest/sendcommand (commandpath header → callbackPath)
+ * ▼
+ * AsyncRESTCallController.sendCommand()
+ * ▼
+ * RESTFacadeImpl.sendCommand() → httpCallhandlers.get(callbackPath)
+ * ▼
+ * WebhookCallbackClient.onCallback(T cmd)
+ * ├─ protocol.extractTaskId(cmd)
+ * ├─ pendingCalls.remove(taskId) ← CAS point (atomic, prevents double invocation)
+ * ├─ cancel timeout
+ * └─ protocol.isSuccess(cmd) ? completion.success(cmd) : completion.fail(error)
+ *
+ *
+ * This class is a plain POJO — not a Spring bean. It is created and owned by the
+ * plugin-specific API client (e.g. ZnsApiClient) which passes in its dependencies.
+ *
+ * Thread safety: {@code ConcurrentHashMap.remove()} serves as the CAS point.
+ * Among callback arrival, timeout, and send-failure, only one can successfully remove
+ * and thus complete a pending call.
+ *
+ * @param the callback body type
+ */
+public class WebhookCallbackClient {
+ private static final CLogger logger = Utils.getLogger(WebhookCallbackClient.class);
+
+ private final WebhookProtocol protocol;
+ private final RESTFacade restf;
+ private final ThreadFacade thdf;
+ private final ConcurrentHashMap> pendingCalls = new ConcurrentHashMap<>();
+ private String callbackUrl;
+
+ private static class PendingEntry {
+ final ReturnValueCompletion completion;
+ final ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt;
+
+ PendingEntry(ReturnValueCompletion completion,
+ ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt) {
+ this.completion = completion;
+ this.timeoutReceipt = timeoutReceipt;
+ }
+ }
+
+ public WebhookCallbackClient(WebhookProtocol protocol, RESTFacade restf, ThreadFacade thdf) {
+ this.protocol = protocol;
+ this.restf = restf;
+ this.thdf = thdf;
+ }
+
+ /**
+ * Register the callback handler on the sendCommand channel.
+ * Must be called once during the owning component's start() lifecycle.
+ */
+ public void start() {
+ this.callbackUrl = restf.getSendCommandUrl();
+ restf.registerSyncHttpCallHandler(
+ protocol.getCallbackPath(),
+ protocol.getCallbackClass(),
+ this::onCallback);
+ }
+
+ /**
+ * Register a pending call and return its task identifier.
+ *
+ * The caller should use the returned taskId to decorate the outgoing request
+ * headers via {@link WebhookProtocol#decorateRequest}, then send the HTTP request.
+ * If the send fails, the caller must invoke {@link #fail} to clean up.
+ *
+ * @param completion the completion to invoke when the callback arrives (or on timeout)
+ * @param unit timeout time unit
+ * @param timeout timeout value
+ * @return the generated taskId
+ */
+ public String submit(ReturnValueCompletion completion, TimeUnit unit, long timeout) {
+ String taskId = Platform.getUuid();
+
+ ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt = thdf.submitTimeoutTask(() -> {
+ fail(taskId, operr("[Webhook Timeout] callback timed out for taskId[%s], path[%s]",
+ taskId, protocol.getCallbackPath()));
+ }, unit, timeout);
+
+ pendingCalls.put(taskId, new PendingEntry<>(completion, timeoutReceipt));
+ return taskId;
+ }
+
+ /**
+ * Actively fail a pending call (e.g. when the HTTP send fails).
+ *
+ * {@code ConcurrentHashMap.remove()} is atomic — only one of
+ * (callback / timeout / send-failure) can win, preventing double invocation.
+ */
+ public void fail(String taskId, ErrorCode error) {
+ PendingEntry entry = pendingCalls.remove(taskId);
+ if (entry != null) {
+ entry.timeoutReceipt.cancel();
+ entry.completion.fail(error);
+ }
+ }
+
+ /**
+ * @return the callback URL that the external system should POST results to
+ */
+ public String getCallbackUrl() {
+ return callbackUrl;
+ }
+
+ /**
+ * Override the callback URL. Use this when the callback is handled by a
+ * dedicated HTTP endpoint (e.g. a Spring Controller) rather than the
+ * sendCommand channel.
+ */
+ public void setCallbackUrl(String callbackUrl) {
+ this.callbackUrl = callbackUrl;
+ }
+
+ /**
+ * @return the protocol adapter
+ */
+ public WebhookProtocol getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Deliver a callback that was received outside the sendCommand channel
+ * (e.g. from a dedicated Spring Controller endpoint for external systems).
+ */
+ public void deliverCallback(T cmd) {
+ onCallback(cmd);
+ }
+
+ /**
+ * Callback handler invoked by the RESTFacade sendCommand channel.
+ */
+ private String onCallback(T cmd) {
+ String taskId = protocol.extractTaskId(cmd);
+ if (taskId == null) {
+ logger.warn(String.format("received webhook callback without taskId on path[%s], ignoring",
+ protocol.getCallbackPath()));
+ return null;
+ }
+
+ PendingEntry entry = pendingCalls.remove(taskId);
+ if (entry == null) {
+ logger.warn(String.format("received webhook callback for unknown taskId[%s] on path[%s], ignoring",
+ taskId, protocol.getCallbackPath()));
+ return null;
+ }
+
+ entry.timeoutReceipt.cancel();
+
+ if (protocol.isSuccess(cmd)) {
+ entry.completion.success(cmd);
+ } else {
+ String error = protocol.extractError(cmd);
+ entry.completion.fail(operr("webhook callback failed for taskId[%s], path[%s], error: %s",
+ taskId, protocol.getCallbackPath(), error != null ? error : "unknown"));
+ }
+
+ return null;
+ }
+}
+
diff --git a/core/src/main/java/org/zstack/core/rest/webhook/WebhookProtocol.java b/core/src/main/java/org/zstack/core/rest/webhook/WebhookProtocol.java
new file mode 100644
index 00000000000..c644fc58f19
--- /dev/null
+++ b/core/src/main/java/org/zstack/core/rest/webhook/WebhookProtocol.java
@@ -0,0 +1,55 @@
+package org.zstack.core.rest.webhook;
+
+import java.util.Map;
+
+/**
+ * Defines the protocol adaptation for an external system's webhook callback mechanism.
+ *
+ * Different external controllers (e.g., SDN controllers) use different header conventions,
+ * callback body formats, and success/failure semantics. This interface abstracts those
+ * differences so that {@link WebhookCallbackClient} can handle the common async lifecycle
+ * (pending call registration, timeout, CAS-guarded callback dispatch) generically.
+ *
+ * @param the callback body type that the external system POSTs back
+ */
+public interface WebhookProtocol {
+
+ /**
+ * The path to register on the sendCommand channel (e.g. "/zns/callback").
+ * This will be passed to {@code RESTFacade.registerSyncHttpCallHandler}.
+ */
+ String getCallbackPath();
+
+ /**
+ * The class used to deserialize the callback JSON body.
+ */
+ Class getCallbackClass();
+
+ /**
+ * Extract the task identifier from the callback body.
+ * This must match the taskId returned by {@link WebhookCallbackClient#submit}.
+ */
+ String extractTaskId(T callback);
+
+ /**
+ * Determine whether the callback indicates a successful operation.
+ */
+ boolean isSuccess(T callback);
+
+ /**
+ * Extract a human-readable error description from the callback body.
+ * Called only when {@link #isSuccess} returns false.
+ */
+ String extractError(T callback);
+
+ /**
+ * Decorate the outgoing HTTP request headers with the task identifier and callback URL,
+ * following the conventions of the external system.
+ *
+ * @param headers mutable map to add headers to
+ * @param taskId the unique task identifier for this async call
+ * @param callbackUrl the URL the external system should POST the result to
+ */
+ void decorateRequest(Map headers, String taskId, String callbackUrl);
+}
+
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/ZStackL2NetworkType.adoc b/docs/modules/network/pages/networkResource/ZStackL2NetworkType.adoc
new file mode 100644
index 00000000000..14c22fcda29
--- /dev/null
+++ b/docs/modules/network/pages/networkResource/ZStackL2NetworkType.adoc
@@ -0,0 +1,504 @@
+= 网络类型体系:L2NetworkType / VSwitchType / VmNicType
+
+ZStack 的网络类型由三层模型组成,分别描述二层网络拓扑、虚拟交换机实现和虚拟机网卡类型。
+
+== 三层类型模型
+
+[cols="1,1,1"]
+|===
+| L2NetworkType (网络拓扑类型) | VSwitchType (虚拟交换机类型) | VmNicType (网卡类型)
+
+| 定义二层网络的拓扑结构
+| 定义物理机上的虚拟交换机实现
+| 定义虚拟机网卡的驱动/接口类型
+
+| 由 `L2NetworkFactory` 注册
+| 由 `VmInstanceNicFactory` 注册
+| 由 `VSwitchType.addVmNicType()` 绑定
+
+| 存储在 `L2NetworkVO.type`
+| 存储在 `L2NetworkVO.vSwitchType`
+| 存储在 `VmNicVO.type`
+|===
+
+=== 关系
+
+----
+L2NetworkType 1 ──── N VSwitchType 1 ──── N VmNicType
+ (一种L2可选多种vSwitch) (一种vSwitch可有多种NIC子类型)
+----
+
+例:
+
+----
+L2NoVlanNetwork ──┬── LinuxBridge ──── VIRTUAL_NIC (SubType=NONE)
+ └── OVN_DPDK ──┬── dpdkvhostuserclient (SubType=NONE)
+ └── VF (SubType=SRIOV)
+
+L2GeneveNetwork ──── ZNS ──┬── VIRTUAL_NIC (SubType=NONE, 默认)
+ └── dpdkvhostuserclient (SubType=VHOSTUSER, 需system tag指定)
+
+L2VlanNetwork ──── LinuxBridge ──── VIRTUAL_NIC (SubType=NONE)
+----
+
+== 注册机制
+
+=== L2NetworkType
+
+在 `L2NetworkFactory.getType()` 中通过 `new L2NetworkType("xxx")` 自动注册到静态 Map。
+
+[source,java]
+----
+// L2NoVlanL2NetworkFactory.java
+public L2NetworkType getType() {
+ return new L2NetworkType(L2NetworkConstant.L2_NO_VLAN_NETWORK_TYPE);
+}
+----
+
+=== VSwitchType
+
+在 `VmInstanceNicFactory.getType()` 中通过 `new VSwitchType("xxx")` 自动注册到静态 Map。
+
+[source,java]
+----
+// ZnsVmNicFactory.java
+static final VSwitchType vSwitchType = new VSwitchType("ZNS");
+----
+
+=== VmNicType
+
+通过 `VSwitchType.addVmNicType(SubType, VmNicType)` 绑定到 VSwitchType。
+
+[source,java]
+----
+// ZnsVmNicFactory.java
+vSwitchType.addVmNicType(VmNicType.VmNicSubType.NONE, type);
+----
+
+== 全部 L2NetworkType(10种)
+
+[cols="2,3,2"]
+|===
+| L2NetworkType | 工厂类 | 模块
+
+| L2NoVlanNetwork
+| `L2NoVlanL2NetworkFactory`
+| network/
+
+| L2VlanNetwork
+| `L2VlanNetworkFactory`
+| network/
+
+| VxlanNetwork
+| `VxlanNetworkFactory`
+| plugin/vxlan/
+
+| VxlanNetworkPool
+| `VxlanNetworkPoolFactory`
+| plugin/vxlan/
+
+| HardwareVxlanNetwork
+| `HardwareVxlanNetworkFactory`
+| plugin/sdnController/
+
+| HardwareVxlanNetworkPool
+| `HardwareVxlanNetworkPoolFactory`
+| plugin/sdnController/
+
+| L2GeneveNetwork
+| `L2GeneveNetworkFactory`
+| premium/zns/
+
+| PortGroupNetwork
+| `L2PortGroupNetworkFactory`
+| premium/virtualSwitch/
+
+| VirtualSwitchNetwork
+| `L2VirtualSwitchNetworkFactory`
+| premium/virtualSwitch/
+
+| TfNetwork
+| `TfL2NetworkFactory`
+| plugin/sugonSdn/
+|===
+
+== 全部 VSwitchType(7种)
+
+[cols="2,1,1,2"]
+|===
+| VSwitchType | sdnControllerType | nicLifecycleManagedByFactory | NicFactory
+
+| LinuxBridge
+| null
+| false
+| `VmNicFactory`
+
+| ZNS
+| "ZNS"
+| *true*
+| `ZnsVmNicFactory`
+
+| MacVlan
+| null
+| false
+| `VmMacVlanNicFactory`
+
+| OVN_DPDK
+| null
+| false
+| `VmOvnVhostUserNicFactory`
+
+| OVS_DPDK
+| null
+| false
+| `VmOvnVhostUserNicFactory`
+
+| TF
+| null
+| false
+| `TfVmNicFactory`
+
+| OVS_DPDK(vDPA)
+| null
+| false
+| `VmVdpaNicFactory`
+|===
+
+=== VSwitchType 关键属性
+
+[cols="2,4,1"]
+|===
+| 属性 | 作用 | ZNS 值
+
+| `sdnControllerType`
+| 关联的 SDN 控制器类型,null 表示无 SDN
+| "ZNS"
+
+| `nicLifecycleManagedByFactory`
+| NIC 生命周期由 NicFactory 管理 (true) 还是 SdnControllerManager 管理 (false)
+| *true*
+
+| `attachToCluster`
+| 是否需要手动挂载集群
+| false(ZNS 通过 TransportZone 自动挂载)
+
+| `useDpdk`
+| 是否使用 DPDK
+| false
+|===
+
+== L2NetworkType → VSwitchType 映射
+
+L2NetworkVO 同时持有 `type`(L2NetworkType)和 `vSwitchType`(VSwitchType)。
+L2 创建时由用户或系统指定 vSwitchType。同一 L2NetworkType 可搭配不同 VSwitchType:
+
+[cols="2,2,3"]
+|===
+| L2NetworkType | 可用 VSwitchType | 说明
+
+| L2NoVlanNetwork
+| LinuxBridge, OVN_DPDK, OVS_DPDK, ZNS
+| 普通无 VLAN 二层网络
+
+| L2VlanNetwork
+| LinuxBridge, OVN_DPDK, OVS_DPDK, ZNS
+| VLAN tagged 网络
+
+| VxlanNetwork
+| LinuxBridge
+| 软件 VXLAN 覆盖网络
+
+| HardwareVxlanNetwork
+| (SDN 专用)
+| 硬件 SDN VXLAN
+
+| L2GeneveNetwork
+| ZNS
+| ZNS Geneve 覆盖网络
+
+| PortGroupNetwork
+| (VirtualSwitch)
+| VMware 端口组
+
+| TfNetwork
+| TF
+| Tungsten Fabric
+|===
+
+== VSwitchType → VmNicType 映射(按 VmNicSubType)
+
+请求创建 NIC 时:
+
+. 检查 VM 是否有 SRIOV 系统标签 → SubType=SRIOV
+. 检查全局配置是否启用 VHOSTUSER → SubType=VHOSTUSER
+. 否则 → SubType=NONE
+
+`VSwitchType.getVmNicType(subType)` 从 nicTypes Map 中查找对应的 VmNicType。
+
+[cols="2,1,1,1"]
+|===
+| VSwitchType | SubType=NONE | SubType=SRIOV | SubType=VHOSTUSER
+
+| LinuxBridge
+| VIRTUAL_NIC
+| —
+| —
+
+| ZNS
+| VIRTUAL_NIC
+| —
+| dpdkvhostuserclient
+
+| MacVlan
+| MACVLAN_NIC
+| —
+| —
+
+| OVN_DPDK
+| OVN_VHOSTUSER
+| OVN_VF
+| —
+
+| OVS_DPDK
+| —
+| VDPA
+| OVS_VHOSTUSER
+
+| TF
+| TF_NIC
+| —
+| —
+|===
+
+== 创建 VM 时 NIC 组装完整流程
+
+----
+ VmAllocateNicFlow
+ │
+ L3Network → L2Network → VSwitchType → VmNicType → VmInstanceNicFactory
+ │
+ factory.createVmNic()
+ ┌───────────┴───────────┐
+ │ │
+ (持久化 VmNicVO) factory.afterCreateVmNic()
+ │
+ ┌─────────────────────────┤
+ │ │
+ 普通 NIC (空操作) ZNS NIC:
+ createSegmentPort()
+ → 创建 UsedIpVO
+ → 回写 VmNicVO.ip/gateway
+ │
+ VmInstantiateResourcePreFlow
+ │
+ SdnControllerManagerImpl.preInstantiateVmResource()
+ │
+ ┌──────────────┼──────────────┐
+ │ │ │
+ sdnControllerType nicLifecycle 其他 SDN:
+ == null? ManagedByFactory? controller.addVmNics()
+ → 跳过(普通) → 跳过(ZNS已处理) (OVN/HW-VXLAN 路径)
+ │
+ KVMHost.completeNicInfo()
+ │
+ KVMCompleteNicInformationExtensionPoint (按 L2Type 分发)
+ ┌──────┬──────┼──────┬──────┐
+ │ │ │ │ │
+ NoVlan Vlan VxLAN Geneve PortGroup
+ bridge bridge bridge bridge ...
+ 无VLAN +VLAN +VNI (ZNS管)
+ │
+ StartVmCmd → KVM Agent
+----
+
+== L2 物理机配置(Realize)决策
+
+[cols="2,3,3"]
+|===
+| L2 Backend | realize() 行为 | 触发时机
+
+| KVMRealizeL2NoVlan
+| `CreateBridgeCmd` → 物理机创建网桥
+| Host 连接时 + NIC 挂载时
+
+| KVMRealizeL2Vlan
+| `CreateVlanBridgeCmd` → 创建 VLAN 子接口 + 网桥
+| Host 连接时 + NIC 挂载时
+
+| KVMRealizeL2Vxlan
+| `CreateVxlanBridgeCmd` → 创建 VXLAN 隧道 + 网桥
+| 按需(`InstantiateResourceOnAttachingNic`)
+
+| KVMRealizeL2Geneve
+| *空操作*
+| ZNS/OVS controller 自行管理网桥
+|===
+
+== 全部 VmNicType(6种)
+
+[cols="2,2,2,3"]
+|===
+| VmNicType | 类型名 | NicFactory | 说明
+
+| VNIC
+| `"VNIC"`
+| `VmNicFactory`
+| 标准 virtio 网卡,最常用的类型
+
+| dpdkvhostuserclient
+| `"dpdkvhostuserclient"`
+| `VmOvnVhostUserNicFactory`
+| DPDK vhost-user 网卡,使用 OVS 的 vhost-user socket 进行高性能数据传输
+
+| VF
+| `"VF"`
+| `VmVfNicFactory`
+| SR-IOV Virtual Function 网卡,直通物理网卡的虚拟功能
+
+| vDPA
+| `"vDPA"`
+| `VmVdpaNicFactory`
+| vDPA (virtio Data Path Acceleration) 网卡,硬件加速的 virtio 数据路径
+
+| MACVLAN
+| `"MACVLAN"`
+| `VmMacVlanNicFactory`
+| MacVLAN 网卡,基于 MAC 地址的虚拟 LAN
+
+| TFVNIC
+| `"TFVNIC"`
+| `TfVmNicFactory`
+| Tungsten Fabric 网卡
+|===
+
+=== 各 VmNicType 的使用条件
+
+VmNicType 的选择由 `VmNicManagerImpl.getVmNicType()` 决定,依据三个因素:
+
+. L2 网络的 VSwitchType
+. VM 是否有 SRIOV 系统标签(`enableSRIOV::{l3Uuid}`)
+. 全局配置 `ENABLE_VHOSTUSER` 是否开启
+
+选择优先级:SRIOV > VHOSTUSER > NONE,若 VSwitchType 不支持请求的 SubType,回退到 NONE。
+
+[cols="2,2,2,2,2"]
+|===
+| VmNicType | VSwitchType | L2NetworkType | VmNicSubType | 使用条件
+
+| VNIC
+| LinuxBridge
+| L2NoVlanNetwork, L2VlanNetwork
+| NONE
+| 默认情况:未启用 SRIOV 且未启用 VHOSTUSER
+
+| dpdkvhostuserclient
+| OVN_DPDK
+| L2NoVlanNetwork
+| NONE
+| L2 使用 OVN_DPDK vSwitch,默认情况
+
+| dpdkvhostuserclient
+| OVS_DPDK
+| L2NoVlanNetwork, L2VlanNetwork
+| VHOSTUSER
+| L2 使用 OVS_DPDK vSwitch 且全局配置 `ENABLE_VHOSTUSER=true`
+
+| dpdkvhostuserclient
+| OVN_DPDK
+| L2NoVlanNetwork
+| SRIOV
+| L2 使用 OVN_DPDK vSwitch 且 VM 有 `enableSRIOV` 标签(注册为 VF 类型)
+
+| VF
+| LinuxBridge
+| L2NoVlanNetwork, L2VlanNetwork
+| SRIOV
+| L2 使用 LinuxBridge 且 VM 有 `enableSRIOV` 标签
+
+| vDPA
+| OVS_DPDK
+| L2NoVlanNetwork, L2VlanNetwork
+| NONE, SRIOV
+| L2 使用 OVS_DPDK vSwitch,默认或 SRIOV 模式
+
+| MACVLAN
+| MacVlan
+| L2NoVlanNetwork, L2VlanNetwork
+| NONE
+| L2 使用 MacVlan vSwitch
+
+| VNIC
+| ZNS
+| L2NoVlanNetwork, L2VlanNetwork, L2GeneveNetwork
+| NONE
+| L2 使用 ZNS vSwitch,默认网卡类型
+
+| dpdkvhostuserclient
+| ZNS
+| L2NoVlanNetwork, L2VlanNetwork, L2GeneveNetwork
+| VHOSTUSER
+| L2 使用 ZNS vSwitch,用户通过 system tag 指定使用 dpdkvhostuserclient
+
+| TFVNIC
+| TF
+| TfNetwork
+| NONE
+| L2 使用 TF (Tungsten Fabric) vSwitch
+|===
+
+=== NicTO 中 srcPath 设置规则
+
+当 VSwitchType 为 OVN_DPDK 或 ZNS 时,`completeNicInformation()` 会设置
+`srcPath = "/var/run/openvswitch/" + nic.getInternalName()`,用于 DPDK vhost-user socket 连接。
+
+[cols="2,2,2,1"]
+|===
+| L2NetworkType | VSwitchType | 设置 srcPath? | Backend
+
+| L2NoVlanNetwork
+| LinuxBridge
+| 否
+| KVMRealizeL2NoVlanNetworkBackend
+
+| L2NoVlanNetwork
+| OVN_DPDK / ZNS
+| *是*
+| KVMRealizeL2NoVlanNetworkBackend
+
+| L2VlanNetwork
+| LinuxBridge
+| 否
+| KVMRealizeL2VlanNetworkBackend
+
+| L2VlanNetwork
+| OVN_DPDK / ZNS
+| *是*
+| KVMRealizeL2VlanNetworkBackend
+
+| L2GeneveNetwork
+| ZNS
+| 否(仅 OVN_DPDK 设置)
+| KVMRealizeL2GeneveNetworkBackend
+
+| VxlanNetwork
+| LinuxBridge
+| 否
+| KVMRealizeL2VxlanNetworkBackend
+|===
+
+== SDN 两条路径对比
+
+=== Path A: Factory 管理(ZNS)
+
+* `VSwitchType.nicLifecycleManagedByFactory = true`
+* `SdnControllerManagerImpl.preInstantiateVmResource()` 跳过
+* `VmInstanceNicFactory`(ZnsVmNicFactory)在 `afterCreateVmNic()` 中直接调 ZNS API
+* Port 在 VM stop/reboot 时持久化,不随 VM 生命周期反复创建/删除
+
+=== Path B: SdnControllerManager 管理(OVN / Hardware VXLAN)
+
+* `VSwitchType.nicLifecycleManagedByFactory = false`,`sdnControllerType != null`
+* `SdnControllerManagerImpl.preInstantiateVmResource()` 处理
+* 通过 `L2NetworkSystemTags.SdnControllerUuid` 找到控制器 UUID
+* 调用 `SdnControllerL2.addVmNics()` 创建端口
+* Port 随 VM start/stop 反复创建/删除
diff --git a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc
new file mode 100644
index 00000000000..b536d694109
--- /dev/null
+++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc
@@ -0,0 +1,911 @@
+= 对接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 之间共享。
+
+=== UUID 格式约定
+
+ZNS 使用带连字符的 UUID 格式(`550e8400-e29b-41d4-a716-446655440000`),Cloud 使用不带连字符的紧凑 UUID 格式(`550e8400e29b41d4a716446655440000`)。
+
+Cloud 在调用 ZNS API 时需要将 Cloud UUID 转换为 ZNS UUID 格式,在接收 ZNS 响应时需要将 ZNS UUID 转换回 Cloud 格式。
+
+== Cloud 与 ZNS 通信协议
+
+=== 传输层
+
+Cloud 通过 HTTP REST API 与 ZNS 通信。所有请求/响应体均为 JSON 格式。
+
+全局配置参数:
+
+[cols="2,2,2"]
+|===
+|配置项 |默认值 |说明
+
+|`zns.controller.scheme`
+|`http`
+|HTTP 或 HTTPS
+
+|`zns.controller.port`
+|`7278`
+|ZNS API 端口
+
+|`zns.controller.timeout`
+|`300000`(5分钟)
+|请求超时时间(毫秒)
+
+|===
+
+=== 异步回调模式
+
+所有写操作(POST/PATCH/DELETE)采用异步回调模式:
+
+....
+Cloud ZNS
+ │ POST/PATCH/DELETE + headers │
+ │ x-web-hook: │
+ │ x-job-uuid: │
+ │─────────────────────────────────────────►│
+ │ ◄── 202 Accepted │ (ZNS 立即返回)
+ │ │
+ │ (ZNS 异步处理完成后回调 Cloud) │
+ │ ◄── POST │
+ │ { taskUuid, success, status, data } │
+ │ │
+ │ Cloud 根据 taskUuid 匹配到 │
+ │ 对应的等待中的调用并完成 │
+....
+
+所有读操作(GET)为同步调用,直接返回 200 + JSON body。
+
+=== 请求串行化
+
+对同一个 ZNS IP 的所有 API 调用通过任务链串行执行,避免并发操作导致的冲突。
+
+== ZNS SDN控制器
+
+ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`SugonSdnController`、`OvnController`、`HuaweiIMasterSdnController` 等实现。
+
+新定义 `ZnsControllerVO`,继承 `SdnControllerVO`:
+
+* vendorType:ZNS
+* vendorVersion:用于记录当前连接的 ZNS 版本(详见 <> 章节)
+* transportZones:关联的 `ZnsTransportZoneVO` 列表(一对多)
+
+[NOTE]
+必须先在 ZNS 完成添加 Computer Manager 的操作,然后在 Cloud 侧创建对应的 SdnController。
+ZNS SDN Controller 保持 SystemTags:`computerManagerUuid::xxxx`,这里的 xxxx 是 ZNS 创建的 Computer Manager UUID。
+后续 Cloud 调用 ZNS API 创建 segment、segment port 时,Cloud 会根据 computerManagerUuid 组装 cms 信息。
+
+Cloud 还通过 SystemTag 记录以下映射关系:
+
+* `znsSegmentUuid::{segmentUuid}` 在 `L2NetworkVO` 上 — 映射 Cloud L2 → ZNS segment
+* `znsSegmentPortUuid::{portUuid}` 在 `VmNicVO` 上 — 映射 Cloud NIC → ZNS segment port
+
+=== 创建SDN控制器
+
+==== 1. 验证 Computer Manager
+
+根据 systemTag 中提供的 `computerManagerUuid`,调用 `GET /zns/api/v1/fabric/compute-managers/{uuid}` 验证 Computer Manager 在 ZNS 上存在且可用。
+
+==== 2. 同步 Compute Collections(集群映射)
+
+根据 `GET /zns/api/v1/fabric/compute-collections` 获取 compute collection 列表,过滤出属于当前 Computer Manager 的条目。
+
+ZNS 侧直接使用 Cloud 的 cluster UUID,因此可以直接按 UUID 匹配 Cloud 侧的 `ClusterVO`,无需 name 匹配。
+
+构建以下映射关系:
+* `znsClusterUuids`:属于该 Computer Manager 的 ZNS cluster UUID 集合
+* `znsClusterToZstackCluster`:ZNS cluster UUID → Cloud cluster UUID
+
+==== 3. 获取 Discovered Nodes(主机发现)
+
+根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于步骤 2 中确定的 cluster 集合,且 `managementIp != null`:
+
+** `HostData.managementIp`:匹配 Cloud `HostVO.managementIp`,找到对应 `HostVO.uuid`
+** `HostData.clusterId`:即 Cloud `ClusterVO.uuid`
+** `HostData.transportNodeProfileId`:用于后续推导 vSwitchType 和建立 transport zone → cluster 的反向映射
+
+[NOTE]
+ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` 相同。
+
+==== 4. 推导 vSwitchType 并创建 Host Ref
+
+根据 `HostData.transportNodeProfileId` 调用 `GET /zns/api/v1/fabric/transport-node-profiles/{uuid}` 获取 `TransportNodeProfileData`,取 `hostSwitchProfiles[0]` 调用 `GET /zns/api/v1/fabric/host-switch-profiles/{uuid}` 获取 `HostSwitchProfileData`:
+
+** `HostSwitchProfileData.type`:枚举值 `dpdk` 或 `kernel`,用于推导 `SdnControllerHostRefVO.vSwitchType`
+** `HostSwitchProfileData.transportZoneIds`:关联的 transport zone UUID 列表,建立反向缓存 `transportZoneUuid → Set`
+
+[NOTE]
+OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel`)和 `switchType`(字符串描述,如 `"OVS"`)两个字段。
+推导 `vSwitchType` 使用的是 `type` 枚举字段。
+
+根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则:
+
+[cols="2,2"]
+|===
+|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType
+
+|`dpdk`
+|`OvsDpdk`
+
+|`kernel`
+|`OvsKernel`
+
+|其它未知值,或者 profile 查询失败
+|`ZNS`
+
+|===
+
+==== 5. 同步 Transport Zones
+
+调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,持久化到 `ZnsTransportZoneVO`。主要字段如下:
+
+[cols="2,3,2"]
+|===
+|字段 |来源 |说明
+
+|`uuid`
+|transport zone UUID(转换为 Cloud 格式)
+|主键
+
+|`isDefault`
+|每种 type 的第一个 transport zone 设为 true
+|是否为默认 transport zone
+
+|`name`
+|transport zone 返回字段
+|transport zone 名称
+
+|`description`
+|transport zone 返回字段
+|描述信息
+
+|`type`
+|transport zone 返回字段
+|类型,典型值为 `vlan` 或 `overlay`
+
+|`physicalNetwork`
+|transport zone 返回字段
+|物理网络标识
+
+|`status`
+|transport zone 返回字段
+|当前状态
+
+|`tags`
+|transport zone 返回字段
+|标签信息
+
+|`znsSdnControllerUuid`
+|当前 `ZnsSdnControllerVO.uuid`
+|外键,关联到所属 ZNS SDN Controller
+
+|===
+
+[NOTE]
+`isDefault` 的设置规则:每种 type(vlan、overlay)的第一个 transport zone 自动设为默认。重连时保留已有的默认设置。
+
+==== 6. 同步 Segments 并创建 L2/L3
+
+根据 computerManagerUuid 从 ZNS 获取 segments 列表(带 cms 过滤)。
+
+对每个 segment:
+
+* 根据 segment 信息创建 L2Network、L3Network
+* 根据 segment.ipam 信息创建 IpRange
+* 通过 PATCH 将 Cloud L2 UUID 回写到 ZNS segment 的 cms.cms_resource_uuid 中
+
+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.physicalInterface` 固定写入空字符串
+* `L2Network.virtualNetworkId` 取 `segment.virtual_network_id`(Geneve/VLAN)
+* `L3Network.category` 当前初始化逻辑固定为 `Private`
+
+根据 `segment.transport_zone_uuid` 查询步骤 4 缓存的 `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(包括 compute collections → discovered nodes → derive vSwitchType),以处理新加入的主机或 vSwitchType 发生变化的主机。
+映射关系与创建时完全相同(`HostData.managementIp` → `HostVO`,`HostSwitchProfileData.type` → `vSwitchType`),
+区别仅在于写库操作改为 upsert:
+
+[cols="3,2"]
+|===
+|情况 |操作
+
+|`SdnControllerHostRefVO` 不存在(新主机)
+|INSERT 新记录
+
+|已存在但 `vSwitchType` 或 `vtepIp` 发生变化
+|UPDATE
+
+|已存在且字段未变
+|跳过
+
+|===
+
+[NOTE]
+创建时只做 INSERT;重连时使用 upsert,可处理主机上下线及 vSwitchType 变更的情况。
+
+==== 2. 刷新 Transport Zones
+
+调用 `GET /zns/api/v1/fabric/transport-zones` 重新拉取 transport zone 列表,对 `ZnsTransportZoneVO` 做 upsert,同时删除 ZNS 侧已不存在的旧记录。重连时保留已有的 `isDefault` 设置。
+
+==== 3. Segment 协调(以 Cloud 为基准)
+
+重连以 Cloud 数据库中所有 `vSwitchType = ZNS` 的 `L2NetworkVO` 为基准,与 ZNS 侧属于本 Computer Manager 的 segment 做三路对比。
+ZNS 侧 segment 通过 cms 元数据中的 `cms_resource_uuid` 与 Cloud L2 关联。
+
+[cols="2,2,3"]
+|===
+|Cloud 侧 L2NetworkVO |ZNS 侧 segment |操作
+
+|不存在(`cms_resource_uuid` 指向已删除 L2,或 segment 无 `cms_resource_uuid`)
+|存在
+|调用 `DELETE /zns/api/v1/segments` 删除孤儿 segment
+
+|存在
+|不存在
+|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3/IpRange 信息
+
+|存在
+|存在但参数不一致(名称、描述、CIDR 等)
+|调用 `PATCH /zns/api/v1/segments/{uuid}` 更新
+
+|存在
+|存在且参数一致
+|无操作(仅同步 systemTag 确保 segment UUID 映射正确)
+
+|===
+
+[NOTE]
+重连 *不会* 根据 ZNS segment 在 Cloud 侧创建新的 L2Network / L3Network / IpRange / `L2NetworkClusterRefVO`。
+如果 ZNS 存在但 Cloud 不存在,视为孤儿 segment 并删除(与创建阶段的单向导入方向相反)。
+
+==== 4. Segment Port 协调
+
+完成 segment 协调后,对每个已与 Cloud L2 匹配的 segment,逐一协调其 port。
+Port 通过 cms 元数据中的 `cms_resource_uuid` 与 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
+
+|两侧均存在
+|(当前实现不做参数比对更新,仅同步 systemTag)
+|无操作
+
+|===
+
+=== 心跳探活(Ping)
+
+Cloud 定期发送 `SdnControllerPingMsg`,通过调用 `GET /zns/api/v1/fabric/compute-managers/{uuid}` 验证 Computer Manager 连接是否正常。
+
+* 验证成功 → 控制器保持 Connected 状态
+* 验证失败 → 控制器状态变为 Disconnected
+
+=== 删除SDN控制器
+
+删除 ZNS SDN Controller 时的清理流程:
+
+. 根据 computerManagerUuid 从 ZNS 查询属于本 Controller 的所有 segments
+. 批量调用 `DELETE /zns/api/v1/segments` 删除这些 segments(force = true),连带删除 port
+. 删除 Cloud 本地的 `ZnsTransportZoneVO` 记录
+. 调用 `DELETE /zns/api/v1/fabric/compute-managers/{uuid}` 删除 Computer Manager
+
+[NOTE]
+删除 segments 和 compute manager 过程中如果 ZNS 调用失败,仅打印告警日志,不阻断删除流程。
+Cloud 侧的 L2Network、L3Network、IpRange、VmNic 等资源由 `SdnControllerVO` 级联删除机制清理。
+
+== L2Network
+
+=== 基础信息
+
+`L2NetworkVO` 的重要字段:
+
+* `type`:L2NetworkType.types,有:L2NoVlanNetwork、L2VlanNetwork、VxlanNetworkPool、VxlanNetwork、TfL2Network、HardwareVxlanNetworkPool、HardwareVxlanNetwork
+* `vSwitchType`:VSwitchType.types,有:LinuxBridge、TfL2Network、MacVlan、OvnDpdk、OvsDpdk、OvsKernel、ZNS
+* `virtualNetworkId`:vlanId 或 vxlanID
+* `physicalInterface`:物理网卡名称
+
+ZNS L2Network 的类型定义:
+
+[cols="1,2"]
+|===
+|属性 |值
+
+|type
+|L2NoVlanNetwork、L2VlanNetwork、L2GeneveNetwork(新增类型,类似于L2VlanNetwork)
+
+|vSwitchType
+|ZNS(固定值,不区分 kernel 和 dpdk)
+
+|physicalInterface
+|空字符串
+
+|virtualNetworkId
+|Vlan Id 或 Geneve Id
+
+|===
+
+=== 创建 L2Network
+
+创建 ZNS 二层网络时,Cloud 根据 L2 类型从已缓存的 `ZnsTransportZoneVO` 中选择默认 transport zone(`isDefault = true`):
+
+[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
+
+|===
+
+调用 `POST /zns/api/v1/segments` 创建 segment,请求体包含:name、description、transport_type(转为大写)、transport_zone_uuid、virtual_network_id、cms 信息。
+
+创建成功后,将 ZNS 返回的 segment UUID 通过 systemTag 记录到 L2NetworkVO 上。
+
+[NOTE]
+创建 L2Network 时,Cloud 会自动将 `vSwitchType` 设为 `ZNS`,`physicalInterface` 设为空字符串。这通过 API 拦截器(`ZnsApiInterceptor`)在 `APICreateL2NetworkMsg` 处理前自动完成。
+
+[NOTE]
+ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。
+因此 Cloud 创建的 L2Network 在 OVN 侧默认可在该 transport zone 覆盖的全部物理机上使用,而不是由 OVN 提供 cluster 级隔离。
+
+[NOTE]
+Cloud 侧会额外施加一层调度约束:只有二层网络实际加载了某个 cluster 后,该 cluster 中的物理机才允许用于创建虚拟机。
+也就是说,transport zone 决定的是底层网络可达范围,`L2NetworkClusterRefVO` 决定的是 Cloud 侧可调度范围。
+
+=== 删除 L2Network
+
+调用 `DELETE /zns/api/v1/segments` 删除对应的 ZNS segment(force = true),并清理 systemTag。
+如果 ZNS 侧删除失败(例如 segment 已不存在),仅打印告警日志,不阻断 Cloud 侧的删除流程。
+
+=== APIChangeL2NetworkVlanIdMsg
+
+* L2GeneveNetwork 类型不支持修改 VlanId,需要在 API 拦截器中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException`
+* L2VlanNetwork、L2NoVlanNetwork 类型支持
+
+=== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg
+
+当前实现为空操作(no-op)。ZNS 通过 transport zone 管理网络覆盖范围,attach/detach cluster 仅影响 Cloud 侧的调度约束。
+
+== 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 不添加网络服务。
+
+=== 创建/删除 IpRange
+
+创建 IpRange 时,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `gateway_address` 设为 IpRange 的 `networkCidr`,同步 CIDR 信息到 ZNS。
+
+删除最后一个 IpRange 时,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `gateway_address` 设为空字符串,清除 ZNS 侧的 CIDR 信息。
+
+[NOTE]
+创建/删除 L3Network 本身不触发 ZNS API 调用,仅 IpRange 的变化才同步到 ZNS。
+
+=== MTU 同步
+
+当用户修改 L3Network MTU 时,如果该 L3 属于 ZNS 网络,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `mtu` 字段同步到 ZNS segment。
+
+== VmNic
+
+VmNicType 的值有:VNIC、VF、`dpdkvhostuserclient`。ZNS 可能是 dpdk 模式,也可能是 kernel 模式。在 UI 选择 ZNS 网络后,用户可以选择网卡类型:VNIC 或 `dpdkvhostuserclient`。
+
+=== 虚拟机的物理机分配
+
+创建虚拟机选择了 ZNS 网络时:
+
+* 默认网卡类型是 VNIC,需要选择到部署了 OvnKernel 的物理机
+* 如果选择了 `dpdkvhostuserclient`,需要选择到部署了 OvnDpdk 的物理机
+
+=== 网卡创建过程
+
+创建虚拟机或给虚拟机添加网卡时,会调用 `VmAllocateNicFlow` 创建网卡。
+
+ZNS 网络创建过程:
+
+. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType
+. 调用 ZNS 创建 segment port API(`POST /zns/api/v1/segments/{uuid}/ports`),请求体包含:name、mac、ip、vm_uuid、cms 信息。ZNS 返回分配的 IP 地址
+. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程
+. 根据获取的参数创建/更新 `VmNicVO`、`UsedIpVO`
+. 将 ZNS 返回的 port UUID 通过 systemTag 记录到 VmNicVO 上
+
+=== 网卡删除过程
+
+* `VmReturnReleaseNicFlow`:在 `destroyVmWorkFlowElements` 中被调用,用于虚拟机销毁时释放网卡资源
+* `VmDetachNicFlow`:在云主机删除网卡时调用
+
+两个 Flow 中都需要:
+
+. 调用 ZNS 删除 segment port API(`DELETE /zns/api/v1/segments/{uuid}/ports`)
+. 清理 systemTag
+. 删除 `VmNicVO`、`UsedIpVO`
+
+[NOTE]
+如果 ZNS 侧删除失败,仅打印告警日志,不阻断 Cloud 侧的删除流程。
+
+=== IP 变更(VmIpChanged)
+
+当虚拟机 IP 发生变化时(`SetVmStaticIp`、`ChangeVmIp` 等操作),Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}/ports/{portUuid}` 更新 ZNS 侧的 port IP。
+
+=== ChangeVmNicNetwork(换网操作)
+
+`APIChangeVmNicNetworkMsg` 涉及 detach 旧网络 + attach 新网络:
+
+* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络(API 拦截器阻断)
+* 不支持在不同 ZNS 控制器之间换网(API 拦截器阻断)
+* 从 ZNS 网络变换成同一控制器下的 ZNS 网络:
+** `beforeUpdateNic`:记录旧 port 上下文(znsIp、segmentUuid、portUuid)
+** `afterUpdateNic`:先删除旧 segment port,再在新 segment 上创建新 port
+
+=== DPDK 网卡的特殊处理
+
+由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。
+这个逻辑与 OVN DPDK 虚拟网卡一致。
+
+=== FilterAttachableL3NetworkExtensionPoint
+
+获取虚拟机可挂载的 L3 网络列表时,ZNS 网络有额外的过滤规则:
+
+* 如果虚拟机尚未挂载 ZNS 网络:ZNS 类型的 L3 仅当虚拟机所在物理机被该 ZNS 控制器管理时才可挂载
+* 如果虚拟机已挂载 ZNS 网络:只允许挂载同一 ZNS 控制器下的 L3 或非 ZNS 的 L3
+
+== API 拦截器
+
+`ZnsApiInterceptor` 在以下场景进行拦截:
+
+* `APICreateL2NetworkMsg`:如果 systemTag 指定了 ZNS SDN 控制器,自动设置 `vSwitchType = ZNS`、`physicalInterface = ""`
+* `APIChangeL2NetworkVlanIdMsg`:禁止修改 L2GeneveNetwork 的 VLAN ID
+* `APIChangeVmNicNetworkMsg`:禁止 ZNS 与非 ZNS 网络之间换网,禁止不同 ZNS 控制器之间换网
+* `APISdnControllerAddHostMsg` / `RemoveHostMsg` / `ChangeHostMsg`:禁止对 ZNS 控制器手动管理主机(由 ZNS 自动管理)
+
+[[api-version]]
+== API 版本兼容性
+
+=== 问题背景
+
+Cloud 和 ZNS 是独立部署的两个组件,可能出现版本不一致:
+
+* **场景A**:Cloud 升级了,ZNS 没升级 — Cloud 发出 ZNS 不认识的 API 格式
+* **场景B**:ZNS 升级了,Cloud 没升级 — Cloud 用旧格式调新 API,参数缺失或语义变化
+* **场景C**:运行中 ZNS 被升级/降级 — Cloud 缓存的版本信息过期
+
+=== 设计思路
+
+....
+1. 每个 API 独立版本
+ 不是给 ZNS 一个整体版本号,而是给每个 API 独立的版本号。
+ ZNS 一年 2-3 个版本,但不是每个 API 都会变。
+
+2. ZNS 是自己能力的真相源
+ 每个 API 通过 HTTP OPTIONS 方法声明自己支持哪些版本。
+ ZNS 各 API 各管各的版本,不集中到一个大接口里。
+
+3. Cloud 通过 HTTP Header 声明自己发送的版本
+ 每次请求带 X-Api-Version header,ZNS 可据此选择处理逻辑。
+....
+
+=== 版本模型
+
+==== 每个 API 有独立版本
+
+....
+API 路径 + 方法 Cloud 发送的版本 ZNS 支持的版本
+────────────────────────────────────── ──────────────── ───────────────
+POST /zns/api/v1/segments 1.1 [1.0, 1.1]
+PATCH /zns/api/v1/segments/{uuid} 1.1 [1.0, 1.1, 1.2]
+DELETE /zns/api/v1/segments 1.0 [1.0]
+GET /zns/api/v1/segments 1.1 [1.0, 1.1]
+POST /zns/api/v1/segments/{id}/ports 1.0 [1.0, 1.1]
+PATCH /zns/api/v1/segments/{id}/ports 1.1 [1.0, 1.1]
+DELETE /zns/api/v1/segments/{id}/ports 1.0 [1.0]
+....
+
+* *Cloud 侧*:每个 API 有一个确定的版本,表示"我发出去的请求是什么格式"
+* *ZNS 侧*:每个 API 支持一组版本,表示"我能理解哪些格式"
+* *兼容条件*:Cloud 发送的版本 ∈ ZNS 支持的版本集合
+
+==== API 版本变更举例
+
+....
+ZNS v1.0 发布:
+ PATCH /segments 支持 [1.0]
+ POST /ports 支持 [1.0]
+
+ZNS v1.1 发布(PATCH segment 新增 mtu 字段):
+ PATCH /segments 支持 [1.0, 1.1] ← 新增 1.1,但仍兼容 1.0
+ POST /ports 支持 [1.0] ← 没变
+
+ZNS v1.2 发布(PATCH segment 删除了某个旧字段):
+ PATCH /segments 支持 [1.1, 1.2] ← 不再支持 1.0
+ POST /ports 支持 [1.0, 1.1] ← 新增 1.1
+....
+
+=== 版本查询:HTTP OPTIONS 方法
+
+每个 API 路径通过标准的 HTTP OPTIONS 方法返回自己支持的版本。
+
+....
+Cloud ZNS
+ │ │
+ │ OPTIONS /zns/api/v1/segments │
+ │───────────────────────────────────────────────────►│
+ │ │
+ │ ◄── 204 No Content │
+ │ Headers: │
+ │ Allow: GET, POST, DELETE │ ← 标准头
+ │ X-Api-Versions: POST=1.0,1.1; │ ← 自定义头
+ │ DELETE=1.0; │
+ │ GET=1.0,1.1 │
+ │ │
+ │ OPTIONS /zns/api/v1/segments/{uuid} │
+ │───────────────────────────────────────────────────►│
+ │ │
+ │ ◄── 204 No Content │
+ │ Headers: │
+ │ Allow: GET, PATCH │
+ │ X-Api-Versions: PATCH=1.0,1.1,1.2; │
+ │ GET=1.0,1.1 │
+ │ │
+....
+
+`X-Api-Versions` Header 格式:
+....
+X-Api-Versions: {METHOD1}={ver1},{ver2};{METHOD2}={ver1},{ver2}
+
+示例:POST=1.0,1.1;DELETE=1.0;GET=1.0,1.1
+....
+
+=== 版本声明:X-Api-Version 请求头
+
+Cloud 每次发送 API 请求时,通过 Header 声明自己发送的是哪个版本的格式:
+
+....
+Cloud ZNS
+ │ │
+ │ PATCH /zns/api/v1/segments/{uuid} │
+ │ Headers: │
+ │ X-Api-Version: 1.1 ← 声明版本 │
+ │ Content-Type: application/json │
+ │ x-web-hook: ... │
+ │ x-job-uuid: ... │
+ │ Body: │
+ │ { "name": "...", "mtu": 9000 } ← 1.1 格式 │
+ │───────────────────────────────────────────────────►│
+ │ │
+ │ ZNS 收到请求: │
+ │ 1. 检查 X-Api-Version = 1.1 │
+ │ 2. PATCH segments 支持 1.1? → 用 1.1 处理逻辑 │
+ │ 不支持? → 返回 400 │
+ │ │
+....
+
+==== ZNS 处理不支持的版本
+
+....
+ZNS 返回:
+ HTTP 400 Bad Request
+ {
+ "error": "unsupported_api_version",
+ "message": "PATCH /segments does not support version 1.3",
+ "supported_versions": ["1.0", "1.1", "1.2"]
+ }
+....
+
+==== ZNS 处理没有版本 Header 的请求(兼容旧版 Cloud)
+
+没有 `X-Api-Version` header 时,ZNS 按该 API 支持的最低版本处理,确保旧版 Cloud 仍能正常使用。
+
+=== 版本检查时机
+
+....
+核心原则:
+ 不在每次 API 调用前询问 ZNS(会导致每次操作延迟翻倍)
+ 而是在 3 个时机批量拉取版本信息,缓存在内存中
+
+ ZNS 版本一年才变 2-3 次,变版本必然有运维动作(升级),
+ 升级后通常会触发 reconnect。Ping 间隔内版本突然变化的概率极低。
+....
+
+[cols="2,3,3"]
+|===
+|时机 |触发条件 |行为
+
+|添加控制器(preInit)
+|用户首次添加 ZNS 控制器到 Cloud
+|对每个 API 路径发 OPTIONS → 检查兼容性 → 缓存。不兼容则拒绝添加
+
+|重连控制器(reconnect)
+|管理员手动触发重连,或网络恢复后自动重连
+|清除旧缓存 → 重新发 OPTIONS → 检查 → 缓存。不兼容则重连失败
+
+|心跳探活(ping)
+|Cloud 定时 Ping ZNS
+|对每个 API 路径发 OPTIONS → 与缓存对比。版本变化 → 刷新缓存 → 重新检查。不兼容 → Ping 失败 → 标记 Disconnected
+
+|===
+
+=== 双层防御
+
+版本兼容性在两个层面保证:
+
+....
+第 1 层: Cloud 提前检查(发请求之前)
+ Cloud 在 preInit/reconnect/ping 时通过 OPTIONS 拉取版本
+ 缓存到内存,每次发请求前本地查缓存
+ 不兼容 → 不发请求,直接报错或降级
+ 作用: 避免发出注定失败的请求
+
+第 2 层: ZNS 兜底校验(收到请求时)
+ ZNS 收到请求后检查 X-Api-Version header
+ 不支持 → 返回 400 + supported_versions
+ 作用: 即使 Cloud 缓存过期,ZNS 也能拒绝不兼容的请求
+....
+
+=== 不兼容时的处理策略
+
+不同 API 不兼容时,处理方式不同:
+
+[cols="3,1,3"]
+|===
+|API (路径 + 方法) |重要性 |不兼容时行为
+
+|POST /segments(创建网络)
+|关键
+|阻断: 拒绝添加/重连控制器
+
+|DELETE /segments(删除网络)
+|关键
+|阻断
+
+|POST /segments/{id}/ports(创建端口)
+|关键
+|阻断
+
+|DELETE /segments/{id}/ports(删除端口)
+|关键
+|阻断
+
+|GET /segments(列表,对账用)
+|关键
+|阻断
+
+|GET /segments/{id}/ports(列表,对账用)
+|关键
+|阻断
+
+|PATCH /segments/{uuid}(更新 mtu 等)
+|非关键
+|降级: 跳过 + 打印告警日志
+
+|PATCH /segments/{id}/ports/{id}(更新端口)
+|非关键
+|降级: 跳过 + 打印告警日志
+
+|===
+
+=== 兼容旧版
+
+==== Cloud 遇到旧版 ZNS(不支持 OPTIONS 版本响应)
+
+如果 OPTIONS 返回 404 或 204 但没有 `X-Api-Versions` header,Cloud 假设所有 API 只支持 `["1.0"]`。
+
+==== ZNS 遇到旧版 Cloud(不带 X-Api-Version header)
+
+没有 `X-Api-Version` header 时,ZNS 按最低支持版本处理。
+
+=== 错误信息设计
+
+==== Cloud 新,ZNS 旧
+
+....
+ZNS controller [192.168.1.10] does not support the following APIs
+required by this Cloud version:
+ - PATCH /segments: Cloud sends v1.1, ZNS supports [1.0]
+ - POST /ports: Cloud sends v1.1, ZNS supports [1.0]
+Please upgrade ZNS to a compatible version.
+....
+
+==== ZNS 新,Cloud 旧
+
+....
+This Cloud version is not compatible with ZNS [192.168.1.10]:
+ - PATCH /segments: Cloud sends v1.0, ZNS supports [1.1, 1.2] (v1.0 dropped)
+Please upgrade Cloud to a compatible version.
+....
+
+==== 非关键 API 降级
+
+....
+WARN: ZNS [192.168.1.10] does not support PATCH /segments v1.1
+(ZNS supports [1.0]). MTU sync will be skipped.
+This does not affect core network operations. Upgrade ZNS to enable MTU sync.
+....
+
+=== 版本维护规范
+
+==== 什么时候需要升版本
+
+需要升版本:
+
+* 请求体新增必填字段
+* 请求体删除字段
+* 字段类型变化(string → int)
+* 字段语义变化(单位从 MB 变成 KB)
+* 响应体结构变化(影响 Cloud 解析)
+
+不需要升版本:
+
+* 请求体新增可选字段(ZNS 忽略未知字段即可)
+* 纯内部实现变化,接口不变
+* 性能优化,接口不变
+
+==== ZNS 的向后兼容策略
+
+建议每个 API 至少兼容前 2 个版本。
+
+....
+示例: PATCH /segments
+ v1.2 发布时 → supported: [1.0, 1.1, 1.2] (兼容 3 个版本)
+ v1.3 发布时 → supported: [1.1, 1.2, 1.3] (下线 1.0)
+
+这样可以给 Cloud 升级留出足够的时间窗口。
+....
+
+=== 双方职责总结
+
+==== ZNS 侧
+
+. 每个 API 路径实现 OPTIONS 方法,返回 `Allow` + `X-Api-Versions` header
+. 收到请求时检查 `X-Api-Version` header:支持则用对应版本处理;不支持则返回 400;缺失则按最低版本处理
+. 每次 API 格式变化时在该 API 的 OPTIONS 响应中新增版本号,保留旧版本兼容
+. 新增 API 时实现 OPTIONS,初始版本为 `1.0`
+
+==== Cloud 侧
+
+. 维护 `CLOUD_API_VERSIONS` 表,记录当前发出的每个 API 的格式版本
+. 在 preInit、reconnect、ping 三个时机通过 OPTIONS 拉取版本并缓存
+. 每次发请求带上 `X-Api-Version` header
+. 每次修改 API 请求体格式时同步更新版本号
+. 标记每个 API 的重要性(关键/非关键),决定不兼容时是阻断还是降级
diff --git a/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..f0b2dd42194 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,7 +39,11 @@ 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 ACCEL_TYPE_VDPA = "vDPA";
+ public static final String ACCEL_TYPE_VHOST_USER_SPACE = "dpdkvhostuserclient";
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/header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java
new file mode 100644
index 00000000000..97606fddcb4
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java
@@ -0,0 +1,49 @@
+package org.zstack.header.vm;
+
+import org.zstack.header.core.Completion;
+
+import java.util.List;
+
+/**
+ * Extension point called after VmNicVO is persisted and non-SDN NIC IPs have
+ * been allocated (i.e. after VmAllocateNicIpFlow), but before the VM is
+ * instantiated on the hypervisor.
+ *
+ * Implementations should:
+ * 1. Filter NICs that belong to SDN-managed L2 networks (via VSwitchType).
+ * 2. Send REST API calls to the SDN controller to create ports (e.g.
+ * OVN logical switch ports, ZNS segment ports).
+ * 3. For controllers that return IP addresses (e.g. ZNS), write the
+ * returned IPs back into UsedIpVO and VmNicVO.
+ *
+ * If the implementation fails, VmAllocateSdnNicFlow will trigger a rollback
+ * via {@link #rollbackSdnNic}.
+ */
+public interface AfterAllocateSdnNicExtensionPoint {
+ /**
+ * Create SDN ports for the given NICs.
+ *
+ * @param spec the VM instance spec
+ * @param nics all NICs from spec.getDestNics() — implementation filters SDN NICs internally
+ * @param completion success/fail callback
+ */
+ void afterAllocateSdnNic(VmInstanceSpec spec, List nics, Completion completion);
+
+ /**
+ * Rollback: remove SDN ports and clean up any IPs allocated by the SDN controller.
+ *
+ * @param spec the VM instance spec
+ * @param nics all NICs from spec.getDestNics()
+ * @param completion success/fail callback (best-effort — failures should be logged but not block rollback)
+ */
+ void rollbackSdnNic(VmInstanceSpec spec, List nics, Completion completion);
+
+ /**
+ * Release SDN ports for NICs being detached or destroyed.
+ * Used by VmDetachNicFlow and VmReturnReleaseNicFlow.
+ *
+ * @param nics NICs to release (implementation filters SDN NICs internally)
+ * @param completion success/fail callback (best-effort)
+ */
+ void releaseSdnNics(List nics, Completion completion);
+}
diff --git a/header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java
new file mode 100644
index 00000000000..0afba727a42
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java
@@ -0,0 +1,19 @@
+package org.zstack.header.vm;
+
+import org.zstack.header.core.Completion;
+
+/**
+ * Extension point called after IP address(es) have been successfully allocated
+ * and flushed to the database for VmNics in VmAllocateNicIpFlow.
+ *
+ * At the time this fires:
+ * - VmNicVO rows exist in the database (created by VmAllocateNicFlow)
+ * - UsedIpVO rows are committed (allocated by VmAllocateNicIpFlow)
+ * - spec.getDestNics() contains up-to-date NIC inventories with IP info
+ *
+ * If the implementation fails, the flow chain rolls back:
+ * VmAllocateNicIpFlow.rollback (returns IPs) → VmAllocateNicFlow.rollback (deletes NICs).
+ */
+public interface AfterAllocateVmNicIpExtensionPoint {
+ void afterAllocateVmNicIp(VmInstanceSpec spec, Completion completion);
+}
diff --git a/header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java
new file mode 100644
index 00000000000..102114111ca
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java
@@ -0,0 +1,12 @@
+package org.zstack.header.vm;
+
+import org.zstack.header.core.Completion;
+
+/**
+ * Extension point called after a VmNic has been deleted from the database.
+ * Implementations perform post-deletion cleanup (e.g., deleting SDN segment ports).
+ * Cloud DB deletion must succeed before this extension point is invoked.
+ */
+public interface AfterReleaseVmNicExtensionPoint {
+ void afterReleaseVmNic(VmNicInventory nic, Completion completion);
+}
diff --git a/header/src/main/java/org/zstack/header/vm/BeforeAllocateVmNicExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/BeforeAllocateVmNicExtensionPoint.java
new file mode 100644
index 00000000000..f354a9d2521
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/vm/BeforeAllocateVmNicExtensionPoint.java
@@ -0,0 +1,14 @@
+package org.zstack.header.vm;
+
+import org.zstack.header.core.Completion;
+
+/**
+ * Extension point called after a VmNic is persisted but before it is fully configured
+ * in VmAllocateNicFlow. The VmNicVO (and its ResourceVO) already exists in the database,
+ * so implementations may safely create SystemTags referencing the NIC UUID.
+ * If the implementation fails, the NIC will be cleaned up during flow rollback.
+ * Use case: create SDN segment ports and save port-UUID system tags for the NIC.
+ */
+public interface BeforeAllocateVmNicExtensionPoint {
+ void beforeAllocateVmNic(VmNicInventory nic, VmInstanceSpec spec, Completion completion);
+}
diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceNicFactory.java b/header/src/main/java/org/zstack/header/vm/VmInstanceNicFactory.java
index 44ce4a8831e..690c30cf46d 100755
--- a/header/src/main/java/org/zstack/header/vm/VmInstanceNicFactory.java
+++ b/header/src/main/java/org/zstack/header/vm/VmInstanceNicFactory.java
@@ -1,5 +1,6 @@
package org.zstack.header.vm;
+import org.zstack.header.core.Completion;
import org.zstack.header.network.l2.VSwitchType;
import org.zstack.header.network.l3.UsedIpInventory;
diff --git a/header/src/main/java/org/zstack/header/vm/VmOvsNicConstant.java b/header/src/main/java/org/zstack/header/vm/VmOvsNicConstant.java
deleted file mode 100644
index 964a41e8861..00000000000
--- a/header/src/main/java/org/zstack/header/vm/VmOvsNicConstant.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.zstack.header.vm;
-
-import org.zstack.header.configuration.PythonClass;
-
-@PythonClass
-public class VmOvsNicConstant {
- public static final String ACCEL_TYPE_VDPA = "vDPA";
- public static final String ACCEL_TYPE_VHOST_USER_SPACE = "dpdkvhostuserclient";
-}
diff --git a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java
index 6f0b796e5b8..79578eb5686 100755
--- a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java
+++ b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java
@@ -80,7 +80,9 @@ private void validate(final APIAttachL2NetworkToClusterMsg msg) {
/* current ovs only support vlan, vxlan*/
L2NetworkVO l2 = dbf.findByUuid(msg.getL2NetworkUuid(), L2NetworkVO.class);
- if (!StringUtils.isEmpty(l2.getPhysicalInterface())) {
+ // ZNS L2 networks are managed by SDN controller, physicalInterface is irrelevant
+ if (!L2NetworkConstant.VSWITCH_TYPE_ZNS.equals(l2.getvSwitchType())
+ && !StringUtils.isEmpty(l2.getPhysicalInterface())) {
/* find l2 network with same physical interface, but different vswitch Type */
List otherL2s = Q.New(L2NetworkVO.class).select(L2NetworkVO_.uuid)
.eq(L2NetworkVO_.physicalInterface, l2.getPhysicalInterface())
@@ -127,26 +129,45 @@ private void validate(APICreateL2NetworkMsg msg) {
private void validate(APIChangeL2NetworkVlanIdMsg msg) {
L2NetworkVO l2 = dbf.findByUuid(msg.getL2NetworkUuid(), L2NetworkVO.class);
- l2.getAttachedClusterRefs().forEach(ref -> {
- if (Q.New(HostVO.class).eq(HostVO_.clusterUuid, ref.getClusterUuid())
- .notEq(HostVO_.status, HostStatus.Connected).isExists()) {
- throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_NETWORK_L2_10013, "cannot change vlan for l2Network[uuid:%s]" +
- " because there are hosts status in Connecting or Disconnected", l2.getUuid()));
- }
- if (!Q.New(ClusterVO.class).eq(ClusterVO_.uuid, ref.getClusterUuid())
- .eq(ClusterVO_.hypervisorType, L2NetworkConstant.KVM_HYPERVISOR_TYPE).isExists()) {
- throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_NETWORK_L2_10014, "cannot change vlan for l2Network[uuid:%s]" +
- " because it only supports an L2Network that is exclusively attached to a kvm cluster", l2.getUuid()));
- }
- });
+ if (!L2NetworkConstant.VSWITCH_TYPE_ZNS.equals(l2.getvSwitchType())) {
+ l2.getAttachedClusterRefs().forEach(ref -> {
+ if (Q.New(HostVO.class).eq(HostVO_.clusterUuid, ref.getClusterUuid())
+ .notEq(HostVO_.status, HostStatus.Connected).isExists()) {
+ throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_NETWORK_L2_10013, "cannot change vlan for l2Network[uuid:%s]" +
+ " because there are hosts status in Connecting or Disconnected", l2.getUuid()));
+ }
+ if (!Q.New(ClusterVO.class).eq(ClusterVO_.uuid, ref.getClusterUuid())
+ .eq(ClusterVO_.hypervisorType, L2NetworkConstant.KVM_HYPERVISOR_TYPE).isExists()) {
+ throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_NETWORK_L2_10014, "cannot change vlan for l2Network[uuid:%s]" +
+ " because it only supports an L2Network that is exclusively attached to a kvm cluster", l2.getUuid()));
+ }
+ });
+ }
// pvlan isolated not support change vlan
if (l2.getIsolated()) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10015, "cannot change vlan for l2Network[uuid:%s]" +
" because this l2Network is isolated", l2.getUuid()));
}
+ String targetType = StringUtils.trimToNull(msg.getType());
+ msg.setType(targetType);
+ // When type is not specified (or blank), default to the current network type.
+ if (targetType == null) {
+ targetType = l2.getType();
+ msg.setType(targetType);
+ }
+
+ boolean targetIsVlan = L2NetworkConstant.L2_VLAN_NETWORK_TYPE.equals(targetType);
+ boolean targetIsNoVlan = L2NetworkConstant.L2_NO_VLAN_NETWORK_TYPE.equals(targetType);
+ boolean targetIsGeneve = L2NetworkConstant.L2_GENEVE_NETWORK_TYPE.equals(targetType);
+ boolean targetIsVxlan = L2NetworkConstant.VXLAN_NETWORK_TYPE.equals(targetType);
+ if (!targetIsVlan && !targetIsNoVlan && !targetIsGeneve && !targetIsVxlan) {
+ throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10021,
+ "unsupported l2Network type[%s] for ChangeL2NetworkVlanId", targetType));
+ }
+
String sdnControllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID
.getTokenByResourceUuid(msg.getL2NetworkUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (msg.getType().equals(L2NetworkConstant.L2_VLAN_NETWORK_TYPE)) {
+ if (targetIsVlan) {
if (msg.getVlan() == null) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10016, "vlan is required for " +
"ChangeL2NetworkVlanId with type[%s]", msg.getType()));
@@ -157,7 +178,9 @@ private void validate(APIChangeL2NetworkVlanIdMsg msg) {
List attachedClusters = l2.getAttachedClusterRefs().stream()
.map(L2NetworkClusterRefVO::getClusterUuid).collect(Collectors.toList());
List l2s;
- if (sdnControllerUuid == null) {
+ if (attachedClusters.isEmpty()) {
+ l2s = java.util.Collections.emptyList();
+ } else if (sdnControllerUuid == null) {
l2s = SQL.New("select l2" +
" from L2NetworkVO l2, L2NetworkClusterRefVO ref" +
" where l2.uuid = ref.l2NetworkUuid" +
@@ -190,7 +213,7 @@ private void validate(APIChangeL2NetworkVlanIdMsg msg) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10018, "There has been a l2Network attached to cluster with virtual network id[%s] and physical interface[%s]. Failed to change L2 network[uuid:%s]",
msg.getVlan(), l2.getPhysicalInterface(), l2.getUuid()));
}
- } else if (msg.getType().equals(L2NetworkConstant.L2_NO_VLAN_NETWORK_TYPE)) {
+ } else if (targetIsNoVlan) {
if (msg.getVlan() != null) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10019, "vlan is not allowed for " +
"ChangeL2NetworkVlanId with type[%s]", msg.getType()));
@@ -198,7 +221,9 @@ private void validate(APIChangeL2NetworkVlanIdMsg msg) {
List attachedClusters = l2.getAttachedClusterRefs().stream()
.map(L2NetworkClusterRefVO::getClusterUuid).collect(Collectors.toList());
List l2s;
- if (sdnControllerUuid != null) {
+ if (attachedClusters.isEmpty()) {
+ l2s = java.util.Collections.emptyList();
+ } else if (sdnControllerUuid != null) {
l2s = SQL.New("select l2" +
" from L2NetworkVO l2, L2NetworkClusterRefVO ref, SystemTagVO tag" +
" where l2.uuid = ref.l2NetworkUuid" +
@@ -227,6 +252,15 @@ private void validate(APIChangeL2NetworkVlanIdMsg msg) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10020, "There has been a l2Network attached to cluster that has physical interface[%s]. Failed to change l2Network[uuid:%s]",
l2.getPhysicalInterface(), l2.getUuid()));
}
+ } else if (targetIsGeneve) {
+ if (msg.getVlan() == null) {
+ throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10016, "vni is required for " +
+ "ChangeL2NetworkVlanId with type[%s]", msg.getType()));
+ }
+ if (msg.getVlan() < 1 || msg.getVlan() > 16777215) {
+ throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10017, "invalid vni[%d] for " +
+ "ChangeL2NetworkVlanId, must be between 1 and 16777215", msg.getVlan()));
+ }
}
}
}
diff --git a/network/src/main/java/org/zstack/network/l2/L2NetworkExtensionPointEmitter.java b/network/src/main/java/org/zstack/network/l2/L2NetworkExtensionPointEmitter.java
index fa864bafaf2..b1eecae4d5e 100755
--- a/network/src/main/java/org/zstack/network/l2/L2NetworkExtensionPointEmitter.java
+++ b/network/src/main/java/org/zstack/network/l2/L2NetworkExtensionPointEmitter.java
@@ -53,6 +53,20 @@ public void run(L2NetworkDeleteExtensionPoint arg) {
});
}
+ public void beforeUpdate(final L2NetworkInventory inv) {
+ for (L2NetworkUpdateExtensionPoint ext : updateExtensions) {
+ try {
+ ext.beforeChangeL2NetworkVlanId(inv);
+ } catch (RuntimeException e) {
+ // propagate validation failures and other runtime exceptions immediately
+ throw e;
+ } catch (Exception e) {
+ logger.warn(String.format("unhandled exception in L2NetworkUpdateExtensionPoint.beforeChangeL2NetworkVlanId of %s",
+ ext.getClass().getCanonicalName()), e);
+ }
+ }
+ }
+
public void afterUpdate(final L2NetworkInventory inv) {
CollectionUtils.safeForEach(updateExtensions, arg -> arg.afterChangeL2NetworkVlanId(inv));
}
diff --git a/network/src/main/java/org/zstack/network/l2/L2NetworkHostHelper.java b/network/src/main/java/org/zstack/network/l2/L2NetworkHostHelper.java
index a612b3847c5..0136649f4bf 100644
--- a/network/src/main/java/org/zstack/network/l2/L2NetworkHostHelper.java
+++ b/network/src/main/java/org/zstack/network/l2/L2NetworkHostHelper.java
@@ -15,7 +15,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
import static java.util.Arrays.asList;
@@ -87,6 +86,10 @@ public L2NetworkHostRefInventory getL2NetworkHostRef(String l2NetworkUuid, Strin
}
public static Set getHostsByL2NetworkAttachedCluster(L2NetworkInventory l2NetworkInventory) {
+ if (l2NetworkInventory.getAttachedClusterUuids() == null || l2NetworkInventory.getAttachedClusterUuids().isEmpty()) {
+ return new HashSet<>();
+ }
+
return new HashSet<>(Q.New(HostVO.class)
.in(HostVO_.clusterUuid, l2NetworkInventory.getAttachedClusterUuids())
.notIn(HostVO_.state,asList(HostState.PreMaintenance, HostState.Maintenance))
diff --git a/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java b/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java
index 821a76ad462..ec9a7a5be0b 100755
--- a/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java
+++ b/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java
@@ -443,6 +443,7 @@ public String getSyncSignature() {
@Override
public void run(SyncTaskChain chain) {
+ extpEmitter.beforeUpdate(getSelfInventory());
changeL2NetworkVlanId(msg, new Completion(chain) {
@Override
public void success() {
@@ -968,6 +969,10 @@ protected void scripts() {
}
L2NetworkVO tl2 = Q.New(L2NetworkVO.class).eq(L2NetworkVO_.uuid, msg.getL2NetworkUuid()).find();
+ // ZNS L2NoVlan segments are uniquely identified by SDN controller, not by physicalInterface
+ if (L2NetworkConstant.VSWITCH_TYPE_ZNS.equals(tl2.getvSwitchType())) {
+ return;
+ }
for (L2NetworkVO l2 : l2s) {
if (l2.getPhysicalInterface().equals(tl2.getPhysicalInterface())) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10006, "There has been a l2Network[uuid:%s, name:%s] attached to cluster[uuid:%s] that has physical interface[%s]. Failed to attach l2Network[uuid:%s]",
@@ -980,8 +985,10 @@ protected void scripts() {
l2s = SQL.New("select l2" +
" from L2VlanNetworkVO l2, L2NetworkClusterRefVO ref" +
" where l2.uuid = ref.l2NetworkUuid" +
- " and ref.clusterUuid = :clusterUuid")
- .param("clusterUuid", msg.getClusterUuid()).list();
+ " and ref.clusterUuid = :clusterUuid" +
+ " and l2.vSwitchType != :znsType")
+ .param("clusterUuid", msg.getClusterUuid())
+ .param("znsType", L2NetworkConstant.VSWITCH_TYPE_ZNS).list();
} else {
l2s = SQL.New("select l2" +
" from L2VlanNetworkVO l2, L2NetworkClusterRefVO ref, SystemTagVO tag" +
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 384a5d2c1df..77b7ca81495 100755
--- a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java
+++ b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java
@@ -184,6 +184,41 @@ public void run(MessageReply reply) {
}
});
}
+ }).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(dbf.findByUuid(msg.getL3NetworkUuid(), L3NetworkVO.class));
+ 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/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java
index a8a1378288b..6ed711cb251 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java
@@ -1242,6 +1242,14 @@ public static class NicTO extends BaseVirtualDeviceTO {
private Boolean isolated;
+ // bridge sub-type: null for Linux bridge, "openvswitch" for OVS bridge
+ // generates in libvirt XML
+ private String bridgePortType;
+
+ // OVS external_ids:iface-id, used by SDN controller to identify the port
+ // generates
+ private String interfaceId;
+
public List getIps() {
return ips;
}
@@ -1434,6 +1442,22 @@ public void setL2NetworkUuid(String l2NetworkUuid) {
this.l2NetworkUuid = l2NetworkUuid;
}
+ public String getBridgePortType() {
+ return bridgePortType;
+ }
+
+ public void setBridgePortType(String bridgePortType) {
+ this.bridgePortType = bridgePortType;
+ }
+
+ public String getInterfaceId() {
+ return interfaceId;
+ }
+
+ public void setInterfaceId(String interfaceId) {
+ this.interfaceId = interfaceId;
+ }
+
public static NicTO fromVmNicInventory(VmNicInventory nic) {
KVMAgentCommands.NicTO to = new KVMAgentCommands.NicTO();
to.setMac(nic.getMac());
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMApiInterceptor.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMApiInterceptor.java
index 93fd688935b..e8e30a6711f 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMApiInterceptor.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMApiInterceptor.java
@@ -111,6 +111,11 @@ private void validate(APIAttachL2NetworkToClusterMsg msg) {
return;
}
+ // ZNS L2 networks don't create vlan devices on host, skip length check
+ if (L2NetworkConstant.VSWITCH_TYPE_ZNS.equals(l2.getvSwitchType())) {
+ return;
+ }
+
if (NetworkUtils.generateVlanDeviceName(l2.getPhysicalInterface(), l2.getVlan()).length()
> L2NetworkConstant.LINUX_IF_NAME_MAX_SIZE) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_KVM_10139, "cannot create vlan-device on %s because it's too long"
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java
index a245757517d..3872012b6ce 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java
@@ -4179,6 +4179,16 @@ private NicTO completeNicInfo(VmNicInventory nic) {
KVMCompleteNicInformationExtensionPoint extp = factory.getCompleteNicInfoExtension(L2NetworkType.valueOf(l2inv.getType()));
NicTO to = extp.completeNicInformation(l2inv, l3Inv, nic);
+ if (L2NetworkConstant.VSWITCH_TYPE_ZNS.equals(l2inv.getvSwitchType())) {
+ to.setBridgeName("br-int");
+ to.setBridgePortType("openvswitch");
+ String ifaceId = VmSystemTags.IFACE_ID.getTokenByResourceUuid(
+ nic.getUuid(), VmSystemTags.IFACE_ID_TOKEN);
+ if (ifaceId != null) {
+ to.setInterfaceId(ifaceId);
+ }
+ }
+
if (to.getUseVirtio() == null) {
to.setUseVirtio(VmSystemTags.VIRTIO.hasTag(nic.getVmInstanceUuid()));
to.setIps(getCleanTrafficIp(nic));
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java
index acb129fe6dc..86a32d715c9 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java
@@ -47,6 +47,7 @@
import org.zstack.header.network.l2.L2NetworkType;
import org.zstack.header.network.l2.L2NetworkVO;
import org.zstack.header.network.l2.L2NetworkVO_;
+
import org.zstack.header.rest.RESTFacade;
import org.zstack.header.rest.SyncHttpCallHandler;
import org.zstack.header.tag.FormTagExtensionPoint;
@@ -371,7 +372,7 @@ protected void populateExtensions() {
public KVMCompleteNicInformationExtensionPoint getCompleteNicInfoExtension(L2NetworkType type) {
KVMCompleteNicInformationExtensionPoint extp = completeNicInfoExtensions.get(type);
if (extp == null) {
- throw new IllegalArgumentException(String.format("unble to fine KVMCompleteNicInformationExtensionPoint supporting L2NetworkType[%s]", type));
+ throw new IllegalArgumentException(String.format("unable to find KVMCompleteNicInformationExtensionPoint supporting L2NetworkType[%s]", type));
}
return extp;
}
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 1ed462ecfcd..3ec9dd0780e 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.java
@@ -236,7 +236,7 @@ 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 (L2NetworkConstant.ACCEL_TYPE_VHOST_USER_SPACE.equals(nic.getType())) {
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 74e700dd9f6..e486fddff98 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java
@@ -249,7 +249,7 @@ 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 (L2NetworkConstant.ACCEL_TYPE_VHOST_USER_SPACE.equals(nic.getType())) {
to.setSrcPath(L2NetworkConstant.OVN_DPDK_VNIC_SRC_PATH + nic.getInternalName());
}
diff --git a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerBase.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerBase.java
index f1d1f11829b..5fb9ea1c368 100644
--- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerBase.java
+++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerBase.java
@@ -903,7 +903,7 @@ public void fail(ErrorCode errorCode) {
@Override
public void run(FlowTrigger trigger, Map data) {
sdnPingTracker.untrack(msg.getSdnControllerUuid());
- dbf.removeByPrimaryKey(msg.getSdnControllerUuid(), SdnControllerVO.class);
+ controller.deleteSdnControllerDb(self);
trigger.next();
}
}).done(new FlowDoneHandler(completion) {
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/SdnControllerManagerImpl.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java
index 2368fbb95ae..e879d436472 100644
--- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java
+++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java
@@ -47,10 +47,10 @@
import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*;
public class SdnControllerManagerImpl extends AbstractService implements SdnControllerManager,
- L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, InstantiateResourceOnAttachingNicExtensionPoint,
- PreVmInstantiateResourceExtensionPoint, VmReleaseResourceExtensionPoint,
- ReleaseNetworkServiceOnDetachingNicExtensionPoint, SecurityGroupGetSdnBackendExtensionPoint,
- AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint {
+ L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint,
+ SecurityGroupGetSdnBackendExtensionPoint,
+ AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint,
+ AfterAllocateSdnNicExtensionPoint {
private static final CLogger logger = Utils.getLogger(SdnControllerManagerImpl.class);
private static final Logger log = LoggerFactory.getLogger(SdnControllerManagerImpl.class);
@@ -270,8 +270,13 @@ private void handle(APIAddSdnControllerMsg msg) {
@Override
public void run(MessageReply reply) {
if (reply.isSuccess()) {
- tagMgr.createTagsFromAPICreateMessage(msg, vo.getUuid(), SdnControllerVO.class.getSimpleName());
- event.setInventory(SdnControllerInventory.valueOf(dbf.findByUuid(vo.getUuid(), SdnControllerVO.class)));
+ try {
+ tagMgr.createTagsFromAPICreateMessage(msg, vo.getUuid(), SdnControllerVO.class.getSimpleName());
+ event.setInventory(SdnControllerInventory.valueOf(dbf.findByUuid(vo.getUuid(), SdnControllerVO.class)));
+ } catch (Exception e) {
+ logger.warn(String.format("failed to load SdnControllerVO[uuid:%s] after init: %s",
+ vo.getUuid(), e.getMessage()), e);
+ }
} else {
event.setError(reply.getError());
}
@@ -454,267 +459,13 @@ public void done(ErrorCodeList errorCodeList) {
});
}
- @Override
- public void releaseVmResource(VmInstanceSpec spec, Completion completion) {
- if (VmInstanceConstant.VmOperation.DetachNic != spec.getCurrentVmOperation() &&
- VmInstanceConstant.VmOperation.Destroy != spec.getCurrentVmOperation()) {
- completion.success();
- return;
- }
-
- if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) {
- completion.success();
- return;
- }
-
- // we run into this situation when VM nics are all detached and the
- // VM is being rebooted
- if (spec.getDestNics().isEmpty()) {
- completion.success();
- return;
- }
-
- Map> nicMaps = new HashMap<>();
- for (VmNicInventory nic : spec.getDestNics()) {
- L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
- if (l3Vo == null) {
- continue;
- }
-
- L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
- if (l2VO == null) {
- continue;
- }
-
- VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType());
- if (vSwitchType.getSdnControllerType() == null) {
- continue;
- }
-
- String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
- l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (controllerUuid == null) {
- completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10005, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid()));
- return;
- }
-
- nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic);
- }
-
- if (nicMaps.isEmpty()) {
- completion.success();
- return;
- }
-
- removeLogicalPort(nicMaps, completion);
- }
-
- @Override
- public void instantiateResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, Completion completion) {
- L2NetworkVO l2NetworkVO = dbf.findByUuid(l3.getL2NetworkUuid(), L2NetworkVO.class);
- VSwitchType vSwitchType = VSwitchType.valueOf(l2NetworkVO.getvSwitchType());
- if (vSwitchType.getSdnControllerType() == null) {
- completion.success();
- return;
- }
-
- String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
- l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (controllerUuid == null) {
- completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid()));
- return;
- }
-
- Map> nicMaps = new HashMap<>();
- List nics = new ArrayList<>();
- nics.add(spec.getDestNics().get(0));
- nicMaps.put(controllerUuid, nics);
- sdnAddVmNics(nicMaps, completion);
- }
-
- @Override
- public void releaseResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, NoErrorCompletion completion) {
- L2NetworkVO l2NetworkVO = dbf.findByUuid(l3.getL2NetworkUuid(), L2NetworkVO.class);
- VSwitchType vSwitchType = VSwitchType.valueOf(l2NetworkVO.getvSwitchType());
- if (vSwitchType.getSdnControllerType() == null) {
- completion.done();
- return;
- }
-
- String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
- l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (controllerUuid == null) {
- logger.warn(String.format("sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid()));
- completion.done();
- return;
- }
-
- Map> nicMaps = new HashMap<>();
- List nics = new ArrayList<>();
- nics.add(spec.getDestNics().get(0));
- nicMaps.put(controllerUuid, nics);
-
- removeLogicalPort(nicMaps, new Completion(completion) {
- @Override
- public void success() {
- completion.done();
- }
-
- @Override
- public void fail(ErrorCode errorCode) {
- logger.info(String.format("failed to remove logical port for vm[uuid:%s] nic[internalName:%s], because: %s",
- spec.getVmInventory().getUuid(), spec.getDestNics().get(0).getInternalName(), errorCode.getDetails()));
- completion.done();
- }
- });
- }
-
- @Override
- public void releaseResourceOnDetachingNic(VmInstanceSpec spec, VmNicInventory nic, NoErrorCompletion completion) {
- L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
- L2NetworkVO l2NetworkVO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
- VSwitchType vSwitchType = VSwitchType.valueOf(l2NetworkVO.getvSwitchType());
- if (vSwitchType.getSdnControllerType() == null) {
- completion.done();
- return;
- }
-
- String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
- l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (controllerUuid == null) {
- logger.warn(String.format("sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid()));
- completion.done();
- return;
- }
-
- Map> nicMaps = new HashMap<>();
- List nics = new ArrayList<>();
- nics.add(spec.getDestNics().get(0));
- nicMaps.put(controllerUuid, nics);
-
- removeLogicalPort(nicMaps, new Completion(completion) {
- @Override
- public void success() {
- completion.done();
- }
-
- @Override
- public void fail(ErrorCode errorCode) {
- logger.info(String.format("failed to remove logical port for vm[uuid:%s] nic[internalName:%s], because: %s",
- spec.getVmInventory().getUuid(), spec.getDestNics().get(0).getInternalName(), errorCode.getDetails()));
- completion.done();
- }
- });
- }
-
- @Override
- public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstantiateResourceException {
-
- }
-
- @Override
- public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) {
- if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) {
- completion.success();
- return;
- }
-
- // we run into this situation when VM nics are all detached and the
- // VM is being rebooted
- if (spec.getDestNics().isEmpty()) {
- completion.success();
- return;
- }
-
- Map> nicMaps = new HashMap<>();
- for (VmNicInventory nic : spec.getDestNics()) {
- L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
- if (l3Vo == null) {
- continue;
- }
-
- L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
- if (l2VO == null) {
- continue;
- }
-
- VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType());
- if (vSwitchType.getSdnControllerType() ==null) {
- continue;
- }
-
- String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
- l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (controllerUuid == null) {
- completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10007, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid()));
- return;
- }
-
- nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic);
- }
-
- if (nicMaps.isEmpty()) {
- completion.success();
- return;
- }
-
- sdnAddVmNics(nicMaps, completion);
- }
-
- @Override
- public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) {
- // create/start/reboot vm failed, code will go here VmInstantiateResourcePreFlow.rollack()
- // vm change image failed,
- if (VmInstanceConstant.VmOperation.NewCreate != spec.getCurrentVmOperation()) {
- completion.success();
- return;
- }
-
- if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) {
- completion.success();
- return;
- }
-
- // we run into this situation when VM nics are all detached and the
- // VM is being rebooted
- if (spec.getDestNics().isEmpty()) {
- completion.success();
- return;
- }
-
- Map> nicMaps = new HashMap<>();
- for (VmNicInventory nic : spec.getDestNics()) {
- L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
- if (l3Vo == null) {
- continue;
- }
-
- L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
- if (l2VO == null) {
- continue;
- }
-
- VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType());
- if (vSwitchType.getSdnControllerType() ==null) {
- continue;
- }
-
- String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
- l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
- if (controllerUuid == null) {
- completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10008, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid()));
- return;
- }
-
- nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic);
- }
-
- if (nicMaps.isEmpty()) {
- completion.success();
- return;
- }
-
- removeLogicalPort(nicMaps, completion);
+ /**
+ * Returns true if the L2 network should be skipped for SDN port management:
+ * it has no SDN controller type configured on its VSwitchType.
+ */
+ private boolean shouldSkipSdnForNic(L2NetworkVO l2VO) {
+ VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType());
+ return vSwitchType.getSdnControllerType() == null;
}
@Override
@@ -899,4 +650,113 @@ private SdnControllerVO getSdnControllerVO(L3NetworkInventory l3Network) {
}
return dbf.findByUuid(sdnControllerUuid, SdnControllerVO.class);
}
+
+ @Override
+ public void afterAllocateSdnNic(VmInstanceSpec spec, List nics, Completion completion) {
+ if (nics == null || nics.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ Map> nicMaps = new HashMap<>();
+ for (VmNicInventory nic : nics) {
+ L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
+ if (l3Vo == null) {
+ continue;
+ }
+
+ L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
+ if (l2VO == null || shouldSkipSdnForNic(l2VO)) {
+ continue;
+ }
+
+ String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
+ l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
+ if (controllerUuid == null) {
+ completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] has not attached controller", l2VO.getUuid()));
+ return;
+ }
+
+ nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic);
+ }
+
+ if (nicMaps.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ sdnAddVmNics(nicMaps, completion);
+ }
+
+ @Override
+ public void rollbackSdnNic(VmInstanceSpec spec, List nics, Completion completion) {
+ if (nics == null || nics.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ Map> nicMaps = new HashMap<>();
+ for (VmNicInventory nic : nics) {
+ L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
+ if (l3Vo == null) {
+ continue;
+ }
+
+ L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
+ if (l2VO == null || shouldSkipSdnForNic(l2VO)) {
+ continue;
+ }
+
+ String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
+ l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
+ if (controllerUuid == null) {
+ continue;
+ }
+
+ nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic);
+ }
+
+ if (nicMaps.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ removeLogicalPort(nicMaps, completion);
+ }
+
+ @Override
+ public void releaseSdnNics(List nics, Completion completion) {
+ if (nics == null || nics.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ Map> nicMaps = new HashMap<>();
+ for (VmNicInventory nic : nics) {
+ L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
+ if (l3Vo == null) {
+ continue;
+ }
+
+ L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
+ if (l2VO == null || shouldSkipSdnForNic(l2VO)) {
+ continue;
+ }
+
+ String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid(
+ l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN);
+ if (controllerUuid == null) {
+ continue;
+ }
+
+ nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic);
+ }
+
+ if (nicMaps.isEmpty()) {
+ completion.success();
+ return;
+ }
+
+ removeLogicalPort(nicMaps, completion);
+ }
}
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/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkCheckerImpl.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkCheckerImpl.java
index d3a46ed8d78..9f9e9848714 100644
--- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkCheckerImpl.java
+++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkCheckerImpl.java
@@ -38,7 +38,7 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti
}
private void validate(APIChangeL2NetworkVlanIdMsg msg) {
- if (!msg.getType().equals(VxlanNetworkConstant.VXLAN_NETWORK_TYPE)){
+ if (!VxlanNetworkConstant.VXLAN_NETWORK_TYPE.equals(msg.getType())){
return;
}
if (!NetworkUtils.isValidVni(msg.getVlan())) {
diff --git a/rest/src/main/resources/scripts/RestDocumentationGenerator.groovy b/rest/src/main/resources/scripts/RestDocumentationGenerator.groovy
index c11a35e40a7..7f4df6504a9 100755
--- a/rest/src/main/resources/scripts/RestDocumentationGenerator.groovy
+++ b/rest/src/main/resources/scripts/RestDocumentationGenerator.groovy
@@ -787,10 +787,13 @@ class RestDocumentationGenerator implements DocumentGenerator {
return globalConfigMarkDown
}
- Boolean isConsistent(GlobalConfigMarkDown md, GlobalConfig globalConfig) {
- if (md == null || globalConfig == null) {
- return false
- }
+ List isConsistent(GlobalConfigMarkDown md, GlobalConfig globalConfig) {
+ if (md == null) {
+ return ["GlobalConfigMarkDown is null"]
+ }
+ if (globalConfig == null) {
+ return ["GlobalConfig is null"]
+ }
String mdPath =
PathUtil.join(PathUtil.join(Paths.get("../doc").toAbsolutePath().normalize().toString(),
"globalconfig"), md.globalConfig.category, md.globalConfig.name) + ".md"
@@ -798,70 +801,77 @@ class RestDocumentationGenerator implements DocumentGenerator {
initializer.bindResources.get(globalConfig.getIdentity()).each { classes.add(it.getName()) }
List newClasses = classes.sort()
String validatorString = initializer.validatorMap.get(globalConfig.getIdentity())
- Boolean flag = true
- if (md.globalConfig.name != globalConfig.name) {
- logger.info("name of ${mdPath} is not latest")
- flag = false
- }
- if (md.globalConfig.defaultValue != globalConfig.defaultValue) {
- logger.info("defaultValue of ${mdPath} is not latest")
- flag = false
- }
- if (StringUtils.trimToEmpty(md.globalConfig.description) != StringUtils.trimToEmpty(globalConfig.description)) {
- logger.info("desc of ${mdPath} is not latest")
- flag = false
- }
- if (md.globalConfig.type != globalConfig.type) {
- if (globalConfig.type != null) {
- logger.info("type of ${mdPath} is not latest")
- flag = false
- }
- }
- if (md.globalConfig.category != globalConfig.category) {
- logger.info("category of ${mdPath} is not latest")
- flag = false
- }
- List oldClasses = md.globalConfig.resources.sort()
- if (oldClasses != newClasses) {
- logger.info("classes of ${mdPath} is not latest")
- flag = false
- }
+ List mismatches = []
+ if (md.globalConfig.name != globalConfig.name) {
+ logger.info("name of ${mdPath} is not latest")
+ mismatches.add("name mismatch in ${mdPath}: markdown='${md.globalConfig.name}', current='${globalConfig.name}'")
+ }
+ if (md.globalConfig.defaultValue != globalConfig.defaultValue) {
+ logger.info("defaultValue of ${mdPath} is not latest")
+ mismatches.add("defaultValue mismatch in ${mdPath}: markdown='${md.globalConfig.defaultValue}', current='${globalConfig.defaultValue}'")
+ }
+ if (StringUtils.trimToEmpty(md.globalConfig.description) != StringUtils.trimToEmpty(globalConfig.description)) {
+ logger.info("desc of ${mdPath} is not latest")
+ mismatches.add("description mismatch in ${mdPath}: markdown='${StringUtils.trimToEmpty(md.globalConfig.description)}', current='${StringUtils.trimToEmpty(globalConfig.description)}'")
+ }
+ if (md.globalConfig.type != globalConfig.type) {
+ if (globalConfig.type != null) {
+ logger.info("type of ${mdPath} is not latest")
+ mismatches.add("type mismatch in ${mdPath}: markdown='${md.globalConfig.type}', current='${globalConfig.type}'")
+ }
+ }
+ if (md.globalConfig.category != globalConfig.category) {
+ logger.info("category of ${mdPath} is not latest")
+ mismatches.add("category mismatch in ${mdPath}: markdown='${md.globalConfig.category}', current='${globalConfig.category}'")
+ }
+ List oldClasses = md.globalConfig.resources.sort()
+ if (oldClasses != newClasses) {
+ logger.info("classes of ${mdPath} is not latest")
+ mismatches.add("resources mismatch in ${mdPath}: markdown='${oldClasses}', current='${newClasses}'")
+ }
if (md.globalConfig.valueRange != (validatorString)) {
boolean useBooleanValidator = (globalConfig.type == "java.lang.Boolean"
&& md.globalConfig.valueRange == "{true, false}")
- if (validatorString != null || !useBooleanValidator) {
- logger.info("valueRange of ${mdPath} is not latest")
- logger.info("valueRange = ${md.globalConfig.valueRange} validatorString = ${validatorString}")
- flag = false
- }
- }
- return flag
- }
+ if (validatorString != null || !useBooleanValidator) {
+ logger.info("valueRange of ${mdPath} is not latest")
+ logger.info("valueRange = ${md.globalConfig.valueRange} validatorString = ${validatorString}")
+ mismatches.add("valueRange mismatch in ${mdPath}: markdown='${md.globalConfig.valueRange}', current='${validatorString}'")
+ }
+ }
+ return mismatches
+ }
void checkMD(String mdPath, GlobalConfig globalConfig) {
String result = ShellUtils.runAndReturn(
"grep '${PLACEHOLDER}' ${mdPath}").stdout.replaceAll("\n", "")
- if (!result.empty) {
- throw new CloudRuntimeException("Placeholders are detected in ${mdPath}, please replace them by content.")
- }
- GlobalConfigMarkDown markDown = getExistGlobalConfigMarkDown(mdPath)
- if (markDown.desc_CN.isEmpty()
- || markDown.name_CN.isEmpty()
- || markDown.valueRangeRemark.isEmpty()
- || markDown.defaultValueRemark.isEmpty()
- || markDown.resourcesGranularitiesRemark.isEmpty()
- || markDown.additionalRemark.isEmpty()
- || markDown.backgroundInformation.isEmpty()
- || markDown.isUIExposed.isEmpty()
- || markDown.isCLIExposed.isEmpty()
- ) {
- throw new CloudRuntimeException("The necessary information of ${mdPath} is missing, please complete the information before submission.")
- }
- if (!isConsistent(markDown, globalConfig)) {
- throw new CloudRuntimeException("${mdPath} is not match with its definition, please use Repair mode to correct it.")
- }
- }
+ if (!result.empty) {
+ throw new CloudRuntimeException("Placeholders detected in ${mdPath}; please replace them with actual content.")
+ }
+ GlobalConfigMarkDown markDown = getExistGlobalConfigMarkDown(mdPath)
+ List missingFields = []
+ if (markDown.desc_CN.isEmpty()) missingFields.add("desc_CN")
+ if (markDown.name_CN.isEmpty()) missingFields.add("name_CN")
+ if (markDown.valueRangeRemark.isEmpty()) missingFields.add("valueRangeRemark")
+ if (markDown.defaultValueRemark.isEmpty()) missingFields.add("defaultValueRemark")
+ if (markDown.resourcesGranularitiesRemark.isEmpty()) missingFields.add("resourcesGranularitiesRemark")
+ if (markDown.additionalRemark.isEmpty()) missingFields.add("additionalRemark")
+ if (markDown.backgroundInformation.isEmpty()) missingFields.add("backgroundInformation")
+ if (markDown.isUIExposed.isEmpty()) missingFields.add("isUIExposed")
+ if (markDown.isCLIExposed.isEmpty()) missingFields.add("isCLIExposed")
+ List inconsistencies = isConsistent(markDown, globalConfig)
+ if (!missingFields.isEmpty() || !inconsistencies.isEmpty()) {
+ StringBuilder sb = new StringBuilder("Validation failed for ${mdPath}:\n")
+ if (!missingFields.isEmpty()) {
+ sb.append("Missing required fields: ${missingFields}\n")
+ }
+ if (!inconsistencies.isEmpty()) {
+ sb.append("Inconsistent fields:\n")
+ inconsistencies.each { sb.append("- ${it}\n") }
+ }
+ throw new CloudRuntimeException(sb.toString())
+ }
+ }
class ElaborationMarkDown {
private def table = ["|编号|描述|原因|操作建议|更多|"]
@@ -2815,23 +2825,32 @@ ${additionalRemark}
return System.getProperty("ignoreError") != null
}
- void testGlobalConfigTemplateAndMarkDown() {
- Map allConfigs = initializer.configs
- allConfigs.each {
- String newPath =
- PathUtil.join(PathUtil.join(Paths.get("../doc").toAbsolutePath().normalize().toString(),
- "globalconfig"), it.value.category, it.value.name) + DEPRECATED + ".md"
- if (new File(newPath).exists()) {
+ void testGlobalConfigTemplateAndMarkDown() {
+ Map allConfigs = initializer.configs
+ List allErrors = []
+ allConfigs.each {
+ String newPath =
+ PathUtil.join(PathUtil.join(Paths.get("../doc").toAbsolutePath().normalize().toString(),
+ "globalconfig"), it.value.category, it.value.name) + DEPRECATED + ".md"
+ if (new File(newPath).exists()) {
return
}
- String mdPath =
- PathUtil.join(PathUtil.join(Paths.get("../doc").toAbsolutePath().normalize().toString(),
- "globalconfig"), it.value.category, it.value.name) + ".md"
- File mdFile = new File(mdPath)
- if (!mdFile.exists()) {
- throw new CloudRuntimeException("Not found the document markdown of the global config ${it.value.name} , please generate it first.")
- }
- checkMD(mdPath, it.value)
- }
- }
-}
+ String mdPath =
+ PathUtil.join(PathUtil.join(Paths.get("../doc").toAbsolutePath().normalize().toString(),
+ "globalconfig"), it.value.category, it.value.name) + ".md"
+ File mdFile = new File(mdPath)
+ if (!mdFile.exists()) {
+ allErrors.add("Global config markdown not found: ${mdPath}")
+ return
+ }
+ try {
+ checkMD(mdPath, it.value)
+ } catch (CloudRuntimeException e) {
+ allErrors.add(e.message)
+ }
+ }
+ if (!allErrors.isEmpty()) {
+ throw new CloudRuntimeException(allErrors.join("\n\n"))
+ }
+ }
+}
diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java
index 614350ecd69..671562c5368 100644
--- a/sdk/src/main/java/SourceClassMap.java
+++ b/sdk/src/main/java/SourceClassMap.java
@@ -608,6 +608,9 @@ 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.network.zns.ZnsControllerInventory", "org.zstack.sdk.ZnsControllerInventory");
+ put("org.zstack.network.zns.ZnsTransportZoneInventory", "org.zstack.sdk.ZnsTransportZoneInventory");
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");
@@ -1185,6 +1188,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");
@@ -1623,6 +1627,8 @@ public class SourceClassMap {
put("org.zstack.sdk.ZdfsInventory", "org.zstack.header.zdfs.ZdfsInventory");
put("org.zstack.sdk.ZdfsService", "org.zstack.ai.message.ModelCenterServiceInventory$ZdfsService");
put("org.zstack.sdk.ZdfsStorageInventory", "org.zstack.header.zdfs.ZdfsStorageInventory");
+ put("org.zstack.sdk.ZnsControllerInventory", "org.zstack.network.zns.ZnsControllerInventory");
+ put("org.zstack.sdk.ZnsTransportZoneInventory", "org.zstack.network.zns.ZnsTransportZoneInventory");
put("org.zstack.sdk.ZoneInventory", "org.zstack.header.zone.ZoneInventory");
put("org.zstack.sdk.databasebackup.DatabaseBackupInventory", "org.zstack.header.storage.database.backup.DatabaseBackupInventory");
put("org.zstack.sdk.databasebackup.DatabaseBackupStorageRefInventory", "org.zstack.header.storage.database.backup.DatabaseBackupStorageRefInventory");
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..e76ddab6ff8
--- /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, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode)
+ );
+ }
+
+ 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/sdk/src/main/java/org/zstack/sdk/NicTO.java b/sdk/src/main/java/org/zstack/sdk/NicTO.java
index 6eecb1466a8..c1457f95d4b 100644
--- a/sdk/src/main/java/org/zstack/sdk/NicTO.java
+++ b/sdk/src/main/java/org/zstack/sdk/NicTO.java
@@ -197,4 +197,20 @@ public java.lang.Boolean getIsolated() {
return this.isolated;
}
+ public java.lang.String bridgePortType;
+ public void setBridgePortType(java.lang.String bridgePortType) {
+ this.bridgePortType = bridgePortType;
+ }
+ public java.lang.String getBridgePortType() {
+ return this.bridgePortType;
+ }
+
+ public java.lang.String interfaceId;
+ public void setInterfaceId(java.lang.String interfaceId) {
+ this.interfaceId = interfaceId;
+ }
+ public java.lang.String getInterfaceId() {
+ return this.interfaceId;
+ }
+
}
diff --git a/sdk/src/main/java/org/zstack/sdk/ZnsControllerInventory.java b/sdk/src/main/java/org/zstack/sdk/ZnsControllerInventory.java
new file mode 100644
index 00000000000..2f207674572
--- /dev/null
+++ b/sdk/src/main/java/org/zstack/sdk/ZnsControllerInventory.java
@@ -0,0 +1,15 @@
+package org.zstack.sdk;
+
+
+
+public class ZnsControllerInventory extends org.zstack.sdk.SdnControllerInventory {
+
+ public java.util.List transportZones;
+ public void setTransportZones(java.util.List transportZones) {
+ this.transportZones = transportZones;
+ }
+ public java.util.List getTransportZones() {
+ return this.transportZones;
+ }
+
+}
diff --git a/sdk/src/main/java/org/zstack/sdk/ZnsTransportZoneInventory.java b/sdk/src/main/java/org/zstack/sdk/ZnsTransportZoneInventory.java
new file mode 100644
index 00000000000..d3026306804
--- /dev/null
+++ b/sdk/src/main/java/org/zstack/sdk/ZnsTransportZoneInventory.java
@@ -0,0 +1,95 @@
+package org.zstack.sdk;
+
+
+
+public class ZnsTransportZoneInventory {
+
+ public java.lang.String uuid;
+ public void setUuid(java.lang.String uuid) {
+ this.uuid = uuid;
+ }
+ public java.lang.String getUuid() {
+ return this.uuid;
+ }
+
+ public java.lang.String name;
+ public void setName(java.lang.String name) {
+ this.name = name;
+ }
+ public java.lang.String getName() {
+ return this.name;
+ }
+
+ public java.lang.String description;
+ public void setDescription(java.lang.String description) {
+ this.description = description;
+ }
+ public java.lang.String getDescription() {
+ return this.description;
+ }
+
+ public java.lang.String type;
+ public void setType(java.lang.String type) {
+ this.type = type;
+ }
+ public java.lang.String getType() {
+ return this.type;
+ }
+
+ public java.lang.String physicalNetwork;
+ public void setPhysicalNetwork(java.lang.String physicalNetwork) {
+ this.physicalNetwork = physicalNetwork;
+ }
+ public java.lang.String getPhysicalNetwork() {
+ return this.physicalNetwork;
+ }
+
+ public java.lang.String status;
+ public void setStatus(java.lang.String status) {
+ this.status = status;
+ }
+ public java.lang.String getStatus() {
+ return this.status;
+ }
+
+ public boolean isDefault;
+ public void setIsDefault(boolean isDefault) {
+ this.isDefault = isDefault;
+ }
+ public boolean getIsDefault() {
+ return this.isDefault;
+ }
+
+ public java.lang.String tags;
+ public void setTags(java.lang.String tags) {
+ this.tags = tags;
+ }
+ public java.lang.String getTags() {
+ return this.tags;
+ }
+
+ public java.lang.String znsSdnControllerUuid;
+ public void setZnsSdnControllerUuid(java.lang.String znsSdnControllerUuid) {
+ this.znsSdnControllerUuid = znsSdnControllerUuid;
+ }
+ public java.lang.String getZnsSdnControllerUuid() {
+ return this.znsSdnControllerUuid;
+ }
+
+ public java.sql.Timestamp createDate;
+ public void setCreateDate(java.sql.Timestamp createDate) {
+ this.createDate = createDate;
+ }
+ public java.sql.Timestamp getCreateDate() {
+ return this.createDate;
+ }
+
+ public java.sql.Timestamp lastOpDate;
+ public void setLastOpDate(java.sql.Timestamp lastOpDate) {
+ this.lastOpDate = lastOpDate;
+ }
+ public java.sql.Timestamp getLastOpDate() {
+ return this.lastOpDate;
+ }
+
+}
diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
index 8a8f2b91416..2c6715bc92c 100644
--- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
+++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
@@ -962,8 +962,8 @@ abstract class ApiHelper {
}
- def addBareMetal2Gateway(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2GatewayAction.class) Closure c) {
- def a = new org.zstack.sdk.AddBareMetal2GatewayAction()
+ def addBareMetal2DpuChassis(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2DpuChassisAction.class) Closure c) {
+ def a = new org.zstack.sdk.AddBareMetal2DpuChassisAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
c.resolveStrategy = Closure.OWNER_FIRST
c.delegate = a
@@ -989,8 +989,8 @@ abstract class ApiHelper {
}
- def addBareMetal2IpmiChassis(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2IpmiChassisAction.class) Closure c) {
- def a = new org.zstack.sdk.AddBareMetal2IpmiChassisAction()
+ def addBareMetal2Gateway(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2GatewayAction.class) Closure c) {
+ def a = new org.zstack.sdk.AddBareMetal2GatewayAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
c.resolveStrategy = Closure.OWNER_FIRST
c.delegate = a
@@ -1016,26 +1016,26 @@ abstract class ApiHelper {
}
- def addBareMetal2DpuChassis(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2DpuChassisAction.class) Closure c) {
- def a = new org.zstack.sdk.AddBareMetal2DpuChassisAction()
+ def addBareMetal2IpmiChassis(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2IpmiChassisAction.class) Closure c) {
+ def a = new org.zstack.sdk.AddBareMetal2IpmiChassisAction()
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())
@@ -10061,6 +10061,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..cb05661c4f5 100644
--- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy
+++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy
@@ -1,8 +1,10 @@
package org.zstack.testlib
+import org.springframework.http.HttpEntity
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.zstack.sdk.SdnControllerInventory
+import org.zstack.utils.gson.JSONObjectUtil
import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands
import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.LoginReply
import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.LoginRsp
@@ -15,7 +17,6 @@ import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.GetH3cTenantsRsp
import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.H3cTenantStruct
import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.GetH3cTeamLederIpReply
import org.zstack.sdnController.h3cVcfc.H3cVcfcV2Commands
-import org.springframework.http.HttpEntity
import org.zstack.sugonSdnController.controller.SugonSdnControllerConstant
import org.zstack.sugonSdnController.controller.api.ApiSerializer
import org.zstack.sugonSdnController.controller.api.TfCommands
@@ -58,9 +59,9 @@ class SdnControllerSpec extends Spec implements HasSession {
}
postCreate {
- inventory = querySdnController {
+ inventory = JSONObjectUtil.rehashObject(querySdnController {
conditions=["uuid=${inventory.uuid}".toString()]
- }[0]
+ }[0], SdnControllerInventory.class)
}
return id(name, inventory.uuid)
diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
index 0deb26d677d..20d2870ed5c 100644
--- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
+++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
@@ -1015,6 +1015,7 @@ public class CloudOperationsErrorCode {
public static final String ORG_ZSTACK_NETWORK_L2_10019 = "ORG_ZSTACK_NETWORK_L2_10019";
public static final String ORG_ZSTACK_NETWORK_L2_10020 = "ORG_ZSTACK_NETWORK_L2_10020";
+ public static final String ORG_ZSTACK_NETWORK_L2_10021 = "ORG_ZSTACK_NETWORK_L2_10021";
public static final String ORG_ZSTACK_CONSOLE_10000 = "ORG_ZSTACK_CONSOLE_10000";
@@ -11930,6 +11931,54 @@ public class CloudOperationsErrorCode {
public static final String ORG_ZSTACK_NETWORK_OVN_10084 = "ORG_ZSTACK_NETWORK_OVN_10084";
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10000 = "ORG_ZSTACK_NETWORK_ZNS_10000";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10001 = "ORG_ZSTACK_NETWORK_ZNS_10001";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10002 = "ORG_ZSTACK_NETWORK_ZNS_10002";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10003 = "ORG_ZSTACK_NETWORK_ZNS_10003";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10004 = "ORG_ZSTACK_NETWORK_ZNS_10004";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10005 = "ORG_ZSTACK_NETWORK_ZNS_10005";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10006 = "ORG_ZSTACK_NETWORK_ZNS_10006";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10007 = "ORG_ZSTACK_NETWORK_ZNS_10007";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10008 = "ORG_ZSTACK_NETWORK_ZNS_10008";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10009 = "ORG_ZSTACK_NETWORK_ZNS_10009";
+
+ // ZNS error-code semantic mapping:
+ // 10010 unsupported API for ZNS controller
+ // 10011 ZNS L2 only supports L3BasicNetwork
+ // 10012 duplicate ZNS L2NoVlan creation under same controller
+ // 10013 invalid ZNS L2 target type in change-vlan flow
+ // 10014 Geneve type can only change VNI, not L2 type
+ // 10015 cannot switch to Geneve / cannot move NIC across ZNS controllers
+ // 10016 duplicate Geneve VNI under same controller
+ // 10017 non-ZNS L2 cannot change to Geneve type
+ // 10018 ZNS non-Geneve L2 network cannot change type to L2GeneveNetwork
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10010 = "ORG_ZSTACK_NETWORK_ZNS_10010";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10011 = "ORG_ZSTACK_NETWORK_ZNS_10011";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10012 = "ORG_ZSTACK_NETWORK_ZNS_10012";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10013 = "ORG_ZSTACK_NETWORK_ZNS_10013";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10014 = "ORG_ZSTACK_NETWORK_ZNS_10014";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10015 = "ORG_ZSTACK_NETWORK_ZNS_10015";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10016 = "ORG_ZSTACK_NETWORK_ZNS_10016";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10017 = "ORG_ZSTACK_NETWORK_ZNS_10017";
+
+ public static final String ORG_ZSTACK_NETWORK_ZNS_10018 = "ORG_ZSTACK_NETWORK_ZNS_10018";
+
public static final String ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_MARKETPLACE_10000 = "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_MARKETPLACE_10000";
public static final String ORG_ZSTACK_ALIYUN_NAS_STORAGE_PRIMARY_IMAGESTORE_10000 = "ORG_ZSTACK_ALIYUN_NAS_STORAGE_PRIMARY_IMAGESTORE_10000";