From 16218919dc2fbee23a28f6121ede281f6e519573 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 13 Mar 2026 20:06:42 +0800 Subject: [PATCH 01/11] [sdnController]: integraion zns into cloud Resolves: ZCF-1365 Change-Id: I73687962636e7871626e687761626d6661716668 --- .../zstack/compute/vm/VmAllocateNicFlow.java | 278 +++++++--- .../zstack/compute/vm/VmDetachNicFlow.java | 61 ++- .../zstack/compute/vm/VmNicManagerImpl.java | 9 +- .../compute/vm/VmReturnReleaseNicFlow.java | 64 ++- conf/db/upgrade/V5.5.18__schema.sql | 31 ++ conf/springConfigXml/sdnController.xml | 2 + .../rest/webhook/WebhookCallbackClient.java | 172 ++++++ .../core/rest/webhook/WebhookProtocol.java | 55 ++ docs/modules/network/nav.adoc | 1 + .../networkResource/ZStackL2NetworkType.adoc | 504 ++++++++++++++++++ .../pages/networkResource/ZnsIntegration.adoc | 449 ++++++++++++++++ .../networkResource/networkResource.adoc | 3 +- .../header/network/l2/L2NetworkConstant.java | 6 + .../AfterSetL3NetworkMtuExtensionPoint.java | 7 + .../header/network/l3/L3NetworkInventory.java | 3 + .../header/network/l3/L3NetworkType.java | 9 + .../zstack/header/network/l3/L3NetworkVO.java | 3 + .../vm/AfterReleaseVmNicExtensionPoint.java | 12 + .../vm/BeforeAllocateVmNicExtensionPoint.java | 14 + .../header/vm/VmInstanceNicFactory.java | 1 + .../zstack/header/vm/VmOvsNicConstant.java | 9 - .../network/l3/L3NetworkManagerImpl.java | 35 ++ .../java/org/zstack/kvm/KVMHostFactory.java | 3 +- .../kvm/KVMRealizeL2NoVlanNetworkBackend.java | 2 +- .../kvm/KVMRealizeL2VlanNetworkBackend.java | 2 +- .../sdnController/SdnControllerFactory.java | 2 - .../SdnControllerManagerImpl.java | 82 ++- .../h3cVcfc/H3cVcfcSdnControllerFactory.java | 6 - .../controller/SugonSdnControllerFactory.java | 5 - sdk/src/main/java/SourceClassMap.java | 2 + .../sdk/CreateL2GeneveNetworkAction.java | 131 +++++ .../zstack/sdk/L2GeneveNetworkInventory.java | 15 + .../java/org/zstack/testlib/ApiHelper.groovy | 27 + .../zstack/testlib/SdnControllerSpec.groovy | 179 +++++++ 34 files changed, 2058 insertions(+), 126 deletions(-) create mode 100644 conf/db/upgrade/V5.5.18__schema.sql create mode 100644 core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java create mode 100644 core/src/main/java/org/zstack/core/rest/webhook/WebhookProtocol.java create mode 100644 docs/modules/network/pages/networkResource/ZStackL2NetworkType.adoc create mode 100644 docs/modules/network/pages/networkResource/ZnsIntegration.adoc create mode 100644 header/src/main/java/org/zstack/header/network/l3/AfterSetL3NetworkMtuExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/vm/BeforeAllocateVmNicExtensionPoint.java delete mode 100644 header/src/main/java/org/zstack/header/vm/VmOvsNicConstant.java create mode 100644 sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java index db5da3e3ce8..2e1634421a5 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java @@ -11,6 +11,7 @@ import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.SQLBatch; import org.zstack.core.errorcode.ErrorFacade; +import org.zstack.header.core.Completion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.core.workflow.Flow; import org.zstack.header.core.workflow.FlowRollback; @@ -97,14 +98,7 @@ public void run(final FlowTrigger trigger, final Map data) { int deviceId = deviceIdBitmap.nextClearBit(0); deviceIdBitmap.set(deviceId); MacOperator mo = new MacOperator(); - String customMac = mo.getMac(spec.getVmInventory().getUuid(), nw.getUuid()); - if (customMac != null){ - mo.deleteCustomMacSystemTag(spec.getVmInventory().getUuid(), nw.getUuid(), customMac); - customMac = customMac.toLowerCase(); - } else { - customMac = MacOperator.generateMacWithDeviceId((short) deviceId); - } - final String mac = customMac; + final String mac = allocateMac(mo, spec, nw, deviceId); CustomNicOperator nicOperator = new CustomNicOperator(spec.getVmInventory().getUuid(),nw.getUuid()); final String customNicUuid = nicOperator.getCustomNicId(); @@ -113,91 +107,42 @@ public void run(final FlowTrigger trigger, final Map data) { if (type == null) { errs.add(Platform.operr(ORG_ZSTACK_COMPUTE_VM_10068, "there is no available nicType on L3 network [%s]", nw.getUuid())); wcomp.allDone(); + return; } VmInstanceNicFactory vnicFactory = vmMgr.getVmInstanceNicFactory(type); - - VmNicInventory nic = new VmNicInventory(); - if (customNicUuid != null) { - nic.setUuid(customNicUuid); - } else { - nic.setUuid(Platform.getUuid()); - } - /* the first ip is ipv4 address for dual stack nic */ - nic.setVmInstanceUuid(spec.getVmInventory().getUuid()); - nic.setL3NetworkUuid(nw.getUuid()); - nic.setMac(mac); - nic.setHypervisorType(spec.getDestHost() == null ? - spec.getVmInventory().getHypervisorType() : spec.getDestHost().getHypervisorType()); + VmNicInventory nic = buildNicInventory(spec, nicSpec, nw, mac, customNicUuid, deviceId, disableL3Networks); if (mo.checkDuplicateMac(nic.getHypervisorType(), nic.getL3NetworkUuid(), nic.getMac())) { - trigger.fail(operr(ORG_ZSTACK_COMPUTE_VM_10069, "Duplicate mac address [%s]", nic.getMac())); + errs.add(operr(ORG_ZSTACK_COMPUTE_VM_10069, "Duplicate mac address [%s]", nic.getMac())); + wcomp.allDone(); return; } - if (!StringUtils.isEmpty(nicSpec.getNicDriverType())) { - nic.setDriverType(nicSpec.getNicDriverType()); - } else { - boolean vmImageHasVirtio = VmSystemTags.VIRTIO.hasTag(spec.getVmInventory().getUuid()); - nicManager.setNicDriverType(nic, vmImageHasVirtio, - ImagePlatform.valueOf(spec.getVmInventory().getPlatform()).isParaVirtualization(), - spec.getVmInventory()); - } + // Persist VmNicVO first so that ResourceVO entry exists before extensions + // (e.g. SDN controllers) attempt to create SystemTags referencing the NIC UUID. + VmNicVO nicVO = vnicFactory.createVmNic(nic, spec); - nic.setDeviceId(deviceId); - nic.setInternalName(VmNicVO.generateNicInternalName(spec.getVmInventory().getInternalId(), nic.getDeviceId())); - nic.setState(disableL3Networks.contains(nic.getL3NetworkUuid()) ? VmNicState.disable.toString() : VmNicState.enable.toString()); - new SQLBatch() { + callBeforeAllocateVmNicExtensions(nic, spec, new Completion(wcomp) { @Override - protected void scripts() { - VmNicVO nicVO = vnicFactory.createVmNic(nic, spec); - if (!nw.enableIpAddressAllocation() && nicNetworkInfoMap != null - && nicNetworkInfoMap.containsKey(nw.getUuid()) - && spec.getVmInventory().getType().equals(VmInstanceConstant.USER_VM_TYPE)) { - NicIpAddressInfo nicNicIpAddressInfo = nicNetworkInfoMap.get(nic.getL3NetworkUuid()); - if (!nicNicIpAddressInfo.ipv6Address.isEmpty()) { - UsedIpVO vo = new UsedIpVO(); - vo.setUuid(Platform.getUuid()); - vo.setIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(nicNicIpAddressInfo.ipv6Address)); - vo.setNetmask(IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(nicNicIpAddressInfo.ipv6Address+"/"+ nicNicIpAddressInfo.ipv6Prefix)); - vo.setGateway(nicNicIpAddressInfo.ipv6Gateway.isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(nicNicIpAddressInfo.ipv6Gateway)); - vo.setIpVersion(IPv6Constants.IPv6); - vo.setVmNicUuid(nic.getUuid()); - vo.setL3NetworkUuid(nic.getL3NetworkUuid()); - vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); - vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(nic.getL3NetworkUuid(), vo.getIp())); - nic.setUsedIpUuid(vo.getUuid()); - nicVO.setUsedIpUuid(vo.getUuid()); - nicVO.setIp(vo.getIp()); - nicVO.setNetmask(vo.getNetmask()); - nicVO.setGateway(vo.getGateway()); - dbf.persist(vo); + public void success() { + new SQLBatch() { + @Override + protected void scripts() { + persistStaticIpIfNeeded(nic, nicVO, nw, nicNetworkInfoMap, spec); + nics.add(nic); + VmNicVO updated = dbf.updateAndRefresh(nicVO); + addVmNicConfig(updated, spec, nicSpec); } - if (!nicNicIpAddressInfo.ipv4Address.isEmpty()) { - UsedIpVO vo = new UsedIpVO(); - vo.setUuid(Platform.getUuid()); - vo.setIp(nicNicIpAddressInfo.ipv4Address); - vo.setGateway(nicNicIpAddressInfo.ipv4Gateway); - vo.setNetmask(nicNicIpAddressInfo.ipv4Netmask); - vo.setIpVersion(IPv6Constants.IPv4); - vo.setVmNicUuid(nic.getUuid()); - vo.setL3NetworkUuid(nic.getL3NetworkUuid()); - vo.setIpInLong(NetworkUtils.ipv4StringToLong(vo.getIp())); - vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); - vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(nic.getL3NetworkUuid(), vo.getIp())); - nic.setUsedIpUuid(vo.getUuid()); - nicVO.setUsedIpUuid(vo.getUuid()); - nicVO.setIp(vo.getIp()); - nicVO.setNetmask(vo.getNetmask()); - nicVO.setGateway(vo.getGateway()); - dbf.persist(vo); - } - } - nics.add(nic); - nicVO = dbf.updateAndRefresh(nicVO); - addVmNicConfig(nicVO, spec, nicSpec); + }.execute(); + wcomp.done(); } - }.execute(); - wcomp.done(); + + @Override + public void fail(ErrorCode errorCode) { + errs.add(errorCode); + wcomp.allDone(); + } + }); }).run(new WhileDoneCompletion(trigger) { @Override @@ -211,6 +156,92 @@ public void done(ErrorCodeList errorCodeList) { }); } + private String allocateMac(MacOperator mo, VmInstanceSpec spec, L3NetworkInventory nw, int deviceId) { + String vmUuid = spec.getVmInventory().getUuid(); + String customMac = mo.getMac(vmUuid, nw.getUuid()); + if (customMac != null) { + mo.deleteCustomMacSystemTag(vmUuid, nw.getUuid(), customMac); + return customMac.toLowerCase(); + } + return MacOperator.generateMacWithDeviceId((short) deviceId); + } + + private VmNicInventory buildNicInventory(VmInstanceSpec spec, VmNicSpec nicSpec, + L3NetworkInventory nw, String mac, String customNicUuid, + int deviceId, List disableL3Networks) { + VmNicInventory nic = new VmNicInventory(); + nic.setUuid(customNicUuid != null ? customNicUuid : Platform.getUuid()); + /* the first ip is ipv4 address for dual stack nic */ + nic.setVmInstanceUuid(spec.getVmInventory().getUuid()); + nic.setL3NetworkUuid(nw.getUuid()); + nic.setMac(mac); + nic.setHypervisorType(spec.getDestHost() == null ? + spec.getVmInventory().getHypervisorType() : spec.getDestHost().getHypervisorType()); + + if (!StringUtils.isEmpty(nicSpec.getNicDriverType())) { + nic.setDriverType(nicSpec.getNicDriverType()); + } else { + boolean vmImageHasVirtio = VmSystemTags.VIRTIO.hasTag(spec.getVmInventory().getUuid()); + nicManager.setNicDriverType(nic, vmImageHasVirtio, + ImagePlatform.valueOf(spec.getVmInventory().getPlatform()).isParaVirtualization(), + spec.getVmInventory()); + } + + nic.setDeviceId(deviceId); + nic.setInternalName(VmNicVO.generateNicInternalName(spec.getVmInventory().getInternalId(), nic.getDeviceId())); + nic.setState(disableL3Networks.contains(nic.getL3NetworkUuid()) ? VmNicState.disable.toString() : VmNicState.enable.toString()); + return nic; + } + + private void persistStaticIpIfNeeded(VmNicInventory nic, VmNicVO nicVO, + L3NetworkInventory nw, Map nicNetworkInfoMap, + VmInstanceSpec spec) { + if (nw.enableIpAddressAllocation() || nicNetworkInfoMap == null + || !nicNetworkInfoMap.containsKey(nw.getUuid()) + || !spec.getVmInventory().getType().equals(VmInstanceConstant.USER_VM_TYPE)) { + return; + } + + NicIpAddressInfo nicIpAddressInfo = nicNetworkInfoMap.get(nic.getL3NetworkUuid()); + if (!nicIpAddressInfo.ipv6Address.isEmpty()) { + UsedIpVO vo = new UsedIpVO(); + vo.setUuid(Platform.getUuid()); + vo.setIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(nicIpAddressInfo.ipv6Address)); + vo.setNetmask(IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(nicIpAddressInfo.ipv6Address + "/" + nicIpAddressInfo.ipv6Prefix)); + vo.setGateway(nicIpAddressInfo.ipv6Gateway.isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(nicIpAddressInfo.ipv6Gateway)); + vo.setIpVersion(IPv6Constants.IPv6); + vo.setVmNicUuid(nic.getUuid()); + vo.setL3NetworkUuid(nic.getL3NetworkUuid()); + vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); + vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(nic.getL3NetworkUuid(), vo.getIp())); + nic.setUsedIpUuid(vo.getUuid()); + nicVO.setUsedIpUuid(vo.getUuid()); + nicVO.setIp(vo.getIp()); + nicVO.setNetmask(vo.getNetmask()); + nicVO.setGateway(vo.getGateway()); + dbf.persist(vo); + } + if (!nicIpAddressInfo.ipv4Address.isEmpty()) { + UsedIpVO vo = new UsedIpVO(); + vo.setUuid(Platform.getUuid()); + vo.setIp(nicIpAddressInfo.ipv4Address); + vo.setGateway(nicIpAddressInfo.ipv4Gateway); + vo.setNetmask(nicIpAddressInfo.ipv4Netmask); + vo.setIpVersion(IPv6Constants.IPv4); + vo.setVmNicUuid(nic.getUuid()); + vo.setL3NetworkUuid(nic.getL3NetworkUuid()); + vo.setIpInLong(NetworkUtils.ipv4StringToLong(vo.getIp())); + vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); + vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(nic.getL3NetworkUuid(), vo.getIp())); + nic.setUsedIpUuid(vo.getUuid()); + nicVO.setUsedIpUuid(vo.getUuid()); + nicVO.setIp(vo.getIp()); + nicVO.setNetmask(vo.getNetmask()); + nicVO.setGateway(vo.getGateway()); + dbf.persist(vo); + } + } + private void addVmNicConfig(VmNicVO vmNicVO, VmInstanceSpec vmSpec, VmNicSpec nicSpec) { if (nicSpec == null) { return; @@ -237,6 +268,73 @@ private void addVmNicConfig(VmNicVO vmNicVO, VmInstanceSpec vmSpec, VmNicSpec ni } } + private void callBeforeAllocateVmNicExtensions(VmNicInventory nic, VmInstanceSpec spec, Completion completion) { + List exts = pluginRgty.getExtensionList(BeforeAllocateVmNicExtensionPoint.class); + if (exts.isEmpty()) { + completion.success(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.beforeAllocateVmNic(nic, spec, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + wcomp.addError(errorCode); + wcomp.allDone(); + } + }); + }).run(new WhileDoneCompletion(completion) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (errorCodeList.getCauses().isEmpty()) { + completion.success(); + } else { + completion.fail(errorCodeList.getCauses().get(0)); + } + } + }); + } + + private void callAfterReleaseVmNicExtensions(List nics, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + if (exts.isEmpty() || nics.isEmpty()) { + completion.success(); + return; + } + + new While<>(nics).each((nic, wcomp) -> { + new While<>(exts).each((ext, wcomp2) -> { + ext.afterReleaseVmNic(nic, new Completion(wcomp2) { + @Override + public void success() { + wcomp2.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to call afterReleaseVmNic for nic[uuid:%s], %s", nic.getUuid(), errorCode)); + wcomp2.done(); + } + }); + }).run(new WhileDoneCompletion(wcomp) { + @Override + public void done(ErrorCodeList errorCodeList) { + wcomp.done(); + } + }); + }).run(new WhileDoneCompletion(completion) { + @Override + public void done(ErrorCodeList errorCodeList) { + completion.success(); + } + }); + } + @Override public void rollback(final FlowRollback chain, Map data) { final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); @@ -256,7 +354,19 @@ public void rollback(final FlowRollback chain, Map data) { vnicFactory.releaseVmNic(vmNic); } dbf.removeByPrimaryKeys(destNics.stream().map(VmNicInventory::getUuid).collect(Collectors.toList()), VmNicVO.class); - chain.rollback(); - return; + + callAfterReleaseVmNicExtensions(destNics, new Completion(chain) { + @Override + public void success() { + chain.rollback(); + } + + @Override + public void fail(ErrorCode errorCode) { + // best-effort: log and continue rollback even if SDN cleanup fails + logger.warn(String.format("afterReleaseVmNic extensions failed during rollback: %s", errorCode)); + chain.rollback(); + } + }); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java index a175e7d11e4..149fe4451b8 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java @@ -6,11 +6,14 @@ import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.UpdateQuery; +import org.zstack.header.core.Completion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.core.workflow.FlowTrigger; import org.zstack.header.core.workflow.NoRollbackFlow; +import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.ErrorCodeList; import org.zstack.header.message.MessageReply; import org.zstack.header.network.l3.L3NetworkConstant; @@ -20,6 +23,10 @@ import org.zstack.utils.CollectionUtils; import org.zstack.utils.function.Function; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.List; import java.util.Map; /** @@ -27,12 +34,17 @@ */ @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class VmDetachNicFlow extends NoRollbackFlow { + private static final CLogger logger = Utils.getLogger(VmDetachNicFlow.class); @Autowired private DatabaseFacade dbf; @Autowired private CloudBus bus; @Autowired private VmInstanceDeviceManager vidm; + @Autowired + private VmInstanceManager vmMgr; + @Autowired + private PluginRegistry pluginRgty; @Override public void run(FlowTrigger trigger, Map data) { @@ -68,6 +80,10 @@ public String call(VmNicInventory arg) { return; } + returnIpsAndDeleteNic(nic, trigger); + } + + private void returnIpsAndDeleteNic(VmNicInventory nic, FlowTrigger trigger) { new While<>(nic.getUsedIps()).all((ip, comp) -> { ReturnIpMsg msg = new ReturnIpMsg(); msg.setUsedIpUuid(ip.getUuid()); @@ -83,8 +99,51 @@ public void run(MessageReply reply) { @Override public void done(ErrorCodeList errorCodeList) { dbf.removeByPrimaryKey(nic.getUuid(), VmNicVO.class); - trigger.next(); + + callAfterReleaseVmNicExtensions(nic, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("afterReleaseVmNic extensions failed for nic[uuid:%s]: %s, continue", + nic.getUuid(), errorCode)); + trigger.next(); + } + }); } }); } + + private void callAfterReleaseVmNicExtensions(VmNicInventory nic, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + if (exts.isEmpty()) { + completion.success(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.afterReleaseVmNic(nic, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("afterReleaseVmNic extension failed for nic[uuid:%s]: %s, continue", + nic.getUuid(), errorCode)); + 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/VmNicManagerImpl.java b/compute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.java index 31b3e35d32a..4a2ddd2a8be 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.java @@ -295,10 +295,17 @@ public VmNicType getVmNicType(String vmUuid, L3NetworkInventory l3nw) { logger.debug(String.format("create %s on l3 network[uuid:%s] inside VmAllocateNicFlow", enableSriov ? "vf nic" : "vnic", l3nw.getUuid())); boolean enableVhostUser = NetworkServiceGlobalConfig.ENABLE_VHOSTUSER.value(Boolean.class); + + boolean enableDpdkVhostuser = Q.New(SystemTagVO.class) + .eq(SystemTagVO_.resourceType, VmInstanceVO.class.getSimpleName()) + .eq(SystemTagVO_.resourceUuid, vmUuid) + .eq(SystemTagVO_.tag, String.format("enableDpdkVhostuser::%s", l3nw.getUuid())) + .isExists(); + VmNicType.VmNicSubType subType = VmNicType.VmNicSubType.NONE; if (enableSriov) { subType = VmNicType.VmNicSubType.SRIOV; - } else if (enableVhostUser) { + } else if (enableVhostUser || enableDpdkVhostuser) { subType = VmNicType.VmNicSubType.VHOSTUSER; } L2NetworkVO l2nw = dbf.findByUuid(l3nw.getL2NetworkUuid(), L2NetworkVO.class); diff --git a/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java index 0fd0e2aa068..6e928b635cb 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java @@ -6,10 +6,13 @@ import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; +import org.zstack.header.core.Completion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.core.workflow.FlowTrigger; import org.zstack.header.core.workflow.NoRollbackFlow; +import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.ErrorCodeList; import org.zstack.header.message.MessageReply; import org.zstack.header.network.l3.L3NetworkConstant; @@ -34,6 +37,10 @@ public class VmReturnReleaseNicFlow extends NoRollbackFlow { protected CloudBus bus; @Autowired protected VmInstanceDeletionPolicyManager deletionPolicyMgr; + @Autowired + protected VmInstanceManager vmMgr; + @Autowired + protected PluginRegistry pluginRgty; @Override public void run(FlowTrigger chain, Map data) { @@ -43,6 +50,11 @@ public void run(FlowTrigger chain, Map data) { return; } + returnIpsAndReleaseNics(spec, data, chain); + } + + + private void returnIpsAndReleaseNics(VmInstanceSpec spec, Map data, FlowTrigger chain) { List msgs = new ArrayList<>(spec.getVmInventory().getVmNics().size()); for (VmNicInventory nic : spec.getVmInventory().getVmNics()) { for (UsedIpInventory ip : nic.getUsedIps()) { @@ -66,6 +78,7 @@ public void run(MessageReply reply) { })).run(new WhileDoneCompletion(chain) { @Override public void done(ErrorCodeList errorCodeList) { + List releasedNics = new ArrayList<>(); for (VmNicInventory nic : spec.getVmInventory().getVmNics()) { VmNicVO vo = dbf.findByUuid(nic.getUuid(), VmNicVO.class); if (VmInstanceConstant.USER_VM_TYPE.equals(spec.getVmInventory().getType())) { @@ -82,8 +95,57 @@ public void done(ErrorCodeList errorCodeList) { } else { dbf.remove(vo); } + releasedNics.add(nic); + } + + callAfterReleaseVmNicExtensions(releasedNics, new Completion(chain) { + @Override + public void success() { + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("afterReleaseVmNic extensions failed: %s, continue anyway", errorCode)); + chain.next(); + } + }); + } + }); + } + + private void callAfterReleaseVmNicExtensions(List nics, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + if (exts.isEmpty() || nics.isEmpty()) { + completion.success(); + return; + } + + new While<>(nics).each((nic, wcomp) -> { + new While<>(exts).each((ext, wcomp2) -> { + ext.afterReleaseVmNic(nic, new Completion(wcomp2) { + @Override + public void success() { + wcomp2.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("afterReleaseVmNic extension failed for nic[uuid:%s]: %s, continue", + nic.getUuid(), errorCode)); + wcomp2.done(); + } + }); + }).run(new WhileDoneCompletion(wcomp) { + @Override + public void done(ErrorCodeList errorCodeList) { + wcomp.done(); } - chain.next(); + }); + }).run(new WhileDoneCompletion(completion) { + @Override + public void done(ErrorCodeList errorCodeList) { + completion.success(); } }); } 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/sdnController.xml b/conf/springConfigXml/sdnController.xml index a9be9ec8795..8cf56843846 100644 --- a/conf/springConfigXml/sdnController.xml +++ b/conf/springConfigXml/sdnController.xml @@ -34,6 +34,8 @@ + + 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..2cacfef27b3 --- /dev/null +++ b/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java @@ -0,0 +1,172 @@ +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: + *
    + *
  1. Send an HTTP request to the external system
  2. + *
  3. External system returns immediately (e.g. 202 Accepted)
  4. + *
  5. External system later POSTs back the result to a callback URL
  6. + *
  7. This client matches the callback to the original request and completes it
  8. + *
+ * + *

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; + } + + /** + * @return the protocol adapter + */ + public WebhookProtocol getProtocol() { + return protocol; + } + + /** + * 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..da7bb2e7375 --- /dev/null +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -0,0 +1,449 @@ += 对接ZNS SDN控制器 + +== 总体描述 + +在考虑对接 ZNS SDN 控制器时,首先需要考虑如何做好 Cloud 资源对象和 ZNS 资源映射。 + +* ZNS segments → L2Network + L3Network + IpRange +* ZNS segments port → VmNic + UsedIp +* ZNS segments + transport zone → L2NetworkClusterRefVO + +=== CMS +Cloud L2 和 ZNS segment 之间的资源如何一一对应? +引入一个 CMS 数据结构。 + +[source,go] +---- + type Cms struct { + CmsUuid string + Type string ### cloud/zsv/zaku/zns + IP string ### cloud mn vip + Role string ###owner, user + CmsResourceUuid string ###owner, user +} + +type Segment { + ... + CmsMetaDatas []Cms `json:"cms"` +} +---- +* Cms::Type 定义 cms 类型:Cloud、ZSV、ZAKU、ZNS +* Cms::IP 定义 cms 的 IP 地址,该字段主要为了让维护人员更友好地识别资源,cms 的唯一标识仍是 CmsUuid +* Cms::CmsUuid 定义 cmsUuid。该值是 ZNS 上创建的 Computer Manager UUID,Cloud 在创建 SdnController 时会把这个 UUID 记录到 SdnController 的 systemTag 中 +* Cms::Role 定义 cms 是这个资源的 owner 还是 user。owner 表示该 cms 创建了资源,user 表示该 cms 使用了资源。一个资源可以在多个 cms 之间共享,比如一个 segment 可以被多个 Cloud 使用。资源删除有两种策略: +** 只有当所有 cms 都不使用该资源时,才可以删除(以 CmsMetaDatas 作为资源引用基数); +** 只要 owner 删除了该资源,就删除该资源。必须等待 user 删除后,owner 才能删除 +* Cms::CmsResourceUuid 定义该 cms 系统对应的资源 UUID,在数据同步过程中用于映射 Cms 资源和 ZNS 资源。 + +=== ZNS API + +ZNS API 定义需要包含 Cms 数据: + +* 创建 API:cms 根据自己的资源创建 cms 数据,ZNS 存入数据库 +* 删除 API:cms 根据资源 UUID 删除对应的 cms 数据,ZNS 根据结果执行删除操作 +* 查询 API:需要支持根据 cmsUuid 过滤资源 +* 修改操作者 API:ZNS 可以提供 API 添加/删除资源 user + +[NOTE] +不是所有 API 都需要以上全部操作。 + +=== ZNS 数据库对象 + +不是所有资源对象都需要保持 cms 信息,但是由 cms 创建的资源,或者需要按 cms 查询的资源,都需要保存 cms 信息。 +一个资源可能有多个 cms 信息,表示资源可在多个 cms 之间共享。 + + +== ZNS SDN控制器 + +ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`SugonSdnController`、`OvnController`、`HuaweiIMasterSdnController` 等实现。 + +新定义 `ZnsControllerVO`,继承 `SdnControllerVO`,不添加新的字段: + +* vendorType:ZNS +* vendorVersion:1.0 + +[NOTE] +必须先在 ZNS 完成添加 Computer Manager 的操作,然后在 Cloud 侧创建对应的 SdnController。 +ZNS SDN Controller 保持 SystemTags:`computerManagerUuid::xxxx`,这里的 xxxx 是 ZNS 创建的 Computer Manager UUID。 +后续 Cloud 调用 ZNS API 创建 segment、segment port 时,Cloud 会根据 computerManagerUuid 组装 cms 信息。 + +=== 创建SDN控制器 + +* 根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于当前 Computer Manager 管理的 cluster,且 `managementIp != null`: +** `HostData.managementIp`:匹配 Cloud `HostVO.managementIp`,找到对应 `HostVO.uuid` +** `HostData.clusterId`:即 Cloud `ClusterVO.uuid`(ZNS 侧直接使用 Cloud 的 cluster UUID,无需 name 匹配) +** `HostData.transportNodeProfileId`:用于后续推导 vSwitchType 和建立 transport zone → cluster 的反向映射 + +[NOTE] +ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` 相同。 + +* 根据 `HostData.transportNodeProfileId` 调用 `GET /zns/api/v1/fabric/transport-node-profiles/{uuid}` 获取 `TransportNodeProfileData`,取 `hostSwitchProfiles[0]` 调用 `GET /zns/api/v1/fabric/host-switch-profiles/{uuid}` 获取 `HostSwitchProfileData`: +** `HostSwitchProfileData.type`:枚举值 `dpdk` 或 `kernel`,用于推导 `SdnControllerHostRefVO.vSwitchType` +** `HostSwitchProfileData.transportZoneIds`:关联的 transport zone UUID 列表,建立反向缓存 `transportZoneUuid → Set` + +* 调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,并缓存到当前 `ZnsSdnControllerVO` 关联的数据中。建议新增 `ZnsTransportZone` 数据表,主要字段如下: ++ +[cols="2,3,2"] +|=== +|字段 |来源 |说明 + +|`isDefault` +|transport zone 返回字段 +|是否为默认 transport zone + +|`description` +|transport zone 返回字段 +|描述信息 + +|`name` +|transport zone 返回字段 +|transport zone 名称 + +|`physicalNetwork` +|transport zone 返回字段 +|物理网络标识 + +|`status` +|transport zone 返回字段 +|当前状态 + +|`tags` +|transport zone 返回字段 +|标签信息 + +|`type` +|transport zone 返回字段 +|类型,典型值为 `vlan` 或 `overlay` + +|`znsSdnControllerUuid` +|当前 `ZnsSdnControllerVO.uuid` +|外键,关联到所属 ZNS SDN Controller + +|=== + +[NOTE] +这里的缓存不只是 `transportZoneUuid → Set` 反向索引,还包括 transport zone 自身的元数据。 +前者用于根据 host/profile 关系反查 cluster,后者用于后续创建 Cloud L2Network 时选择默认 transport zone,并为 `segment.transport_zone_uuid` 的解释提供基础数据。 + +[NOTE] +OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel`)和 `switchType`(字符串描述,如 `"OVS"`)两个字段。 +推导 `vSwitchType` 使用的是 `type` 枚举字段。 + +* 根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则: ++ +[cols="2,2"] +|=== +|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType + +|`dpdk` +|`OvsDpdk` + +|`kernel` +|`OvsKernel` + +|其它未知值,或者 profile 查询失败 +|`ZNS` + +|=== +* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,同时持有两类 transport zone 缓存: +** transport zone 元数据缓存:保存到建议新增的 `ZnsTransportZone` +** `transportZoneUuid → Set` 反向缓存:用于 segment 和 cluster 的关联 +* 根据 computerManagerUuid 从 ZNS 获取 segments 列表。 +** 根据 segment 信息创建 L2Network、L3Network +** 根据 segment.ipam 信息创建 IpRange + +ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: + +[cols="1,2,2,2"] +|=== +|ZNS字段 |条件 |Cloud L2Network.type |Cloud L3Network.type + +|`transport_type` +|`overlay` +|`L2GeneveNetwork` +|`L3ZnsNetwork` + +|`transport_type` +|`vlan` 且 `virtual_network_id > 0` +|`L2VlanNetwork` +|`L3ZnsNetwork` + +|`transport_type` +|`vlan` 且 `virtual_network_id == 0` +|`L2NoVlanNetwork` +|`L3ZnsNetwork` + +|=== + +补充说明: + +* `L2Network.vSwitchType` 固定写入 `ZNS` +* `L2Network.virtualNetworkId` 取 `segment.virtual_network_id`(Geneve/VLAN) +* `L3Network.category` 当前初始化逻辑固定为 `Private` + +5. 根据 `segment.transport_zone_uuid` 查询前面缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 + +`L2NetworkClusterRefVO` 的映射关系如下: + +[cols="2,3,2"] +|=== +|字段 |来源 |说明 + +|`l2NetworkUuid` +|当前 segment 创建出的 `L2NetworkVO.uuid` +|当前 L2 网络 UUID + +|`clusterUuid` +|`HostData.clusterId`(经 transport zone 反向缓存查找) +|`segment.transport_zone_uuid` → 缓存 → `Set` + +|`l2ProviderType` +|常量 `ZNS` +|固定值 + +|=== + +=== 重连SDN控制器 + +重连(`APIReconnectSdnControllerMsg`)与创建的核心区别: + +* **创建**:ZNS 是数据源,Cloud 单向从 ZNS 读取资源,并在 Cloud 侧新建 L2Network / L3Network / IpRange / `SdnControllerHostRefVO` / `L2NetworkClusterRefVO` 等对象。 +* **重连**:Cloud 数据库是基准,*不新建* Cloud 资源;仅对 `SdnControllerHostRefVO` 做 upsert,并将 ZNS 侧的 segment / segment port 状态对齐到 Cloud 侧。 + +==== 1. 刷新 SdnControllerHostRefVO(upsert) + +重连仍需重新扫描 host,以处理新加入的主机或 vSwitchType 发生变化的主机。 +映射关系与创建时完全相同(`HostData.managementIp` → `HostVO`,`HostSwitchProfileData.type` → `vSwitchType`), +区别仅在于写库操作改为 upsert: + +[cols="3,2"] +|=== +|情况 |操作 + +|`SdnControllerHostRefVO` 不存在(新主机) +|INSERT 新记录 + +|已存在但 `vSwitchType` 或 `vtepIp` 发生变化 +|UPDATE + +|已存在且字段未变 +|跳过 + +|=== + +[NOTE] +创建时只做 INSERT;重连时使用 upsert,可处理主机上下线及 vSwitchType 变更的情况。 + +==== 2. Segment 协调(以 Cloud 为基准) + +重连以 Cloud 数据库中所有 `vSwitchType = ZNS` 的 `L2NetworkVO` 为基准,与 ZNS 侧属于本 Computer Manager 的 segment 做三路对比。 +ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关联。 + +[cols="2,2,3"] +|=== +|Cloud 侧 L2NetworkVO |ZNS 侧 segment |操作 + +|不存在(`l2Uuid` 指向已删除 L2,或 segment 无 `l2Uuid`) +|存在 +|调用 `DELETE /zns/api/v1/segments` 删除孤儿 segment + +|存在 +|不存在 +|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3 信息 + +|存在 +|存在但参数不一致(如 CIDR) +|调用 `PATCH /zns/api/v1/segments/{uuid}` 更新 + +|存在 +|存在且参数一致 +|无操作 + +|=== + +[NOTE] +重连 *不会* 根据 ZNS segment 在 Cloud 侧创建新的 L2Network / L3Network / IpRange / `L2NetworkClusterRefVO`。 +如果 ZNS 存在但 Cloud 不存在,视为孤儿 segment 并删除(与创建阶段的单向导入方向相反)。 + +==== 3. Segment Port 协调 + +完成 segment 协调后,对每个已与 Cloud L2 匹配的 segment,逐一协调其 port。 +Port 通过 cms 元数据中的 `ExternalIds.vmNicUuid` 与 Cloud `VmNicVO` 关联。 + +[cols="2,2,3"] +|=== +|Cloud 侧 VmNicVO |ZNS 侧 segment port |操作 + +|存在 +|不存在 +|调用 `POST /zns/api/v1/segments/{uuid}/ports` 补建 port + +|不存在 +|存在 +|调用 `DELETE /zns/api/v1/segments/{uuid}/ports` 删除孤儿 port + +|两侧均存在 +|(当前实现不做参数比对更新) +|无操作 + +|=== + +=== 删除SDN控制器 +删除 ZNS SDN Controller 时,会级联删除 ZNS 侧的 Computer Manager 和 Segment、Segment Port 等资源。Cloud 侧的 L2Network、L3Network、VmNic 等也会一起删除。 + +== L2Network + +=== 基础信息 + +`L2NetworkVO` 的重要字段: + +* `type`:L2NetworkType.types,有:L2NoVlanNetwork、L2VlanNetwork、VxlanNetworkPool、VxlanNetwork、TfL2Network、HardwareVxlanNetworkPool、HardwareVxlanNetwork +* `vSwitchType`:VSwitchType.types,有:LinuxBridge、TfL2Network、MacVlan、OvnDpdk、OvsDpdk、OvsKernel、ZNS +* `virtualNetworkId`:vlanId 或 vxlanID +* `physicalInterface`:物理网卡名称 + +ZNS L2Network 的类型定义: + +[cols="1,2"] +|=== +|属性 |值 + +|type +|L2NoVlanNetwork、L2VlanNetwork、L2GeneveNetwork(新增类型,类似于L2VlanNetwork) + +|vSwitchType +|ZNS(固定值,不区分 kernel 和 dpdk) + +|physicalInterface +|null + +|virtualNetworkId +|Vlan Id 或 Geneve Id + +|=== + +=== 创建 L2Network + +处理逻辑类似 OVN Controller,但调用 ZNS API 创建 segment。 + +创建 ZNS 二层网络时,Cloud 需要先根据 L2 类型从已缓存的 transport zone 中选择默认 transport zone: + +[cols="2,2,3"] +|=== +|Cloud L2 类型 |默认 transport zone.type |说明 + +|`L2VlanNetwork` +|`vlan` +|LAN 网络默认使用 vlan 类型的 transport zone + +|`L2NoVlanNetwork` +|`vlan` +|NoVlan 网络默认使用 vlan 类型的 transport zone + +|`L2GeneveNetwork` +|`overlay` +|Geneve 网络默认使用 overlay 类型的 transport zone + +|=== + +[NOTE] +ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。 +因此 Cloud 创建的 L2Network 在 OVN 侧默认可在该 transport zone 覆盖的全部物理机上使用,而不是由 OVN 提供 cluster 级隔离。 + +[NOTE] +Cloud 侧会额外施加一层调度约束:只有二层网络实际加载了某个 cluster 后,该 cluster 中的物理机才允许用于创建虚拟机。 +也就是说,transport zone 决定的是底层网络可达范围,`L2NetworkClusterRefVO` 决定的是 Cloud 侧可调度范围。 + +==== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg + +处理逻辑类似 OVN Controller,根据 ZNS Host 和 transport zone 的关系,把 ZNS segment 关联到 transport zone。 + +==== APIChangeL2NetworkVlanIdMsg + +* L2GeneveNetwork 类型不支持修改 VlanId,需要在 `L2NetworkApiInterceptor` 中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` +* L2VlanNetwork、L2NoVlanNetwork 类型支持 +* 仅需要修改 L2NetworkVO 数据库,不需要下发到物理机,需要调用修改 ZNS segment API + +== L3Network + +=== 基础信息 + +`L3NetworkVO` 的重要字段: + +* `type`:L3BasicNetwork、L3VpcNetwork +* `category`:Public、Private、System + +ZNS L3 的定义: + +[cols="1,2"] +|=== +|属性 |值/规则 + +|type +|L3ZnsNetwork + +|category(L2GeneveNetwork 类型) +|只能是 Private + +|category(L2NoVlanNetwork、L2VlanNetwork 类型) +|可以是 Public 或 Private + +|=== + +ZNS L3Network 不添加网络服务。 + +=== SetVmStaticIp / ChangeVmIp 操作 + +由于 ZNS 网络的 IP 由 ZNS 管理,`APISetVmStaticIpMsg` 和 `APIChangeVmIpMsg` 需要特殊处理: + +在 `VmInstanceApiInterceptor` 中增加校验:如果目标 L3Network 关联的 L2Network 的 vSwitchType 为 ZNS,需要将用户指定的 IP 传给 ZNS segment port API 进行更新,而非走 Cloud 侧的 IP 分配流程。 + +== VmNic + +VmNicType 的值有:VNIC、VF、`dpdkvhostuserclient`。ZNS 可能是 dpdk 模式,也可能是 kernel 模式。在 UI 选择 ZNS 网络后,用户可以选择网卡类型:VNIC 或 `dpdkvhostuserclient`。 + +=== 虚拟机的物理机分配 + +创建虚拟机选择了 ZNS 网络时: + +* 默认网卡类型是 VNIC,需要选择到部署了 OvnKernel 的物理机 +* 如果选择了 `dpdkvhostuserclient`,需要选择到部署了 OvnDpdk 的物理机 + +=== 网卡创建过程 + +创建虚拟机或给虚拟机添加网卡时,会调用 `VmAllocateNicFlow` 创建网卡。 + +ZNS 网络创建过程: + +1. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType +2. 调用 ZNS 创建 segment port API,返回 ip/掩码/网关、ip6/前缀/网关 +3. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 +4. 根据获取的参数创建 `VmNicVO`、`UsedIpVO` + +=== 网卡删除过程 + +* `VmReturnReleaseNicFlow`:在 `destroyVmWorkFlowElements` 中被调用,用于虚拟机销毁时释放网卡资源 +* `VmDetachNicFlow`:在云主机删除网卡时调用 + +两个 Flow 中都需要: + +1. 调用 ZNS 删除 segment port API +2. 删除 `VmNicVO`、`UsedIpVO` + +=== DPDK 网卡的特殊处理 + +由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 +这个逻辑与 OVN DPDK 虚拟网卡一致。 + +=== ChangeVmNicNetwork(换网操作) + +`APIChangeVmNicNetworkMsg` 涉及 detach 旧网络 + attach 新网络: + +* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络 +* 从 ZNS 网络变换成 ZNS 网络的场景,需要调用 ZNS API 删除旧的 segment port,调用 API 创建新的 segment port,并更新 `VmNicVO`/`UsedIpVO` 等相关数据对象 + +=== FilterAttachableL3NetworkExtensionPoint + +* `APIGetVmAttachableL3NetworkMsg` 必须能获取到 ZNS L3 网络。 + + diff --git a/docs/modules/network/pages/networkResource/networkResource.adoc b/docs/modules/network/pages/networkResource/networkResource.adoc index 9aa66ce7341..b0fbf015282 100644 --- a/docs/modules/network/pages/networkResource/networkResource.adoc +++ b/docs/modules/network/pages/networkResource/networkResource.adoc @@ -2,4 +2,5 @@ * xref:networkResource/L2Network.adoc[] * xref:networkResource/L3Network.adoc[] -* xref:networkResource/VpcRouter.adoc[] \ No newline at end of file +* xref:networkResource/VpcRouter.adoc[] +* xref:networkResource/ZnsIntegration.adoc[] diff --git a/header/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.java b/header/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.java index e72b0b91396..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/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/l3/L3NetworkManagerImpl.java b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java index 384a5d2c1df..e3429ee4320 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(l3Vo); + new While<>(exts).each((ext, wcompl) -> { + ext.afterSetL3NetworkMtu(l3Inv, msg.getMtu(), new Completion(wcompl) { + @Override + public void success() { + wcompl.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + wcompl.addError(errorCode); + wcompl.allDone(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (errorCodeList.getCauses().isEmpty()) { + trigger.next(); + } else { + trigger.fail(errorCodeList.getCauses().get(0)); + } + } + }); + } }).done(new FlowDoneHandler(msg) { @Override public void handle(Map data) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/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/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..eab8fc1488b 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java @@ -50,7 +50,8 @@ public class SdnControllerManagerImpl extends AbstractService implements SdnCont L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, InstantiateResourceOnAttachingNicExtensionPoint, PreVmInstantiateResourceExtensionPoint, VmReleaseResourceExtensionPoint, ReleaseNetworkServiceOnDetachingNicExtensionPoint, SecurityGroupGetSdnBackendExtensionPoint, - AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint { + AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint, + BeforeAllocateVmNicExtensionPoint, AfterReleaseVmNicExtensionPoint { private static final CLogger logger = Utils.getLogger(SdnControllerManagerImpl.class); private static final Logger log = LoggerFactory.getLogger(SdnControllerManagerImpl.class); @@ -486,8 +487,7 @@ public void releaseVmResource(VmInstanceSpec spec, Completion completion) { continue; } - VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType()); - if (vSwitchType.getSdnControllerType() == null) { + if (shouldSkipSdnForNic(l2VO)) { continue; } @@ -512,8 +512,7 @@ public void releaseVmResource(VmInstanceSpec spec, Completion 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) { + if (shouldSkipSdnForNic(l2NetworkVO)) { completion.success(); return; } @@ -535,8 +534,7 @@ public void instantiateResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInve @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) { + if (shouldSkipSdnForNic(l2NetworkVO)) { completion.done(); return; } @@ -573,8 +571,7 @@ public void fail(ErrorCode errorCode) { 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) { + if (shouldSkipSdnForNic(l2NetworkVO)) { completion.done(); return; } @@ -638,8 +635,7 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) continue; } - VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType()); - if (vSwitchType.getSdnControllerType() ==null) { + if (shouldSkipSdnForNic(l2VO)) { continue; } @@ -694,8 +690,7 @@ public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { continue; } - VSwitchType vSwitchType = VSwitchType.valueOf(l2VO.getvSwitchType()); - if (vSwitchType.getSdnControllerType() ==null) { + if (shouldSkipSdnForNic(l2VO)) { continue; } @@ -717,6 +712,15 @@ public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { 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 public SdnControllerFactory getSdnControllerFactory(String type) { SdnControllerFactory factory = sdnControllerFactories.get(type); @@ -899,4 +903,56 @@ private SdnControllerVO getSdnControllerVO(L3NetworkInventory l3Network) { } return dbf.findByUuid(sdnControllerUuid, SdnControllerVO.class); } + + @Override + public void beforeAllocateVmNic(VmNicInventory nic, VmInstanceSpec spec, Completion completion) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + completion.success(); + return; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + completion.success(); + return; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + completion.success(); + return; + } + + Map> nicMaps = new HashMap<>(); + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + sdnAddVmNics(nicMaps, completion); + } + + @Override + public void afterReleaseVmNic(VmNicInventory nic, Completion completion) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + completion.success(); + return; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + completion.success(); + return; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + completion.success(); + return; + } + + Map> nicMaps = new HashMap<>(); + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + 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/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 614350ecd69..5c3b7f59fd6 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -608,6 +608,7 @@ public class SourceClassMap { put("org.zstack.network.service.virtualrouter.VirtualRouterOfferingInventory", "org.zstack.sdk.VirtualRouterOfferingInventory"); put("org.zstack.network.service.virtualrouter.VirtualRouterSoftwareVersionInventory", "org.zstack.sdk.VirtualRouterSoftwareVersionInventory"); put("org.zstack.network.service.virtualrouter.VirtualRouterVmInventory", "org.zstack.sdk.VirtualRouterVmInventory"); + put("org.zstack.network.zns.L2GeneveNetworkInventory", "org.zstack.sdk.L2GeneveNetworkInventory"); put("org.zstack.observabilityServer.ObservabilityServerOfferingInventory", "org.zstack.sdk.ObservabilityServerOfferingInventory"); put("org.zstack.observabilityServer.ObservabilityServerVmInventory", "org.zstack.sdk.ObservabilityServerVmInventory"); put("org.zstack.observabilityServer.service.ObservabilityServerServiceDataInventory", "org.zstack.sdk.ObservabilityServerServiceDataInventory"); @@ -1185,6 +1186,7 @@ public class SourceClassMap { put("org.zstack.sdk.KvmCephIsoTO", "org.zstack.storage.ceph.primary.KvmCephIsoTO"); put("org.zstack.sdk.KvmHostHypervisorMetadataInventory", "org.zstack.kvm.hypervisor.datatype.KvmHostHypervisorMetadataInventory"); put("org.zstack.sdk.KvmHypervisorInfoInventory", "org.zstack.kvm.hypervisor.datatype.KvmHypervisorInfoInventory"); + put("org.zstack.sdk.L2GeneveNetworkInventory", "org.zstack.network.zns.L2GeneveNetworkInventory"); put("org.zstack.sdk.L2NetworkData", "org.zstack.header.network.l2.L2NetworkData"); put("org.zstack.sdk.L2NetworkInventory", "org.zstack.header.network.l2.L2NetworkInventory"); put("org.zstack.sdk.L2PortGroupNetworkInventory", "org.zstack.network.l2.virtualSwitch.header.L2PortGroupNetworkInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java b/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java new file mode 100644 index 00000000000..2d6cceeb9d9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java @@ -0,0 +1,131 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CreateL2GeneveNetworkAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.CreateL2NetworkResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, numberRange = {1L,16777214L}, noTrim = false) + public java.lang.Integer geneveId; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String zoneUuid; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String physicalInterface; + + @Param(required = false) + public java.lang.String type; + + @Param(required = false, validValues = {"LinuxBridge","OvsDpdk","MacVlan","OvnDpdk"}, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vSwitchType = "LinuxBridge"; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean isolated = false; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String pvlan; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.CreateL2NetworkResult value = res.getResult(org.zstack.sdk.CreateL2NetworkResult.class); + ret.value = value == null ? new org.zstack.sdk.CreateL2NetworkResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/l2-networks/geneve"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java b/sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java new file mode 100644 index 00000000000..bbf314e095c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.java @@ -0,0 +1,15 @@ +package org.zstack.sdk; + + + +public class L2GeneveNetworkInventory extends org.zstack.sdk.L2NetworkInventory { + + public java.lang.Integer geneveId; + public void setGeneveId(java.lang.Integer geneveId) { + this.geneveId = geneveId; + } + public java.lang.Integer getGeneveId() { + return this.geneveId; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 8a8f2b91416..e5745afc9a7 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -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..bb64a6f3f54 100644 --- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy @@ -1,6 +1,9 @@ package org.zstack.testlib +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.zstack.sdk.SdnControllerInventory import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands @@ -25,6 +28,10 @@ import org.zstack.sugonSdnController.controller.api.types.Project import org.zstack.sugonSdnController.controller.api.types.VirtualMachine import org.zstack.sugonSdnController.controller.api.types.VirtualMachineInterface import org.zstack.sugonSdnController.controller.api.types.VirtualNetwork +import org.zstack.utils.gson.JSONObjectUtil +import org.zstack.network.zns.ZnsSdnControllerConstant + +import javax.servlet.http.HttpServletRequest /** * Created by shixin.ruan on 2019/09/26. @@ -289,6 +296,178 @@ class SdnControllerSpec extends Spec implements HasSession { ResponseEntity response = new ResponseEntity(json, HttpStatus.OK); return response.getBody() } + + // ===================== ZNS Simulators ===================== + + // Helper: trigger ZNS async callback in a separate thread + def triggerZnsCallback = { HttpEntity entity, EnvSpec spec, Object data -> + String jobUuid = entity.headers.getFirst(ZnsSdnControllerConstant.ZNS_HEADER_JOB_UUID) + String webhook = entity.headers.getFirst(ZnsSdnControllerConstant.ZNS_HEADER_WEBHOOK) + List missingHeaders = [] + if (!jobUuid) { + missingHeaders.add(ZnsSdnControllerConstant.ZNS_HEADER_JOB_UUID) + } + if (!webhook) { + missingHeaders.add(ZnsSdnControllerConstant.ZNS_HEADER_WEBHOOK) + } + if (!missingHeaders.isEmpty()) { + throw new IllegalStateException("Missing required ZNS callback header(s): ${missingHeaders.join(', ')}") + } + + Thread.start { + Thread.sleep(100) + def cmd = [taskUuid: jobUuid, success: true, status: ZnsSdnControllerConstant.ZNS_CALLBACK_STATUS_COMPLETED, data: data] + def headers = new HttpHeaders() + headers.setContentType(MediaType.APPLICATION_JSON) + def body = JSONObjectUtil.toJsonString(cmd) + headers.set("commandpath", "/zns/callback") + spec.restTemplate.exchange(webhook, HttpMethod.POST, + new HttpEntity(body, headers), String.class) + } + } + + // GET/DELETE /zns/api/v1/fabric/compute-managers/{uuid} + simulator("/zns/api/v1/fabric/compute-managers/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + if (req.method == "DELETE") { + triggerZnsCallback(entity, spec, [:]) + return [:] + } + return [success: true, data: [uuid: "cm-uuid-1", name: "cm-1", connectionStatus: "connected"]] + } + + // GET /zns/api/v1/fabric/compute-collections + simulator("/zns/api/v1/fabric/compute-collections") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def c1 = spec.inventoryByName("cluster-1") + def c2 = spec.inventoryByName("cluster-2") + return [success: true, data: [ + [uuid: c1?.uuid ?: "cc-1", name: "cluster-1", computeManagerId: "cm-uuid-1"], + [uuid: c2?.uuid ?: "cc-2", name: "cluster-2", computeManagerId: "cm-uuid-1"] + ], total: 2] + } + + // GET /zns/api/v1/fabric/discovered-nodes + simulator("/zns/api/v1/fabric/discovered-nodes") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def c1 = spec.inventoryByName("cluster-1") + return [success: true, data: [ + [uuid: "dn-1", name: "kvm-1", managementIp: "127.0.0.1", clusterId: c1?.uuid ?: "cc-1", transportNodeProfileId: "tnp-dpdk"], + [uuid: "dn-2", name: "kvm-2", managementIp: "127.0.0.2", clusterId: c1?.uuid ?: "cc-1", transportNodeProfileId: "tnp-dpdk"] + ], total: 2] + } + + // GET /zns/api/v1/fabric/transport-zones + simulator("/zns/api/v1/fabric/transport-zones") { + return [success: true, data: [ + [uuid: "tz-1", name: "tz-overlay", type: "overlay"] + ], total: 1] + } + + // GET /zns/api/v1/fabric/transport-node-profiles/{uuid} + simulator("/zns/api/v1/fabric/transport-node-profiles/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def profileUuid = uri.tokenize("/").last() + if (profileUuid == "tnp-kernel") { + return [success: true, data: [uuid: "tnp-kernel", name: "tnp-kernel", hostSwitchProfiles: ["hsp-kernel"]]] + } + return [success: true, data: [uuid: "tnp-dpdk", name: "tnp-dpdk", hostSwitchProfiles: ["hsp-dpdk"]]] + } + + // GET /zns/api/v1/fabric/host-switch-profiles/{uuid} + simulator("/zns/api/v1/fabric/host-switch-profiles/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def hspUuid = uri.tokenize("/").last() + if (hspUuid == "hsp-kernel") { + return [success: true, data: [uuid: "hsp-kernel", name: "hsp-kernel", type: "kernel", switchType: "OVS", transportZoneIds: ["tz-1"]]] + } + return [success: true, data: [uuid: "hsp-dpdk", name: "hsp-dpdk", type: "dpdk", switchType: "OVS", transportZoneIds: ["tz-1"]]] + } + + // /zns/api/v1/segments — GET(list) / POST(create) / DELETE(batch) + simulator("/zns/api/v1/segments") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + if (req.method == "GET") { + return [success: true, data: [ + [uuid: "zns-sg-1", name: "sg-1", transport_type: "overlay", virtual_network_id: 100, + transport_zone_uuid: "tz-1", + cms: [cmsUuid: "cm-uuid-1", ExternalIds: [:]], + ipams: [[ip_cidr: "10.0.1.0/24", gateway_ip: "10.0.1.1", prefix: 24, + ip_ranges: [[StartIp: "10.0.1.10", EndIp: "10.0.1.100"]]]]], + [uuid: "zns-sg-2", name: "sg-2", transport_type: "overlay", virtual_network_id: 200, + transport_zone_uuid: "tz-1", + cms: [cmsUuid: "cm-uuid-1", ExternalIds: [:]], + ipams: [[ip_cidr: "10.0.2.0/24", gateway_ip: "10.0.2.1", prefix: 24, + ip_ranges: [[StartIp: "10.0.2.10", EndIp: "10.0.2.100"]]]]] + ], total: 2] + } + // POST(create) or DELETE(batch) → async callback + def data = [uuid: "zns-sg-new", name: "new-seg", transport_type: "overlay"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid} — GET(single) / PATCH(update) + simulator("/zns/api/v1/segments/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def segUuid = uri.tokenize("/").last() + if (req.method == "GET") { + return [success: true, data: [uuid: segUuid, name: "seg-" + segUuid, transport_type: "overlay"]] + } + // PATCH → async callback + def data = [uuid: segUuid, name: "seg-" + segUuid, transport_type: "overlay"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid}/ports — GET(list) / POST(create) / DELETE(batch) + def portIpCounter = new java.util.concurrent.atomic.AtomicInteger(50) + simulator("/zns/api/v1/segments/[^/]+/ports") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + if (req.method == "GET") { + return [success: true, data: [], total: 0] + } + // POST(create) or DELETE(batch) → async callback + def body = entity.body ? JSONObjectUtil.toMap(entity.body) : [:] + def uri = req.getRequestURI() + def segParts = uri.split("/segments/") + def segmentUuid = segParts.length > 1 ? segParts[1].split("/ports")[0] : "unknown" + + def data + if (req.method == "POST") { + def allocatedIp = body?.ip ?: ("10.0.1." + portIpCounter.getAndIncrement()) + data = [ + uuid: "zns-port-" + UUID.randomUUID().toString().substring(0, 8), + segment_uuid: segmentUuid, + mac: body?.mac ?: "00:00:00:00:00:01", + ip: allocatedIp, + vm_uuid: body?.vm_uuid + ] + } else { + data = [:] + } + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid}/ports/{portUuid} — GET / PATCH + simulator("/zns/api/v1/segments/[^/]+/ports/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + def uri = req.getRequestURI() + def portUuid = uri.tokenize("/").last() + if (req.method == "GET") { + return [success: true, data: [uuid: portUuid, segment_uuid: "zns-sg-1"]] + } + // PATCH → async callback + def data = [uuid: portUuid, segment_uuid: "zns-sg-1"] + triggerZnsCallback(entity, spec, data) + return [:] + } + + // /zns/api/v1/segments/{uuid}/used-ips — GET + simulator("/zns/api/v1/segments/[^/]+/used-ips") { + return [success: true, data: [], total: 0] + } + + // POST /zns/api/v1/fabric/compute-managers/{uuid}/sync-jobs + simulator("/zns/api/v1/fabric/compute-managers/[^/]+/sync-jobs") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> + triggerZnsCallback(entity, spec, [:]) + return [:] + } } } From b49fe64f081a051bb9b405fdd8f71500622f2d0f Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 25 Mar 2026 11:38:25 +0800 Subject: [PATCH 02/11] [network]: ussu Resolves: ZCF-1365 Change-Id: I7262787a6474667a766d77766165796f73717775 --- .../main/java/org/zstack/network/l3/L3NetworkManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e3429ee4320..77b7ca81495 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java @@ -194,7 +194,7 @@ public void run(FlowTrigger trigger, Map data) { return; } - L3NetworkInventory l3Inv = L3NetworkInventory.valueOf(l3Vo); + 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 From e56d57492bd5c30c6e80f41c27c3ed69b8ae48a5 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 25 Mar 2026 12:30:32 +0800 Subject: [PATCH 03/11] [testlib]: ususu Resolves: ZCF-1365 Change-Id: I647469616679716d7366686c77617073746f776c --- .../zstack/testlib/SdnControllerSpec.groovy | 181 +----------------- 1 file changed, 1 insertion(+), 180 deletions(-) diff --git a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy index bb64a6f3f54..8a5f488c11d 100644 --- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy @@ -1,9 +1,7 @@ package org.zstack.testlib -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod +import org.springframework.http.HttpEntity import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.zstack.sdk.SdnControllerInventory import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands @@ -18,7 +16,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 @@ -28,10 +25,6 @@ import org.zstack.sugonSdnController.controller.api.types.Project import org.zstack.sugonSdnController.controller.api.types.VirtualMachine import org.zstack.sugonSdnController.controller.api.types.VirtualMachineInterface import org.zstack.sugonSdnController.controller.api.types.VirtualNetwork -import org.zstack.utils.gson.JSONObjectUtil -import org.zstack.network.zns.ZnsSdnControllerConstant - -import javax.servlet.http.HttpServletRequest /** * Created by shixin.ruan on 2019/09/26. @@ -296,178 +289,6 @@ class SdnControllerSpec extends Spec implements HasSession { ResponseEntity response = new ResponseEntity(json, HttpStatus.OK); return response.getBody() } - - // ===================== ZNS Simulators ===================== - - // Helper: trigger ZNS async callback in a separate thread - def triggerZnsCallback = { HttpEntity entity, EnvSpec spec, Object data -> - String jobUuid = entity.headers.getFirst(ZnsSdnControllerConstant.ZNS_HEADER_JOB_UUID) - String webhook = entity.headers.getFirst(ZnsSdnControllerConstant.ZNS_HEADER_WEBHOOK) - List missingHeaders = [] - if (!jobUuid) { - missingHeaders.add(ZnsSdnControllerConstant.ZNS_HEADER_JOB_UUID) - } - if (!webhook) { - missingHeaders.add(ZnsSdnControllerConstant.ZNS_HEADER_WEBHOOK) - } - if (!missingHeaders.isEmpty()) { - throw new IllegalStateException("Missing required ZNS callback header(s): ${missingHeaders.join(', ')}") - } - - Thread.start { - Thread.sleep(100) - def cmd = [taskUuid: jobUuid, success: true, status: ZnsSdnControllerConstant.ZNS_CALLBACK_STATUS_COMPLETED, data: data] - def headers = new HttpHeaders() - headers.setContentType(MediaType.APPLICATION_JSON) - def body = JSONObjectUtil.toJsonString(cmd) - headers.set("commandpath", "/zns/callback") - spec.restTemplate.exchange(webhook, HttpMethod.POST, - new HttpEntity(body, headers), String.class) - } - } - - // GET/DELETE /zns/api/v1/fabric/compute-managers/{uuid} - simulator("/zns/api/v1/fabric/compute-managers/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - if (req.method == "DELETE") { - triggerZnsCallback(entity, spec, [:]) - return [:] - } - return [success: true, data: [uuid: "cm-uuid-1", name: "cm-1", connectionStatus: "connected"]] - } - - // GET /zns/api/v1/fabric/compute-collections - simulator("/zns/api/v1/fabric/compute-collections") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - def c1 = spec.inventoryByName("cluster-1") - def c2 = spec.inventoryByName("cluster-2") - return [success: true, data: [ - [uuid: c1?.uuid ?: "cc-1", name: "cluster-1", computeManagerId: "cm-uuid-1"], - [uuid: c2?.uuid ?: "cc-2", name: "cluster-2", computeManagerId: "cm-uuid-1"] - ], total: 2] - } - - // GET /zns/api/v1/fabric/discovered-nodes - simulator("/zns/api/v1/fabric/discovered-nodes") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - def c1 = spec.inventoryByName("cluster-1") - return [success: true, data: [ - [uuid: "dn-1", name: "kvm-1", managementIp: "127.0.0.1", clusterId: c1?.uuid ?: "cc-1", transportNodeProfileId: "tnp-dpdk"], - [uuid: "dn-2", name: "kvm-2", managementIp: "127.0.0.2", clusterId: c1?.uuid ?: "cc-1", transportNodeProfileId: "tnp-dpdk"] - ], total: 2] - } - - // GET /zns/api/v1/fabric/transport-zones - simulator("/zns/api/v1/fabric/transport-zones") { - return [success: true, data: [ - [uuid: "tz-1", name: "tz-overlay", type: "overlay"] - ], total: 1] - } - - // GET /zns/api/v1/fabric/transport-node-profiles/{uuid} - simulator("/zns/api/v1/fabric/transport-node-profiles/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - def uri = req.getRequestURI() - def profileUuid = uri.tokenize("/").last() - if (profileUuid == "tnp-kernel") { - return [success: true, data: [uuid: "tnp-kernel", name: "tnp-kernel", hostSwitchProfiles: ["hsp-kernel"]]] - } - return [success: true, data: [uuid: "tnp-dpdk", name: "tnp-dpdk", hostSwitchProfiles: ["hsp-dpdk"]]] - } - - // GET /zns/api/v1/fabric/host-switch-profiles/{uuid} - simulator("/zns/api/v1/fabric/host-switch-profiles/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - def uri = req.getRequestURI() - def hspUuid = uri.tokenize("/").last() - if (hspUuid == "hsp-kernel") { - return [success: true, data: [uuid: "hsp-kernel", name: "hsp-kernel", type: "kernel", switchType: "OVS", transportZoneIds: ["tz-1"]]] - } - return [success: true, data: [uuid: "hsp-dpdk", name: "hsp-dpdk", type: "dpdk", switchType: "OVS", transportZoneIds: ["tz-1"]]] - } - - // /zns/api/v1/segments — GET(list) / POST(create) / DELETE(batch) - simulator("/zns/api/v1/segments") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - if (req.method == "GET") { - return [success: true, data: [ - [uuid: "zns-sg-1", name: "sg-1", transport_type: "overlay", virtual_network_id: 100, - transport_zone_uuid: "tz-1", - cms: [cmsUuid: "cm-uuid-1", ExternalIds: [:]], - ipams: [[ip_cidr: "10.0.1.0/24", gateway_ip: "10.0.1.1", prefix: 24, - ip_ranges: [[StartIp: "10.0.1.10", EndIp: "10.0.1.100"]]]]], - [uuid: "zns-sg-2", name: "sg-2", transport_type: "overlay", virtual_network_id: 200, - transport_zone_uuid: "tz-1", - cms: [cmsUuid: "cm-uuid-1", ExternalIds: [:]], - ipams: [[ip_cidr: "10.0.2.0/24", gateway_ip: "10.0.2.1", prefix: 24, - ip_ranges: [[StartIp: "10.0.2.10", EndIp: "10.0.2.100"]]]]] - ], total: 2] - } - // POST(create) or DELETE(batch) → async callback - def data = [uuid: "zns-sg-new", name: "new-seg", transport_type: "overlay"] - triggerZnsCallback(entity, spec, data) - return [:] - } - - // /zns/api/v1/segments/{uuid} — GET(single) / PATCH(update) - simulator("/zns/api/v1/segments/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - def uri = req.getRequestURI() - def segUuid = uri.tokenize("/").last() - if (req.method == "GET") { - return [success: true, data: [uuid: segUuid, name: "seg-" + segUuid, transport_type: "overlay"]] - } - // PATCH → async callback - def data = [uuid: segUuid, name: "seg-" + segUuid, transport_type: "overlay"] - triggerZnsCallback(entity, spec, data) - return [:] - } - - // /zns/api/v1/segments/{uuid}/ports — GET(list) / POST(create) / DELETE(batch) - def portIpCounter = new java.util.concurrent.atomic.AtomicInteger(50) - simulator("/zns/api/v1/segments/[^/]+/ports") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - if (req.method == "GET") { - return [success: true, data: [], total: 0] - } - // POST(create) or DELETE(batch) → async callback - def body = entity.body ? JSONObjectUtil.toMap(entity.body) : [:] - def uri = req.getRequestURI() - def segParts = uri.split("/segments/") - def segmentUuid = segParts.length > 1 ? segParts[1].split("/ports")[0] : "unknown" - - def data - if (req.method == "POST") { - def allocatedIp = body?.ip ?: ("10.0.1." + portIpCounter.getAndIncrement()) - data = [ - uuid: "zns-port-" + UUID.randomUUID().toString().substring(0, 8), - segment_uuid: segmentUuid, - mac: body?.mac ?: "00:00:00:00:00:01", - ip: allocatedIp, - vm_uuid: body?.vm_uuid - ] - } else { - data = [:] - } - triggerZnsCallback(entity, spec, data) - return [:] - } - - // /zns/api/v1/segments/{uuid}/ports/{portUuid} — GET / PATCH - simulator("/zns/api/v1/segments/[^/]+/ports/[^/]+") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - def uri = req.getRequestURI() - def portUuid = uri.tokenize("/").last() - if (req.method == "GET") { - return [success: true, data: [uuid: portUuid, segment_uuid: "zns-sg-1"]] - } - // PATCH → async callback - def data = [uuid: portUuid, segment_uuid: "zns-sg-1"] - triggerZnsCallback(entity, spec, data) - return [:] - } - - // /zns/api/v1/segments/{uuid}/used-ips — GET - simulator("/zns/api/v1/segments/[^/]+/used-ips") { - return [success: true, data: [], total: 0] - } - - // POST /zns/api/v1/fabric/compute-managers/{uuid}/sync-jobs - simulator("/zns/api/v1/fabric/compute-managers/[^/]+/sync-jobs") { HttpServletRequest req, HttpEntity entity, EnvSpec spec -> - triggerZnsCallback(entity, spec, [:]) - return [:] - } } } From 1416f6126390cb70b1dc685a92c86bd402251109 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 25 Mar 2026 13:12:07 +0800 Subject: [PATCH 04/11] [utils]: ususu Resolves: ZCF-1365 Change-Id: I7a61747778757574656967626c6a736366716b6a --- .../CloudOperationsErrorCode.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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..abbebd66529 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -11930,6 +11930,28 @@ 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"; + + public static final String ORG_ZSTACK_NETWORK_ZNS_10010 = "ORG_ZSTACK_NETWORK_ZNS_10010"; + 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"; From 42584b773622d85e179fc3afed8a34affc647cec Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 26 Mar 2026 11:22:49 +0800 Subject: [PATCH 05/11] [sdnController]: fix code review commemt Resolves: ZCF-1365 Change-Id: I65767164636167726d6369726f63666b68666477 --- build/pom.xml | 5 +++++ .../org/zstack/sdnController/SdnControllerManagerImpl.java | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build/pom.xml b/build/pom.xml index a1ea6a47ca7..5a097bb382a 100755 --- a/build/pom.xml +++ b/build/pom.xml @@ -636,6 +636,11 @@ ovn ${project.version} + + org.zstack + zns + ${project.version} + org.zstack observabilityServer 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 eab8fc1488b..7a1967e2447 100644 --- a/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java +++ b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java @@ -921,7 +921,7 @@ public void beforeAllocateVmNic(VmNicInventory nic, VmInstanceSpec spec, Complet String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); if (controllerUuid == null) { - completion.success(); + completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); return; } @@ -947,7 +947,7 @@ public void afterReleaseVmNic(VmNicInventory nic, Completion completion) { String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); if (controllerUuid == null) { - completion.success(); + completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); return; } From 4b30043362671e8e4d96a376510cce8ded476f90 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 26 Mar 2026 20:52:33 +0800 Subject: [PATCH 06/11] [rest]: fix bug Resolves: ZCF-1365 Change-Id: I696f6a6a6e6d7867746e70736d6d646861686166 --- .../zstack/compute/vm/VmAllocateNicFlow.java | 49 +- .../compute/vm/VmAllocateSdnNicFlow.java | 122 ++++ .../zstack/compute/vm/VmDetachNicFlow.java | 13 +- .../org/zstack/compute/vm/VmInstanceBase.java | 1 + .../compute/vm/VmReturnReleaseNicFlow.java | 32 +- .../org/zstack/compute/vm/VmSystemTags.java | 5 + conf/springConfigXml/VmInstanceManager.xml | 1 + conf/springConfigXml/sdnController.xml | 7 +- .../rest/webhook/WebhookCallbackClient.java | 17 + .../pages/networkResource/ZnsIntegration.adoc | 614 +++++++++++++++--- .../vm/AfterAllocateSdnNicExtensionPoint.java | 49 ++ .../AfterAllocateVmNicIpExtensionPoint.java | 19 + .../java/org/zstack/kvm/KVMAgentCommands.java | 24 + .../src/main/java/org/zstack/kvm/KVMHost.java | 10 + .../sdnController/SdnControllerBase.java | 2 +- .../SdnControllerManagerImpl.java | 376 +++-------- sdk/src/main/java/SourceClassMap.java | 4 + .../sdk/CreateL2GeneveNetworkAction.java | 2 +- sdk/src/main/java/org/zstack/sdk/NicTO.java | 16 + .../zstack/sdk/ZnsControllerInventory.java | 15 + .../zstack/sdk/ZnsTransportZoneInventory.java | 95 +++ .../java/org/zstack/testlib/ApiHelper.groovy | 18 +- .../zstack/testlib/SdnControllerSpec.groovy | 5 +- 23 files changed, 1040 insertions(+), 456 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java create mode 100644 header/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ZnsControllerInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ZnsTransportZoneInventory.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java index 2e1634421a5..b572bf0ea1e 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java @@ -300,41 +300,6 @@ public void done(ErrorCodeList errorCodeList) { }); } - private void callAfterReleaseVmNicExtensions(List nics, Completion completion) { - List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); - if (exts.isEmpty() || nics.isEmpty()) { - completion.success(); - return; - } - - new While<>(nics).each((nic, wcomp) -> { - new While<>(exts).each((ext, wcomp2) -> { - ext.afterReleaseVmNic(nic, new Completion(wcomp2) { - @Override - public void success() { - wcomp2.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.warn(String.format("failed to call afterReleaseVmNic for nic[uuid:%s], %s", nic.getUuid(), errorCode)); - wcomp2.done(); - } - }); - }).run(new WhileDoneCompletion(wcomp) { - @Override - public void done(ErrorCodeList errorCodeList) { - wcomp.done(); - } - }); - }).run(new WhileDoneCompletion(completion) { - @Override - public void done(ErrorCodeList errorCodeList) { - completion.success(); - } - }); - } - @Override public void rollback(final FlowRollback chain, Map data) { final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); @@ -355,18 +320,6 @@ public void rollback(final FlowRollback chain, Map data) { } dbf.removeByPrimaryKeys(destNics.stream().map(VmNicInventory::getUuid).collect(Collectors.toList()), VmNicVO.class); - callAfterReleaseVmNicExtensions(destNics, new Completion(chain) { - @Override - public void success() { - chain.rollback(); - } - - @Override - public void fail(ErrorCode errorCode) { - // best-effort: log and continue rollback even if SDN cleanup fails - logger.warn(String.format("afterReleaseVmNic extensions failed during rollback: %s", errorCode)); - chain.rollback(); - } - }); + chain.rollback(); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java new file mode 100644 index 00000000000..1279f91245a --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.java @@ -0,0 +1,122 @@ +package org.zstack.compute.vm; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.core.asyncbatch.While; +import org.zstack.core.componentloader.PluginRegistry; +import org.zstack.header.core.Completion; +import org.zstack.header.core.WhileDoneCompletion; +import org.zstack.header.core.workflow.Flow; +import org.zstack.header.core.workflow.FlowRollback; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.vm.*; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.List; +import java.util.Map; + +import static org.zstack.core.progress.ProgressReportService.taskProgress; + +/** + * Placed after VmAllocateNicIpFlow in the flow chain. + * + * For each NIC belonging to an SDN-managed L2 network, this flow delegates + * to the registered {@link AfterAllocateSdnNicExtensionPoint} implementations + * (typically {@code SdnControllerManagerImpl}) which: + * + * - OVN: calls addLogicalPorts() with already-allocated IPs + * - ZNS: calls createSegmentPort(), receives IP back from ZNS, writes to DB + * - H3C/Sugon: default no-op + * + * Non-SDN NICs are skipped by the extension implementation internally. + */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VmAllocateSdnNicFlow implements Flow { + private static final CLogger logger = Utils.getLogger(VmAllocateSdnNicFlow.class); + + @Autowired + private PluginRegistry pluginRgty; + + @Override + public void run(final FlowTrigger trigger, final Map data) { + taskProgress("create SDN ports for nics"); + + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + final List nics = spec.getDestNics(); + + if (nics == null || nics.isEmpty()) { + trigger.next(); + return; + } + + List exts = + pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); + if (exts.isEmpty()) { + trigger.next(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.afterAllocateSdnNic(spec, nics, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + wcomp.addError(errorCode); + wcomp.allDone(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (!errorCodeList.getCauses().isEmpty()) { + trigger.fail(errorCodeList.getCauses().get(0)); + } else { + trigger.next(); + } + } + }); + } + + @Override + public void rollback(final FlowRollback chain, Map data) { + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + final List nics = spec.getDestNics(); + + List exts = + pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); + + if (exts.isEmpty() || nics == null || nics.isEmpty()) { + chain.rollback(); + return; + } + + new While<>(exts).each((ext, wcomp) -> { + ext.rollbackSdnNic(spec, nics, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + // best-effort: log and continue + logger.warn(String.format("failed to rollback SDN nic: %s", errorCode)); + wcomp.done(); + } + }); + }).run(new WhileDoneCompletion(chain) { + @Override + public void done(ErrorCodeList errorCodeList) { + chain.rollback(); + } + }); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java index 149fe4451b8..d59339bf3bd 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java @@ -100,7 +100,7 @@ public void run(MessageReply reply) { public void done(ErrorCodeList errorCodeList) { dbf.removeByPrimaryKey(nic.getUuid(), VmNicVO.class); - callAfterReleaseVmNicExtensions(nic, new Completion(trigger) { + callReleaseSdnNics(java.util.Collections.singletonList(nic), new Completion(trigger) { @Override public void success() { trigger.next(); @@ -108,7 +108,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extensions failed for nic[uuid:%s]: %s, continue", + logger.warn(String.format("releaseSdnNics failed for nic[uuid:%s]: %s, continue", nic.getUuid(), errorCode)); trigger.next(); } @@ -117,15 +117,15 @@ public void fail(ErrorCode errorCode) { }); } - private void callAfterReleaseVmNicExtensions(VmNicInventory nic, Completion completion) { - List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + private void callReleaseSdnNics(List nics, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); if (exts.isEmpty()) { completion.success(); return; } new While<>(exts).each((ext, wcomp) -> { - ext.afterReleaseVmNic(nic, new Completion(wcomp) { + ext.releaseSdnNics(nics, new Completion(wcomp) { @Override public void success() { wcomp.done(); @@ -133,8 +133,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extension failed for nic[uuid:%s]: %s, continue", - nic.getUuid(), errorCode)); + logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode)); wcomp.done(); } }); diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java index 73f3e98b0df..4e904d6dbf4 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -2287,6 +2287,7 @@ void rollback() { flowChain.then(new VmAllocateNicFlow()); flowChain.then(new VmAllocateNicIpFlow()); + flowChain.then(new VmAllocateSdnNicFlow()); flowChain.then(new VmSetDefaultL3NetworkOnAttachingFlow()); setAdditionalFlow(flowChain, spec); if (self.getState() == VmInstanceState.Running) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java index 6e928b635cb..2aa8a459944 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java @@ -98,7 +98,7 @@ public void done(ErrorCodeList errorCodeList) { releasedNics.add(nic); } - callAfterReleaseVmNicExtensions(releasedNics, new Completion(chain) { + callReleaseSdnNics(releasedNics, new Completion(chain) { @Override public void success() { chain.next(); @@ -106,7 +106,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extensions failed: %s, continue anyway", errorCode)); + logger.warn(String.format("releaseSdnNics failed: %s, continue anyway", errorCode)); chain.next(); } }); @@ -114,31 +114,23 @@ public void fail(ErrorCode errorCode) { }); } - private void callAfterReleaseVmNicExtensions(List nics, Completion completion) { - List exts = pluginRgty.getExtensionList(AfterReleaseVmNicExtensionPoint.class); + private void callReleaseSdnNics(List nics, Completion completion) { + List exts = pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); if (exts.isEmpty() || nics.isEmpty()) { completion.success(); return; } - new While<>(nics).each((nic, wcomp) -> { - new While<>(exts).each((ext, wcomp2) -> { - ext.afterReleaseVmNic(nic, new Completion(wcomp2) { - @Override - public void success() { - wcomp2.done(); - } + new While<>(exts).each((ext, wcomp) -> { + ext.releaseSdnNics(nics, new Completion(wcomp) { + @Override + public void success() { + wcomp.done(); + } - @Override - public void fail(ErrorCode errorCode) { - logger.warn(String.format("afterReleaseVmNic extension failed for nic[uuid:%s]: %s, continue", - nic.getUuid(), errorCode)); - wcomp2.done(); - } - }); - }).run(new WhileDoneCompletion(wcomp) { @Override - public void done(ErrorCodeList errorCodeList) { + public void fail(ErrorCode errorCode) { + logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode)); wcomp.done(); } }); diff --git a/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/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 8cf56843846..e76be084ac6 100644 --- a/conf/springConfigXml/sdnController.xml +++ b/conf/springConfigXml/sdnController.xml @@ -26,16 +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 index 2cacfef27b3..f719bb8445e 100644 --- a/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java +++ b/core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java @@ -131,6 +131,15 @@ public String getCallbackUrl() { return callbackUrl; } + /** + * Override the callback URL. Use this when the callback is handled by a + * dedicated HTTP endpoint (e.g. a Spring Controller) rather than the + * sendCommand channel. + */ + public void setCallbackUrl(String callbackUrl) { + this.callbackUrl = callbackUrl; + } + /** * @return the protocol adapter */ @@ -138,6 +147,14 @@ public WebhookProtocol getProtocol() { return protocol; } + /** + * Deliver a callback that was received outside the sendCommand channel + * (e.g. from a dedicated Spring Controller endpoint for external systems). + */ + public void deliverCallback(T cmd) { + onCallback(cmd); + } + /** * Callback handler invoked by the RESTFacade sendCommand channel. */ diff --git a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc index da7bb2e7375..b536d694109 100644 --- a/docs/modules/network/pages/networkResource/ZnsIntegration.adoc +++ b/docs/modules/network/pages/networkResource/ZnsIntegration.adoc @@ -15,7 +15,7 @@ Cloud L2 和 ZNS segment 之间的资源如何一一对应? [source,go] ---- type Cms struct { - CmsUuid string + CmsUuid string Type string ### cloud/zsv/zaku/zns IP string ### cloud mn vip Role string ###owner, user @@ -52,52 +52,166 @@ ZNS API 定义需要包含 Cms 数据: 不是所有资源对象都需要保持 cms 信息,但是由 cms 创建的资源,或者需要按 cms 查询的资源,都需要保存 cms 信息。 一个资源可能有多个 cms 信息,表示资源可在多个 cms 之间共享。 +=== UUID 格式约定 + +ZNS 使用带连字符的 UUID 格式(`550e8400-e29b-41d4-a716-446655440000`),Cloud 使用不带连字符的紧凑 UUID 格式(`550e8400e29b41d4a716446655440000`)。 + +Cloud 在调用 ZNS API 时需要将 Cloud UUID 转换为 ZNS UUID 格式,在接收 ZNS 响应时需要将 ZNS UUID 转换回 Cloud 格式。 + +== Cloud 与 ZNS 通信协议 + +=== 传输层 + +Cloud 通过 HTTP REST API 与 ZNS 通信。所有请求/响应体均为 JSON 格式。 + +全局配置参数: + +[cols="2,2,2"] +|=== +|配置项 |默认值 |说明 + +|`zns.controller.scheme` +|`http` +|HTTP 或 HTTPS + +|`zns.controller.port` +|`7278` +|ZNS API 端口 + +|`zns.controller.timeout` +|`300000`(5分钟) +|请求超时时间(毫秒) + +|=== + +=== 异步回调模式 + +所有写操作(POST/PATCH/DELETE)采用异步回调模式: + +.... +Cloud ZNS + │ POST/PATCH/DELETE + headers │ + │ x-web-hook: │ + │ x-job-uuid: │ + │─────────────────────────────────────────►│ + │ ◄── 202 Accepted │ (ZNS 立即返回) + │ │ + │ (ZNS 异步处理完成后回调 Cloud) │ + │ ◄── POST │ + │ { taskUuid, success, status, data } │ + │ │ + │ Cloud 根据 taskUuid 匹配到 │ + │ 对应的等待中的调用并完成 │ +.... + +所有读操作(GET)为同步调用,直接返回 200 + JSON body。 + +=== 请求串行化 + +对同一个 ZNS IP 的所有 API 调用通过任务链串行执行,避免并发操作导致的冲突。 == ZNS SDN控制器 ZStack 已经定义 `SdnControllerVO`,目前已有 `H3cVcfcSdnController`、`SugonSdnController`、`OvnController`、`HuaweiIMasterSdnController` 等实现。 -新定义 `ZnsControllerVO`,继承 `SdnControllerVO`,不添加新的字段: +新定义 `ZnsControllerVO`,继承 `SdnControllerVO`: * vendorType:ZNS -* vendorVersion:1.0 +* vendorVersion:用于记录当前连接的 ZNS 版本(详见 <> 章节) +* transportZones:关联的 `ZnsTransportZoneVO` 列表(一对多) [NOTE] 必须先在 ZNS 完成添加 Computer Manager 的操作,然后在 Cloud 侧创建对应的 SdnController。 ZNS SDN Controller 保持 SystemTags:`computerManagerUuid::xxxx`,这里的 xxxx 是 ZNS 创建的 Computer Manager UUID。 后续 Cloud 调用 ZNS API 创建 segment、segment port 时,Cloud 会根据 computerManagerUuid 组装 cms 信息。 +Cloud 还通过 SystemTag 记录以下映射关系: + +* `znsSegmentUuid::{segmentUuid}` 在 `L2NetworkVO` 上 — 映射 Cloud L2 → ZNS segment +* `znsSegmentPortUuid::{portUuid}` 在 `VmNicVO` 上 — 映射 Cloud NIC → ZNS segment port + === 创建SDN控制器 -* 根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于当前 Computer Manager 管理的 cluster,且 `managementIp != null`: +==== 1. 验证 Computer Manager + +根据 systemTag 中提供的 `computerManagerUuid`,调用 `GET /zns/api/v1/fabric/compute-managers/{uuid}` 验证 Computer Manager 在 ZNS 上存在且可用。 + +==== 2. 同步 Compute Collections(集群映射) + +根据 `GET /zns/api/v1/fabric/compute-collections` 获取 compute collection 列表,过滤出属于当前 Computer Manager 的条目。 + +ZNS 侧直接使用 Cloud 的 cluster UUID,因此可以直接按 UUID 匹配 Cloud 侧的 `ClusterVO`,无需 name 匹配。 + +构建以下映射关系: +* `znsClusterUuids`:属于该 Computer Manager 的 ZNS cluster UUID 集合 +* `znsClusterToZstackCluster`:ZNS cluster UUID → Cloud cluster UUID + +==== 3. 获取 Discovered Nodes(主机发现) + +根据 `GET /zns/api/v1/fabric/discovered-nodes` 获取 discovered node 列表(`HostData`),过滤条件:`clusterId` 属于步骤 2 中确定的 cluster 集合,且 `managementIp != null`: + ** `HostData.managementIp`:匹配 Cloud `HostVO.managementIp`,找到对应 `HostVO.uuid` -** `HostData.clusterId`:即 Cloud `ClusterVO.uuid`(ZNS 侧直接使用 Cloud 的 cluster UUID,无需 name 匹配) +** `HostData.clusterId`:即 Cloud `ClusterVO.uuid` ** `HostData.transportNodeProfileId`:用于后续推导 vSwitchType 和建立 transport zone → cluster 的反向映射 [NOTE] ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` 相同。 -* 根据 `HostData.transportNodeProfileId` 调用 `GET /zns/api/v1/fabric/transport-node-profiles/{uuid}` 获取 `TransportNodeProfileData`,取 `hostSwitchProfiles[0]` 调用 `GET /zns/api/v1/fabric/host-switch-profiles/{uuid}` 获取 `HostSwitchProfileData`: +==== 4. 推导 vSwitchType 并创建 Host Ref + +根据 `HostData.transportNodeProfileId` 调用 `GET /zns/api/v1/fabric/transport-node-profiles/{uuid}` 获取 `TransportNodeProfileData`,取 `hostSwitchProfiles[0]` 调用 `GET /zns/api/v1/fabric/host-switch-profiles/{uuid}` 获取 `HostSwitchProfileData`: + ** `HostSwitchProfileData.type`:枚举值 `dpdk` 或 `kernel`,用于推导 `SdnControllerHostRefVO.vSwitchType` ** `HostSwitchProfileData.transportZoneIds`:关联的 transport zone UUID 列表,建立反向缓存 `transportZoneUuid → Set` -* 调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,并缓存到当前 `ZnsSdnControllerVO` 关联的数据中。建议新增 `ZnsTransportZone` 数据表,主要字段如下: -+ +[NOTE] +OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel`)和 `switchType`(字符串描述,如 `"OVS"`)两个字段。 +推导 `vSwitchType` 使用的是 `type` 枚举字段。 + +根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则: + +[cols="2,2"] +|=== +|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType + +|`dpdk` +|`OvsDpdk` + +|`kernel` +|`OvsKernel` + +|其它未知值,或者 profile 查询失败 +|`ZNS` + +|=== + +==== 5. 同步 Transport Zones + +调用 `GET /zns/api/v1/fabric/transport-zones` 获取 transport zone 列表,持久化到 `ZnsTransportZoneVO`。主要字段如下: + [cols="2,3,2"] |=== |字段 |来源 |说明 +|`uuid` +|transport zone UUID(转换为 Cloud 格式) +|主键 + |`isDefault` -|transport zone 返回字段 +|每种 type 的第一个 transport zone 设为 true |是否为默认 transport zone +|`name` +|transport zone 返回字段 +|transport zone 名称 + |`description` |transport zone 返回字段 |描述信息 -|`name` +|`type` |transport zone 返回字段 -|transport zone 名称 +|类型,典型值为 `vlan` 或 `overlay` |`physicalNetwork` |transport zone 返回字段 @@ -111,10 +225,6 @@ ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` |transport zone 返回字段 |标签信息 -|`type` -|transport zone 返回字段 -|类型,典型值为 `vlan` 或 `overlay` - |`znsSdnControllerUuid` |当前 `ZnsSdnControllerVO.uuid` |外键,关联到所属 ZNS SDN Controller @@ -122,35 +232,17 @@ ZNS 侧需保证同一个 cluster 内所有 node 的 `transportNodeProfileId` |=== [NOTE] -这里的缓存不只是 `transportZoneUuid → Set` 反向索引,还包括 transport zone 自身的元数据。 -前者用于根据 host/profile 关系反查 cluster,后者用于后续创建 Cloud L2Network 时选择默认 transport zone,并为 `segment.transport_zone_uuid` 的解释提供基础数据。 - -[NOTE] -OpenAPI 中 `HostSwitchProfileData` 同时有 `type`(枚举:`dpdk | kernel`)和 `switchType`(字符串描述,如 `"OVS"`)两个字段。 -推导 `vSwitchType` 使用的是 `type` 枚举字段。 - -* 根据 `HostSwitchProfileData.type` 和 Cloud `HostVO.uuid` 创建 `SdnControllerHostRefVO`,`type` 与 `vSwitchType` 的映射规则: -+ -[cols="2,2"] -|=== -|ZNS HostSwitchProfileData.type |Cloud SdnControllerHostRefVO.vSwitchType +`isDefault` 的设置规则:每种 type(vlan、overlay)的第一个 transport zone 自动设为默认。重连时保留已有的默认设置。 -|`dpdk` -|`OvsDpdk` +==== 6. 同步 Segments 并创建 L2/L3 -|`kernel` -|`OvsKernel` +根据 computerManagerUuid 从 ZNS 获取 segments 列表(带 cms 过滤)。 -|其它未知值,或者 profile 查询失败 -|`ZNS` +对每个 segment: -|=== -* 以上步骤完成后,Cloud 侧就完成了 SdnControllerHostRefVO 的初始化,同时持有两类 transport zone 缓存: -** transport zone 元数据缓存:保存到建议新增的 `ZnsTransportZone` -** `transportZoneUuid → Set` 反向缓存:用于 segment 和 cluster 的关联 -* 根据 computerManagerUuid 从 ZNS 获取 segments 列表。 -** 根据 segment 信息创建 L2Network、L3Network -** 根据 segment.ipam 信息创建 IpRange +* 根据 segment 信息创建 L2Network、L3Network +* 根据 segment.ipam 信息创建 IpRange +* 通过 PATCH 将 Cloud L2 UUID 回写到 ZNS segment 的 cms.cms_resource_uuid 中 ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: @@ -178,10 +270,11 @@ ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: 补充说明: * `L2Network.vSwitchType` 固定写入 `ZNS` +* `L2Network.physicalInterface` 固定写入空字符串 * `L2Network.virtualNetworkId` 取 `segment.virtual_network_id`(Geneve/VLAN) * `L3Network.category` 当前初始化逻辑固定为 `Private` -5. 根据 `segment.transport_zone_uuid` 查询前面缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 +根据 `segment.transport_zone_uuid` 查询步骤 4 缓存的 `transportZoneUuid → Set` 映射,为每个关联的 cluster 创建一条 `L2NetworkClusterRefVO`,建立 ZNS segment 和 Cloud cluster 的映射关系。 `L2NetworkClusterRefVO` 的映射关系如下: @@ -212,7 +305,7 @@ ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: ==== 1. 刷新 SdnControllerHostRefVO(upsert) -重连仍需重新扫描 host,以处理新加入的主机或 vSwitchType 发生变化的主机。 +重连仍需重新扫描 host(包括 compute collections → discovered nodes → derive vSwitchType),以处理新加入的主机或 vSwitchType 发生变化的主机。 映射关系与创建时完全相同(`HostData.managementIp` → `HostVO`,`HostSwitchProfileData.type` → `vSwitchType`), 区别仅在于写库操作改为 upsert: @@ -234,30 +327,34 @@ ZNS `segment.transport_type` 和 ZStack L2/L3 的映射关系如下: [NOTE] 创建时只做 INSERT;重连时使用 upsert,可处理主机上下线及 vSwitchType 变更的情况。 -==== 2. Segment 协调(以 Cloud 为基准) +==== 2. 刷新 Transport Zones + +调用 `GET /zns/api/v1/fabric/transport-zones` 重新拉取 transport zone 列表,对 `ZnsTransportZoneVO` 做 upsert,同时删除 ZNS 侧已不存在的旧记录。重连时保留已有的 `isDefault` 设置。 + +==== 3. Segment 协调(以 Cloud 为基准) 重连以 Cloud 数据库中所有 `vSwitchType = ZNS` 的 `L2NetworkVO` 为基准,与 ZNS 侧属于本 Computer Manager 的 segment 做三路对比。 -ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关联。 +ZNS 侧 segment 通过 cms 元数据中的 `cms_resource_uuid` 与 Cloud L2 关联。 [cols="2,2,3"] |=== |Cloud 侧 L2NetworkVO |ZNS 侧 segment |操作 -|不存在(`l2Uuid` 指向已删除 L2,或 segment 无 `l2Uuid`) +|不存在(`cms_resource_uuid` 指向已删除 L2,或 segment 无 `cms_resource_uuid`) |存在 |调用 `DELETE /zns/api/v1/segments` 删除孤儿 segment |存在 |不存在 -|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3 信息 +|调用 `POST /zns/api/v1/segments` 在 ZNS 新建 segment,参数来自 Cloud L2/L3/IpRange 信息 |存在 -|存在但参数不一致(如 CIDR) +|存在但参数不一致(名称、描述、CIDR 等) |调用 `PATCH /zns/api/v1/segments/{uuid}` 更新 |存在 |存在且参数一致 -|无操作 +|无操作(仅同步 systemTag 确保 segment UUID 映射正确) |=== @@ -265,10 +362,10 @@ ZNS 侧 segment 通过 cms 元数据中的 `ExternalIds.l2Uuid` 与 Cloud L2 关 重连 *不会* 根据 ZNS segment 在 Cloud 侧创建新的 L2Network / L3Network / IpRange / `L2NetworkClusterRefVO`。 如果 ZNS 存在但 Cloud 不存在,视为孤儿 segment 并删除(与创建阶段的单向导入方向相反)。 -==== 3. Segment Port 协调 +==== 4. Segment Port 协调 完成 segment 协调后,对每个已与 Cloud L2 匹配的 segment,逐一协调其 port。 -Port 通过 cms 元数据中的 `ExternalIds.vmNicUuid` 与 Cloud `VmNicVO` 关联。 +Port 通过 cms 元数据中的 `cms_resource_uuid` 与 Cloud `VmNicVO` 关联。 [cols="2,2,3"] |=== @@ -283,13 +380,30 @@ Port 通过 cms 元数据中的 `ExternalIds.vmNicUuid` 与 Cloud `VmNicVO` 关 |调用 `DELETE /zns/api/v1/segments/{uuid}/ports` 删除孤儿 port |两侧均存在 -|(当前实现不做参数比对更新) +|(当前实现不做参数比对更新,仅同步 systemTag) |无操作 |=== +=== 心跳探活(Ping) + +Cloud 定期发送 `SdnControllerPingMsg`,通过调用 `GET /zns/api/v1/fabric/compute-managers/{uuid}` 验证 Computer Manager 连接是否正常。 + +* 验证成功 → 控制器保持 Connected 状态 +* 验证失败 → 控制器状态变为 Disconnected + === 删除SDN控制器 -删除 ZNS SDN Controller 时,会级联删除 ZNS 侧的 Computer Manager 和 Segment、Segment Port 等资源。Cloud 侧的 L2Network、L3Network、VmNic 等也会一起删除。 + +删除 ZNS SDN Controller 时的清理流程: + +. 根据 computerManagerUuid 从 ZNS 查询属于本 Controller 的所有 segments +. 批量调用 `DELETE /zns/api/v1/segments` 删除这些 segments(force = true),连带删除 port +. 删除 Cloud 本地的 `ZnsTransportZoneVO` 记录 +. 调用 `DELETE /zns/api/v1/fabric/compute-managers/{uuid}` 删除 Computer Manager + +[NOTE] +删除 segments 和 compute manager 过程中如果 ZNS 调用失败,仅打印告警日志,不阻断删除流程。 +Cloud 侧的 L2Network、L3Network、IpRange、VmNic 等资源由 `SdnControllerVO` 级联删除机制清理。 == L2Network @@ -315,7 +429,7 @@ ZNS L2Network 的类型定义: |ZNS(固定值,不区分 kernel 和 dpdk) |physicalInterface -|null +|空字符串 |virtualNetworkId |Vlan Id 或 Geneve Id @@ -324,9 +438,7 @@ ZNS L2Network 的类型定义: === 创建 L2Network -处理逻辑类似 OVN Controller,但调用 ZNS API 创建 segment。 - -创建 ZNS 二层网络时,Cloud 需要先根据 L2 类型从已缓存的 transport zone 中选择默认 transport zone: +创建 ZNS 二层网络时,Cloud 根据 L2 类型从已缓存的 `ZnsTransportZoneVO` 中选择默认 transport zone(`isDefault = true`): [cols="2,2,3"] |=== @@ -346,6 +458,13 @@ ZNS L2Network 的类型定义: |=== +调用 `POST /zns/api/v1/segments` 创建 segment,请求体包含:name、description、transport_type(转为大写)、transport_zone_uuid、virtual_network_id、cms 信息。 + +创建成功后,将 ZNS 返回的 segment UUID 通过 systemTag 记录到 L2NetworkVO 上。 + +[NOTE] +创建 L2Network 时,Cloud 会自动将 `vSwitchType` 设为 `ZNS`,`physicalInterface` 设为空字符串。这通过 API 拦截器(`ZnsApiInterceptor`)在 `APICreateL2NetworkMsg` 处理前自动完成。 + [NOTE] ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。 因此 Cloud 创建的 L2Network 在 OVN 侧默认可在该 transport zone 覆盖的全部物理机上使用,而不是由 OVN 提供 cluster 级隔离。 @@ -354,15 +473,19 @@ ZNS 基于 OVN 实现,OVN 不能提供类似 Cloud 的 cluster 能力。 Cloud 侧会额外施加一层调度约束:只有二层网络实际加载了某个 cluster 后,该 cluster 中的物理机才允许用于创建虚拟机。 也就是说,transport zone 决定的是底层网络可达范围,`L2NetworkClusterRefVO` 决定的是 Cloud 侧可调度范围。 -==== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg +=== 删除 L2Network -处理逻辑类似 OVN Controller,根据 ZNS Host 和 transport zone 的关系,把 ZNS segment 关联到 transport zone。 +调用 `DELETE /zns/api/v1/segments` 删除对应的 ZNS segment(force = true),并清理 systemTag。 +如果 ZNS 侧删除失败(例如 segment 已不存在),仅打印告警日志,不阻断 Cloud 侧的删除流程。 -==== APIChangeL2NetworkVlanIdMsg +=== APIChangeL2NetworkVlanIdMsg -* L2GeneveNetwork 类型不支持修改 VlanId,需要在 `L2NetworkApiInterceptor` 中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` +* L2GeneveNetwork 类型不支持修改 VlanId,需要在 API 拦截器中拦截:如果 L2Network 的 type 为 L2GeneveNetwork,抛出 `ApiMessageInterceptionException` * L2VlanNetwork、L2NoVlanNetwork 类型支持 -* 仅需要修改 L2NetworkVO 数据库,不需要下发到物理机,需要调用修改 ZNS segment API + +=== APIAttachL2NetworkToClusterMsg / APIDetachL2NetworkFromClusterMsg + +当前实现为空操作(no-op)。ZNS 通过 transport zone 管理网络覆盖范围,attach/detach cluster 仅影响 Cloud 侧的调度约束。 == L3Network @@ -392,11 +515,18 @@ ZNS L3 的定义: ZNS L3Network 不添加网络服务。 -=== SetVmStaticIp / ChangeVmIp 操作 +=== 创建/删除 IpRange + +创建 IpRange 时,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `gateway_address` 设为 IpRange 的 `networkCidr`,同步 CIDR 信息到 ZNS。 + +删除最后一个 IpRange 时,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `gateway_address` 设为空字符串,清除 ZNS 侧的 CIDR 信息。 + +[NOTE] +创建/删除 L3Network 本身不触发 ZNS API 调用,仅 IpRange 的变化才同步到 ZNS。 -由于 ZNS 网络的 IP 由 ZNS 管理,`APISetVmStaticIpMsg` 和 `APIChangeVmIpMsg` 需要特殊处理: +=== MTU 同步 -在 `VmInstanceApiInterceptor` 中增加校验:如果目标 L3Network 关联的 L2Network 的 vSwitchType 为 ZNS,需要将用户指定的 IP 传给 ZNS segment port API 进行更新,而非走 Cloud 侧的 IP 分配流程。 +当用户修改 L3Network MTU 时,如果该 L3 属于 ZNS 网络,Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}` 将 `mtu` 字段同步到 ZNS segment。 == VmNic @@ -415,10 +545,11 @@ VmNicType 的值有:VNIC、VF、`dpdkvhostuserclient`。ZNS 可能是 dpdk 模 ZNS 网络创建过程: -1. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType -2. 调用 ZNS 创建 segment port API,返回 ip/掩码/网关、ip6/前缀/网关 -3. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 -4. 根据获取的参数创建 `VmNicVO`、`UsedIpVO` +. 和现在逻辑一样分配网卡 mac、internalId、internalName、driverType +. 调用 ZNS 创建 segment port API(`POST /zns/api/v1/segments/{uuid}/ports`),请求体包含:name、mac、ip、vm_uuid、cms 信息。ZNS 返回分配的 IP 地址 +. ZNS L3 网络走 `enableIpAddressAllocation()` 为 false 的流程,Cloud 直接把 ZNS 返回的 IP 地址保存到 `UsedIpVO`,不走 Cloud 侧的 IP 分配流程 +. 根据获取的参数创建/更新 `VmNicVO`、`UsedIpVO` +. 将 ZNS 返回的 port UUID 通过 systemTag 记录到 VmNicVO 上 === 网卡删除过程 @@ -427,23 +558,354 @@ ZNS 网络创建过程: 两个 Flow 中都需要: -1. 调用 ZNS 删除 segment port API -2. 删除 `VmNicVO`、`UsedIpVO` +. 调用 ZNS 删除 segment port API(`DELETE /zns/api/v1/segments/{uuid}/ports`) +. 清理 systemTag +. 删除 `VmNicVO`、`UsedIpVO` -=== DPDK 网卡的特殊处理 +[NOTE] +如果 ZNS 侧删除失败,仅打印告警日志,不阻断 Cloud 侧的删除流程。 -由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 -这个逻辑与 OVN DPDK 虚拟网卡一致。 +=== IP 变更(VmIpChanged) + +当虚拟机 IP 发生变化时(`SetVmStaticIp`、`ChangeVmIp` 等操作),Cloud 调用 `PATCH /zns/api/v1/segments/{uuid}/ports/{portUuid}` 更新 ZNS 侧的 port IP。 === ChangeVmNicNetwork(换网操作) `APIChangeVmNicNetworkMsg` 涉及 detach 旧网络 + attach 新网络: -* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络 -* 从 ZNS 网络变换成 ZNS 网络的场景,需要调用 ZNS API 删除旧的 segment port,调用 API 创建新的 segment port,并更新 `VmNicVO`/`UsedIpVO` 等相关数据对象 +* 不支持从 ZNS 变换成非 ZNS 网络,或从非 ZNS 变换成 ZNS 网络(API 拦截器阻断) +* 不支持在不同 ZNS 控制器之间换网(API 拦截器阻断) +* 从 ZNS 网络变换成同一控制器下的 ZNS 网络: +** `beforeUpdateNic`:记录旧 port 上下文(znsIp、segmentUuid、portUuid) +** `afterUpdateNic`:先删除旧 segment port,再在新 segment 上创建新 port + +=== DPDK 网卡的特殊处理 + +由于 libvirt 不能自动创建 `dpdkvhostuserclient` 类型的网卡,Cloud 需要在虚拟机启动前,在物理机上预先创建对应的 `dpdkvhostuserclient` 网卡。 +这个逻辑与 OVN DPDK 虚拟网卡一致。 === FilterAttachableL3NetworkExtensionPoint -* `APIGetVmAttachableL3NetworkMsg` 必须能获取到 ZNS L3 网络。 +获取虚拟机可挂载的 L3 网络列表时,ZNS 网络有额外的过滤规则: + +* 如果虚拟机尚未挂载 ZNS 网络:ZNS 类型的 L3 仅当虚拟机所在物理机被该 ZNS 控制器管理时才可挂载 +* 如果虚拟机已挂载 ZNS 网络:只允许挂载同一 ZNS 控制器下的 L3 或非 ZNS 的 L3 + +== API 拦截器 + +`ZnsApiInterceptor` 在以下场景进行拦截: + +* `APICreateL2NetworkMsg`:如果 systemTag 指定了 ZNS SDN 控制器,自动设置 `vSwitchType = ZNS`、`physicalInterface = ""` +* `APIChangeL2NetworkVlanIdMsg`:禁止修改 L2GeneveNetwork 的 VLAN ID +* `APIChangeVmNicNetworkMsg`:禁止 ZNS 与非 ZNS 网络之间换网,禁止不同 ZNS 控制器之间换网 +* `APISdnControllerAddHostMsg` / `RemoveHostMsg` / `ChangeHostMsg`:禁止对 ZNS 控制器手动管理主机(由 ZNS 自动管理) + +[[api-version]] +== API 版本兼容性 + +=== 问题背景 + +Cloud 和 ZNS 是独立部署的两个组件,可能出现版本不一致: + +* **场景A**:Cloud 升级了,ZNS 没升级 — Cloud 发出 ZNS 不认识的 API 格式 +* **场景B**:ZNS 升级了,Cloud 没升级 — Cloud 用旧格式调新 API,参数缺失或语义变化 +* **场景C**:运行中 ZNS 被升级/降级 — Cloud 缓存的版本信息过期 + +=== 设计思路 + +.... +1. 每个 API 独立版本 + 不是给 ZNS 一个整体版本号,而是给每个 API 独立的版本号。 + ZNS 一年 2-3 个版本,但不是每个 API 都会变。 + +2. ZNS 是自己能力的真相源 + 每个 API 通过 HTTP OPTIONS 方法声明自己支持哪些版本。 + ZNS 各 API 各管各的版本,不集中到一个大接口里。 + +3. Cloud 通过 HTTP Header 声明自己发送的版本 + 每次请求带 X-Api-Version header,ZNS 可据此选择处理逻辑。 +.... + +=== 版本模型 + +==== 每个 API 有独立版本 + +.... +API 路径 + 方法 Cloud 发送的版本 ZNS 支持的版本 +────────────────────────────────────── ──────────────── ─────────────── +POST /zns/api/v1/segments 1.1 [1.0, 1.1] +PATCH /zns/api/v1/segments/{uuid} 1.1 [1.0, 1.1, 1.2] +DELETE /zns/api/v1/segments 1.0 [1.0] +GET /zns/api/v1/segments 1.1 [1.0, 1.1] +POST /zns/api/v1/segments/{id}/ports 1.0 [1.0, 1.1] +PATCH /zns/api/v1/segments/{id}/ports 1.1 [1.0, 1.1] +DELETE /zns/api/v1/segments/{id}/ports 1.0 [1.0] +.... + +* *Cloud 侧*:每个 API 有一个确定的版本,表示"我发出去的请求是什么格式" +* *ZNS 侧*:每个 API 支持一组版本,表示"我能理解哪些格式" +* *兼容条件*:Cloud 发送的版本 ∈ ZNS 支持的版本集合 + +==== API 版本变更举例 + +.... +ZNS v1.0 发布: + PATCH /segments 支持 [1.0] + POST /ports 支持 [1.0] + +ZNS v1.1 发布(PATCH segment 新增 mtu 字段): + PATCH /segments 支持 [1.0, 1.1] ← 新增 1.1,但仍兼容 1.0 + POST /ports 支持 [1.0] ← 没变 + +ZNS v1.2 发布(PATCH segment 删除了某个旧字段): + PATCH /segments 支持 [1.1, 1.2] ← 不再支持 1.0 + POST /ports 支持 [1.0, 1.1] ← 新增 1.1 +.... + +=== 版本查询:HTTP OPTIONS 方法 + +每个 API 路径通过标准的 HTTP OPTIONS 方法返回自己支持的版本。 + +.... +Cloud ZNS + │ │ + │ OPTIONS /zns/api/v1/segments │ + │───────────────────────────────────────────────────►│ + │ │ + │ ◄── 204 No Content │ + │ Headers: │ + │ Allow: GET, POST, DELETE │ ← 标准头 + │ X-Api-Versions: POST=1.0,1.1; │ ← 自定义头 + │ DELETE=1.0; │ + │ GET=1.0,1.1 │ + │ │ + │ OPTIONS /zns/api/v1/segments/{uuid} │ + │───────────────────────────────────────────────────►│ + │ │ + │ ◄── 204 No Content │ + │ Headers: │ + │ Allow: GET, PATCH │ + │ X-Api-Versions: PATCH=1.0,1.1,1.2; │ + │ GET=1.0,1.1 │ + │ │ +.... + +`X-Api-Versions` Header 格式: +.... +X-Api-Versions: {METHOD1}={ver1},{ver2};{METHOD2}={ver1},{ver2} + +示例:POST=1.0,1.1;DELETE=1.0;GET=1.0,1.1 +.... + +=== 版本声明:X-Api-Version 请求头 + +Cloud 每次发送 API 请求时,通过 Header 声明自己发送的是哪个版本的格式: + +.... +Cloud ZNS + │ │ + │ PATCH /zns/api/v1/segments/{uuid} │ + │ Headers: │ + │ X-Api-Version: 1.1 ← 声明版本 │ + │ Content-Type: application/json │ + │ x-web-hook: ... │ + │ x-job-uuid: ... │ + │ Body: │ + │ { "name": "...", "mtu": 9000 } ← 1.1 格式 │ + │───────────────────────────────────────────────────►│ + │ │ + │ ZNS 收到请求: │ + │ 1. 检查 X-Api-Version = 1.1 │ + │ 2. PATCH segments 支持 1.1? → 用 1.1 处理逻辑 │ + │ 不支持? → 返回 400 │ + │ │ +.... + +==== ZNS 处理不支持的版本 + +.... +ZNS 返回: + HTTP 400 Bad Request + { + "error": "unsupported_api_version", + "message": "PATCH /segments does not support version 1.3", + "supported_versions": ["1.0", "1.1", "1.2"] + } +.... + +==== ZNS 处理没有版本 Header 的请求(兼容旧版 Cloud) + +没有 `X-Api-Version` header 时,ZNS 按该 API 支持的最低版本处理,确保旧版 Cloud 仍能正常使用。 + +=== 版本检查时机 + +.... +核心原则: + 不在每次 API 调用前询问 ZNS(会导致每次操作延迟翻倍) + 而是在 3 个时机批量拉取版本信息,缓存在内存中 + + ZNS 版本一年才变 2-3 次,变版本必然有运维动作(升级), + 升级后通常会触发 reconnect。Ping 间隔内版本突然变化的概率极低。 +.... + +[cols="2,3,3"] +|=== +|时机 |触发条件 |行为 + +|添加控制器(preInit) +|用户首次添加 ZNS 控制器到 Cloud +|对每个 API 路径发 OPTIONS → 检查兼容性 → 缓存。不兼容则拒绝添加 + +|重连控制器(reconnect) +|管理员手动触发重连,或网络恢复后自动重连 +|清除旧缓存 → 重新发 OPTIONS → 检查 → 缓存。不兼容则重连失败 + +|心跳探活(ping) +|Cloud 定时 Ping ZNS +|对每个 API 路径发 OPTIONS → 与缓存对比。版本变化 → 刷新缓存 → 重新检查。不兼容 → Ping 失败 → 标记 Disconnected + +|=== + +=== 双层防御 + +版本兼容性在两个层面保证: + +.... +第 1 层: Cloud 提前检查(发请求之前) + Cloud 在 preInit/reconnect/ping 时通过 OPTIONS 拉取版本 + 缓存到内存,每次发请求前本地查缓存 + 不兼容 → 不发请求,直接报错或降级 + 作用: 避免发出注定失败的请求 + +第 2 层: ZNS 兜底校验(收到请求时) + ZNS 收到请求后检查 X-Api-Version header + 不支持 → 返回 400 + supported_versions + 作用: 即使 Cloud 缓存过期,ZNS 也能拒绝不兼容的请求 +.... + +=== 不兼容时的处理策略 + +不同 API 不兼容时,处理方式不同: + +[cols="3,1,3"] +|=== +|API (路径 + 方法) |重要性 |不兼容时行为 + +|POST /segments(创建网络) +|关键 +|阻断: 拒绝添加/重连控制器 + +|DELETE /segments(删除网络) +|关键 +|阻断 + +|POST /segments/{id}/ports(创建端口) +|关键 +|阻断 + +|DELETE /segments/{id}/ports(删除端口) +|关键 +|阻断 + +|GET /segments(列表,对账用) +|关键 +|阻断 + +|GET /segments/{id}/ports(列表,对账用) +|关键 +|阻断 + +|PATCH /segments/{uuid}(更新 mtu 等) +|非关键 +|降级: 跳过 + 打印告警日志 + +|PATCH /segments/{id}/ports/{id}(更新端口) +|非关键 +|降级: 跳过 + 打印告警日志 + +|=== + +=== 兼容旧版 + +==== Cloud 遇到旧版 ZNS(不支持 OPTIONS 版本响应) + +如果 OPTIONS 返回 404 或 204 但没有 `X-Api-Versions` header,Cloud 假设所有 API 只支持 `["1.0"]`。 + +==== ZNS 遇到旧版 Cloud(不带 X-Api-Version header) + +没有 `X-Api-Version` header 时,ZNS 按最低支持版本处理。 + +=== 错误信息设计 + +==== Cloud 新,ZNS 旧 + +.... +ZNS controller [192.168.1.10] does not support the following APIs +required by this Cloud version: + - PATCH /segments: Cloud sends v1.1, ZNS supports [1.0] + - POST /ports: Cloud sends v1.1, ZNS supports [1.0] +Please upgrade ZNS to a compatible version. +.... + +==== ZNS 新,Cloud 旧 + +.... +This Cloud version is not compatible with ZNS [192.168.1.10]: + - PATCH /segments: Cloud sends v1.0, ZNS supports [1.1, 1.2] (v1.0 dropped) +Please upgrade Cloud to a compatible version. +.... + +==== 非关键 API 降级 + +.... +WARN: ZNS [192.168.1.10] does not support PATCH /segments v1.1 +(ZNS supports [1.0]). MTU sync will be skipped. +This does not affect core network operations. Upgrade ZNS to enable MTU sync. +.... + +=== 版本维护规范 + +==== 什么时候需要升版本 + +需要升版本: + +* 请求体新增必填字段 +* 请求体删除字段 +* 字段类型变化(string → int) +* 字段语义变化(单位从 MB 变成 KB) +* 响应体结构变化(影响 Cloud 解析) + +不需要升版本: + +* 请求体新增可选字段(ZNS 忽略未知字段即可) +* 纯内部实现变化,接口不变 +* 性能优化,接口不变 + +==== ZNS 的向后兼容策略 + +建议每个 API 至少兼容前 2 个版本。 + +.... +示例: PATCH /segments + v1.2 发布时 → supported: [1.0, 1.1, 1.2] (兼容 3 个版本) + v1.3 发布时 → supported: [1.1, 1.2, 1.3] (下线 1.0) + +这样可以给 Cloud 升级留出足够的时间窗口。 +.... + +=== 双方职责总结 + +==== ZNS 侧 + +. 每个 API 路径实现 OPTIONS 方法,返回 `Allow` + `X-Api-Versions` header +. 收到请求时检查 `X-Api-Version` header:支持则用对应版本处理;不支持则返回 400;缺失则按最低版本处理 +. 每次 API 格式变化时在该 API 的 OPTIONS 响应中新增版本号,保留旧版本兼容 +. 新增 API 时实现 OPTIONS,初始版本为 `1.0` +==== Cloud 侧 +. 维护 `CLOUD_API_VERSIONS` 表,记录当前发出的每个 API 的格式版本 +. 在 preInit、reconnect、ping 三个时机通过 OPTIONS 拉取版本并缓存 +. 每次发请求带上 `X-Api-Version` header +. 每次修改 API 请求体格式时同步更新版本号 +. 标记每个 API 的重要性(关键/非关键),决定不兼容时是阻断还是降级 diff --git a/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/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/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/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/SdnControllerManagerImpl.java b/plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java index 7a1967e2447..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,11 +47,10 @@ import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class SdnControllerManagerImpl extends AbstractService implements SdnControllerManager, - L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, InstantiateResourceOnAttachingNicExtensionPoint, - PreVmInstantiateResourceExtensionPoint, VmReleaseResourceExtensionPoint, - ReleaseNetworkServiceOnDetachingNicExtensionPoint, SecurityGroupGetSdnBackendExtensionPoint, + L2NetworkCreateExtensionPoint, L2NetworkDeleteExtensionPoint, + SecurityGroupGetSdnBackendExtensionPoint, AfterAddIpRangeExtensionPoint, IpRangeDeletionExtensionPoint, GetSdnControllerExtensionPoint, - BeforeAllocateVmNicExtensionPoint, AfterReleaseVmNicExtensionPoint { + AfterAllocateSdnNicExtensionPoint { private static final CLogger logger = Utils.getLogger(SdnControllerManagerImpl.class); private static final Logger log = LoggerFactory.getLogger(SdnControllerManagerImpl.class); @@ -271,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()); } @@ -455,263 +459,6 @@ public void done(ErrorCodeList errorCodeList) { }); } - @Override - public void releaseVmResource(VmInstanceSpec spec, Completion completion) { - if (VmInstanceConstant.VmOperation.DetachNic != spec.getCurrentVmOperation() && - VmInstanceConstant.VmOperation.Destroy != spec.getCurrentVmOperation()) { - completion.success(); - return; - } - - if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) { - completion.success(); - return; - } - - // we run into this situation when VM nics are all detached and the - // VM is being rebooted - if (spec.getDestNics().isEmpty()) { - completion.success(); - return; - } - - Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { - continue; - } - - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null) { - continue; - } - - if (shouldSkipSdnForNic(l2VO)) { - continue; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10005, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); - return; - } - - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); - } - - if (nicMaps.isEmpty()) { - completion.success(); - return; - } - - removeLogicalPort(nicMaps, completion); - } - - @Override - public void instantiateResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, Completion completion) { - L2NetworkVO l2NetworkVO = dbf.findByUuid(l3.getL2NetworkUuid(), L2NetworkVO.class); - if (shouldSkipSdnForNic(l2NetworkVO)) { - completion.success(); - return; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid())); - return; - } - - Map> nicMaps = new HashMap<>(); - List nics = new ArrayList<>(); - nics.add(spec.getDestNics().get(0)); - nicMaps.put(controllerUuid, nics); - sdnAddVmNics(nicMaps, completion); - } - - @Override - public void releaseResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, NoErrorCompletion completion) { - L2NetworkVO l2NetworkVO = dbf.findByUuid(l3.getL2NetworkUuid(), L2NetworkVO.class); - if (shouldSkipSdnForNic(l2NetworkVO)) { - completion.done(); - return; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - logger.warn(String.format("sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid())); - completion.done(); - return; - } - - Map> nicMaps = new HashMap<>(); - List nics = new ArrayList<>(); - nics.add(spec.getDestNics().get(0)); - nicMaps.put(controllerUuid, nics); - - removeLogicalPort(nicMaps, new Completion(completion) { - @Override - public void success() { - completion.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.info(String.format("failed to remove logical port for vm[uuid:%s] nic[internalName:%s], because: %s", - spec.getVmInventory().getUuid(), spec.getDestNics().get(0).getInternalName(), errorCode.getDetails())); - completion.done(); - } - }); - } - - @Override - public void releaseResourceOnDetachingNic(VmInstanceSpec spec, VmNicInventory nic, NoErrorCompletion completion) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - L2NetworkVO l2NetworkVO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (shouldSkipSdnForNic(l2NetworkVO)) { - completion.done(); - return; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2NetworkVO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - logger.warn(String.format("sdn l2 network[uuid:%s] is not attached controller", l2NetworkVO.getUuid())); - completion.done(); - return; - } - - Map> nicMaps = new HashMap<>(); - List nics = new ArrayList<>(); - nics.add(spec.getDestNics().get(0)); - nicMaps.put(controllerUuid, nics); - - removeLogicalPort(nicMaps, new Completion(completion) { - @Override - public void success() { - completion.done(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.info(String.format("failed to remove logical port for vm[uuid:%s] nic[internalName:%s], because: %s", - spec.getVmInventory().getUuid(), spec.getDestNics().get(0).getInternalName(), errorCode.getDetails())); - completion.done(); - } - }); - } - - @Override - public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstantiateResourceException { - - } - - @Override - public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { - if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) { - completion.success(); - return; - } - - // we run into this situation when VM nics are all detached and the - // VM is being rebooted - if (spec.getDestNics().isEmpty()) { - completion.success(); - return; - } - - Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { - continue; - } - - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null) { - continue; - } - - if (shouldSkipSdnForNic(l2VO)) { - continue; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10007, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); - return; - } - - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); - } - - if (nicMaps.isEmpty()) { - completion.success(); - return; - } - - sdnAddVmNics(nicMaps, completion); - } - - @Override - public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { - // create/start/reboot vm failed, code will go here VmInstantiateResourcePreFlow.rollack() - // vm change image failed, - if (VmInstanceConstant.VmOperation.NewCreate != spec.getCurrentVmOperation()) { - completion.success(); - return; - } - - if (spec.getL3Networks() == null || spec.getL3Networks().isEmpty()) { - completion.success(); - return; - } - - // we run into this situation when VM nics are all detached and the - // VM is being rebooted - if (spec.getDestNics().isEmpty()) { - completion.success(); - return; - } - - Map> nicMaps = new HashMap<>(); - for (VmNicInventory nic : spec.getDestNics()) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { - continue; - } - - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null) { - continue; - } - - if (shouldSkipSdnForNic(l2VO)) { - continue; - } - - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10008, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); - return; - } - - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); - } - - if (nicMaps.isEmpty()) { - completion.success(); - return; - } - - removeLogicalPort(nicMaps, completion); - } - /** * Returns true if the L2 network should be skipped for SDN port management: * it has no SDN controller type configured on its VSwitchType. @@ -905,54 +652,111 @@ private SdnControllerVO getSdnControllerVO(L3NetworkInventory l3Network) { } @Override - public void beforeAllocateVmNic(VmNicInventory nic, VmInstanceSpec spec, Completion completion) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { + public void afterAllocateSdnNic(VmInstanceSpec spec, List nics, Completion completion) { + if (nics == null || nics.isEmpty()) { completion.success(); return; } - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null || shouldSkipSdnForNic(l2VO)) { - 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); } - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); + if (nicMaps.isEmpty()) { + completion.success(); return; } - Map> nicMaps = new HashMap<>(); - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); sdnAddVmNics(nicMaps, completion); } @Override - public void afterReleaseVmNic(VmNicInventory nic, Completion completion) { - L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); - if (l3Vo == null) { + public void rollbackSdnNic(VmInstanceSpec spec, List nics, Completion completion) { + if (nics == null || nics.isEmpty()) { completion.success(); return; } - L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); - if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + Map> nicMaps = new HashMap<>(); + for (VmNicInventory nic : nics) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + continue; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + continue; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + continue; + } + + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + } + + if (nicMaps.isEmpty()) { completion.success(); return; } - String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( - l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); - if (controllerUuid == null) { - completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] is not attached controller", l2VO.getUuid())); + removeLogicalPort(nicMaps, completion); + } + + @Override + public void releaseSdnNics(List nics, Completion completion) { + if (nics == null || nics.isEmpty()) { + completion.success(); return; } Map> nicMaps = new HashMap<>(); - nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + for (VmNicInventory nic : nics) { + L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); + if (l3Vo == null) { + continue; + } + + L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); + if (l2VO == null || shouldSkipSdnForNic(l2VO)) { + continue; + } + + String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( + l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); + if (controllerUuid == null) { + continue; + } + + nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); + } + + if (nicMaps.isEmpty()) { + completion.success(); + return; + } + removeLogicalPort(nicMaps, completion); } } diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 5c3b7f59fd6..671562c5368 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -609,6 +609,8 @@ public class SourceClassMap { 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"); @@ -1625,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 index 2d6cceeb9d9..e76ddab6ff8 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.java @@ -17,7 +17,7 @@ public static class Result { public Result throwExceptionIfError() { if (error != null) { throw new ApiException( - String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) ); } 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 e5745afc9a7..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()) diff --git a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy index 8a5f488c11d..cb05661c4f5 100644 --- a/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy @@ -4,6 +4,7 @@ import org.springframework.http.HttpEntity import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.zstack.sdk.SdnControllerInventory +import org.zstack.utils.gson.JSONObjectUtil import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.LoginReply import org.zstack.sdnController.h3cVcfc.H3cVcfcCommands.LoginRsp @@ -58,9 +59,9 @@ class SdnControllerSpec extends Spec implements HasSession { } postCreate { - inventory = querySdnController { + inventory = JSONObjectUtil.rehashObject(querySdnController { conditions=["uuid=${inventory.uuid}".toString()] - }[0] + }[0], SdnControllerInventory.class) } return id(name, inventory.uuid) From 6db09089b1899fb81848d7d742fe9915a8f0e6ba Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 3 Apr 2026 10:11:18 +0800 Subject: [PATCH 07/11] [rest]: improve markdown validation error reporting Resolves: ZCF-1365 Change-Id: I73637569786c6d6e6d6479646961726365737074 --- .../scripts/RestDocumentationGenerator.groovy | 175 ++++++++++-------- 1 file changed, 97 insertions(+), 78 deletions(-) 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")) + } + } +} From 96b8ed2be3b690ffe323fcd601b838f225d02418 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 10 Apr 2026 13:16:05 +0800 Subject: [PATCH 08/11] [zns]: add ZNS error code ORG_ZSTACK_NETWORK_ZNS_10011 Resolves: ZCF-2063 Change-Id: Ib7ed3b9a111bad1ec0d6f76ece259bfd25444bb7 --- .../zstack/utils/clouderrorcode/CloudOperationsErrorCode.java | 2 ++ 1 file changed, 2 insertions(+) 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 abbebd66529..c2626d6edc4 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -11952,6 +11952,8 @@ public class CloudOperationsErrorCode { 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_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"; From f1e0ef49e887adec8a59215726b551b865141761 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 10 Apr 2026 13:26:05 +0800 Subject: [PATCH 09/11] [l2]: skip ZNS L2 in attach interceptors AttachL2NetworkToCluster incorrectly blocks ZNS L2 networks. Three validation points assume all L2s use host physical interfaces. ZNS is an SDN controller, physicalInterface is always "" and meaningless. Fix 1: L2NetworkApiInterceptor - skip vSwitchType conflict check for ZNS L2 Fix 2: KVMApiInterceptor - skip vlan device name length check for ZNS L2 Fix 3: L2NoVlanNetwork - skip physicalInterface conflict for ZNS L2NoVlan, exclude ZNS L2Vlan from non-SDN overlap query Resolves: ZCF-2073 Change-Id: I757966777a64736a6d646178786e6b70617a7a77 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../org/zstack/network/l2/L2NetworkApiInterceptor.java | 4 +++- .../java/org/zstack/network/l2/L2NoVlanNetwork.java | 10 ++++++++-- .../main/java/org/zstack/kvm/KVMApiInterceptor.java | 5 +++++ .../utils/clouderrorcode/CloudOperationsErrorCode.java | 8 ++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) 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..fdb12a36f6b 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()) 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..6b5e8f0a9b4 100755 --- a/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java +++ b/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java @@ -968,6 +968,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 +984,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/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/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index c2626d6edc4..733c53d6688 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -11954,6 +11954,14 @@ public class CloudOperationsErrorCode { 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_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"; From f3ed00b61a907a6e477f014ecb75277983f735fd Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 10 Apr 2026 17:31:17 +0800 Subject: [PATCH 10/11] [network]: normalize ZNS L2 change target type handling Handle blank or omitted type in APIChangeL2NetworkVlanIdMsg by normalizing to the current L2 type before validation checks. Resolves: ZSTAC-2074 Change-Id: Icf960d0766b726047d8613305042cfa14e120c61 --- .../network/l2/L2NetworkApiInterceptor.java | 28 ++++++++++++++++--- .../l2/L2NetworkExtensionPointEmitter.java | 14 ++++++++++ .../network/l2/L2NetworkHostHelper.java | 5 +++- .../VxlanNetworkCheckerImpl.java | 2 +- .../CloudOperationsErrorCode.java | 16 +++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) 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 fdb12a36f6b..cf16ae5fb71 100755 --- a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java @@ -146,9 +146,25 @@ private void validate(APIChangeL2NetworkVlanIdMsg msg) { 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); + if (!targetIsVlan && !targetIsNoVlan && !targetIsGeneve) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10011, + "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())); @@ -159,7 +175,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" + @@ -192,7 +210,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())); @@ -200,7 +218,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" + 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/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/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 733c53d6688..4ec31dca27f 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -11950,6 +11950,16 @@ public class CloudOperationsErrorCode { 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"; @@ -11962,6 +11972,12 @@ public class CloudOperationsErrorCode { 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"; From 66c8e13ee0a4108a9b6d74a620f0554b981c8e1e Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 9 Apr 2026 16:05:52 +0800 Subject: [PATCH 11/11] [compute]: release sdn nics first Release SDN NICs before removing VmNicVO. Keep NIC deletion on release failure. Resolves: ZCF-2047 Change-Id: I83f534ea19849467a728e3b6fb9ee2f6bb43bb7e Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../zstack/compute/vm/VmCascadeExtension.java | 9 +++-- .../zstack/compute/vm/VmDetachNicFlow.java | 4 +- .../compute/vm/VmReturnReleaseNicFlow.java | 7 +++- .../network/l2/L2NetworkApiInterceptor.java | 40 ++++++++++++------- .../zstack/network/l2/L2NoVlanNetwork.java | 1 + .../CloudOperationsErrorCode.java | 1 + 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java index 5b2dd2f399a..07247d60b91 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java @@ -300,8 +300,8 @@ protected List handleDeletionForIpRange(List handleDeletionForIpRange(List handleDeletionForIpRange(List releasedNics = new ArrayList<>(); + List nicsToDelete = new ArrayList<>(); for (VmNicInventory nic : spec.getVmInventory().getVmNics()) { VmNicVO vo = dbf.findByUuid(nic.getUuid(), VmNicVO.class); if (VmInstanceConstant.USER_VM_TYPE.equals(spec.getVmInventory().getType())) { VmInstanceDeletionPolicy deletionPolicy = getDeletionPolicy(spec, data); if (deletionPolicy == VmInstanceDeletionPolicy.Direct) { - dbf.remove(vo); + nicsToDelete.add(vo); } else { vo.setUsedIpUuid(null); vo.setIp(null); @@ -93,7 +94,7 @@ public void done(ErrorCodeList errorCodeList) { dbf.update(vo); } } else { - dbf.remove(vo); + nicsToDelete.add(vo); } releasedNics.add(nic); } @@ -101,12 +102,14 @@ public void done(ErrorCodeList errorCodeList) { callReleaseSdnNics(releasedNics, new Completion(chain) { @Override public void success() { + nicsToDelete.forEach(dbf::remove); chain.next(); } @Override public void fail(ErrorCode errorCode) { logger.warn(String.format("releaseSdnNics failed: %s, continue anyway", errorCode)); + nicsToDelete.forEach(dbf::remove); chain.next(); } }); 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 cf16ae5fb71..79578eb5686 100755 --- a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java @@ -129,18 +129,20 @@ 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]" + @@ -157,8 +159,9 @@ private void validate(APIChangeL2NetworkVlanIdMsg msg) { 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); - if (!targetIsVlan && !targetIsNoVlan && !targetIsGeneve) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10011, + 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)); } @@ -249,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/L2NoVlanNetwork.java b/network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java index 6b5e8f0a9b4..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() { 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 4ec31dca27f..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";