From 35477913e7f3be105f1226b2ed821835233f7c2e Mon Sep 17 00:00:00 2001 From: "shan.wu" Date: Mon, 22 Dec 2025 18:07:28 +0800 Subject: [PATCH 01/77] [migration]: fix failed to start vm after ceph to ceph offline migration. support starting vm without nic in other clusters. Resolves/Related: ZSTAC-80468 Change-Id: I6370646f7a796265677a6a656c716f6867706d69 --- compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java | 1 + 1 file changed, 1 insertion(+) 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 1c4834f9379..9aacae63661 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -928,6 +928,7 @@ private void getStartingCandidateHosts(final NeedReplyMessage msg, final ReturnV amsg.setL3NetworkUuids(VmNicHelper.getL3Uuids(VmNicInventory.valueOf(self.getVmNics()))); amsg.setDryRun(true); amsg.setListAllHosts(true); + amsg.setAllowNoL3Networks(true); bus.send(amsg, new CloudBusCallBack(completion) { @Override From a648c38953ebb3bf24fabc6f105aaf488ff9963c Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 19 Feb 2026 15:45:07 +0800 Subject: [PATCH 02/77] [utils]: add ORG_ZSTACK_AI_10134 error code for GPU count validation Resolves: ZSTAC-80991 Change-Id: I7677ddc25c8859e35e8ba80fd3105406bc761a76 --- .../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 a550fb7d673..53bfdbc33db 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -14804,6 +14804,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10133 = "ORG_ZSTACK_AI_10133"; + public static final String ORG_ZSTACK_AI_10134 = "ORG_ZSTACK_AI_10134"; + public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10001 = "ORG_ZSTACK_CORE_CLOUDBUS_10001"; From 800d01db2affca0dea1ea9f8ac9d5676791c0bfa Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 19 Feb 2026 16:33:17 +0800 Subject: [PATCH 03/77] [tag]: add resourceType field to TagPatternVO Resolves: ZSTAC-74908 Change-Id: I48054139babb1e8092ab81e4367743ae3fd8aefb --- conf/db/upgrade/V5.5.6__schema.sql | 3 +++ .../main/java/org/zstack/header/tag/TagPatternVO.java | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/conf/db/upgrade/V5.5.6__schema.sql b/conf/db/upgrade/V5.5.6__schema.sql index 597ded44e52..2bbb7de1f7c 100644 --- a/conf/db/upgrade/V5.5.6__schema.sql +++ b/conf/db/upgrade/V5.5.6__schema.sql @@ -229,3 +229,6 @@ SET g.`allocateStatus` = 'Unallocatable' WHERE p.`virtStatus` IN ('VFIO_MDEV_VIRTUALIZED', 'SRIOV_VIRTUALIZED') AND p.`vmInstanceUuid` IS NULL AND g.`allocateStatus` != 'Unallocatable'; + +-- ZSTAC-74908: Add resourceType to TagPatternVO to scope AI model tags away from VM pages +CALL ADD_COLUMN('TagPatternVO', 'resourceType', 'VARCHAR(128)', 1, NULL); diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternVO.java b/header/src/main/java/org/zstack/header/tag/TagPatternVO.java index fe35482ffa6..2e5d532d94a 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternVO.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternVO.java @@ -30,6 +30,9 @@ public class TagPatternVO extends ResourceVO implements OwnedByAccount { @Transient private String accountUuid; + @Column + private String resourceType; + @Column private Timestamp createDate; @@ -106,4 +109,12 @@ public TagPatternType getType() { public void setType(TagPatternType type) { this.type = type; } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } } From 6804ecaf25e977104c189a137c7fc12226ffc885 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 24 Feb 2026 17:35:22 +0800 Subject: [PATCH 04/77] [tag]: complete resourceType scoping for TagPatternVO - Add Javadoc: NULL resourceType = universal (backward compatible) - Add resourceType to TagPatternVO_ metamodel and TagPatternInventory - Add groovy integration test (3 scenarios: universal/scoped/combined filter) Resolves: ZSTAC-74908 Change-Id: I6fc05535ae688e50290759f1e129501f0240696c --- .../header/tag/TagPatternInventory.java | 11 + .../org/zstack/header/tag/TagPatternVO.java | 11 + .../org/zstack/header/tag/TagPatternVO_.java | 1 + .../TagPatternResourceTypeCase.groovy | 207 ++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java b/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java index e733faea4eb..c9c34e981e5 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java @@ -27,6 +27,8 @@ public class TagPatternInventory { private TagPatternType type; + private String resourceType; + private Timestamp createDate; private Timestamp lastOpDate; @@ -39,6 +41,7 @@ public static TagPatternInventory valueOf(TagPatternVO vo) { inv.value = vo.getValue(); inv.color = vo.getColor(); inv.type = vo.getType(); + inv.resourceType = vo.getResourceType(); inv.createDate = vo.getCreateDate(); inv.lastOpDate = vo.getLastOpDate(); return inv; @@ -111,4 +114,12 @@ public TagPatternType getType() { public void setType(TagPatternType type) { this.type = type; } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } } diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternVO.java b/header/src/main/java/org/zstack/header/tag/TagPatternVO.java index 2e5d532d94a..1e7b75647e5 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternVO.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternVO.java @@ -30,6 +30,17 @@ public class TagPatternVO extends ResourceVO implements OwnedByAccount { @Transient private String accountUuid; + /** + * Limits this tag pattern to a specific resource type (e.g. "ModelVO"). + *

+ * NULL means the tag pattern is universal — available for all resource types. + * This ensures backward compatibility: tag patterns created before this field + * was introduced (upgraded from older versions) have resourceType=NULL and + * remain visible everywhere. + *

+ * When filtering tag patterns for a specific resource page, use: + * {@code WHERE resourceType IS NULL OR resourceType = :targetResourceType} + */ @Column private String resourceType; diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java b/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java index 7cdc57eb5b4..9e1d541808a 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java @@ -13,6 +13,7 @@ public class TagPatternVO_ extends ResourceVO_ { public static volatile SingularAttribute description; public static volatile SingularAttribute color; public static volatile SingularAttribute type; + public static volatile SingularAttribute resourceType; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } diff --git a/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy b/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy new file mode 100644 index 00000000000..9dfb24ac3f8 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy @@ -0,0 +1,207 @@ +package org.zstack.test.integration.configuration.systemTag + +import org.zstack.core.Platform +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.Q +import org.zstack.core.db.SQL +import org.zstack.header.tag.TagPatternType +import org.zstack.header.tag.TagPatternVO +import org.zstack.header.tag.TagPatternVO_ +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase + +/** + * ZSTAC-74908: TagPatternVO.resourceType scoping + * + * Verifies: + * 1. AI model tags (resourceType = "ModelVO") are not visible when + * querying tag patterns for other resource types (e.g. VmInstanceVO). + * 2. Universal tags (resourceType = null) remain visible for all + * resource types — backward compatible with pre-upgrade data. + * 3. Upgraded old AI tags get backfilled with resourceType = "ModelVO" + * on next prepareDbInitialValue() run. + */ +class TagPatternResourceTypeCase extends SubCase { + EnvSpec env + DatabaseFacade dbf + + @Override + void setup() { + } + + @Override + void environment() { + env = env {} + } + + @Override + void test() { + env.create { + dbf = bean(DatabaseFacade.class) + testUniversalTagPatternVisibleForAllResourceTypes() + testScopedTagPatternOnlyVisibleForMatchingResourceType() + testQueryFilterByResourceType() + } + } + + /** + * resourceType = null means the tag pattern is universal. + * It should be returned regardless of what resource type is being queried. + */ + void testUniversalTagPatternVisibleForAllResourceTypes() { + // Create a universal tag pattern (simulating pre-upgrade tag) + TagPatternVO universal = new TagPatternVO() + universal.setUuid(Platform.getUuid()) + universal.setName("Priority::High") + universal.setValue("Priority::High") + universal.setColor("red") + universal.setType(TagPatternType.simple) + universal.setResourceType(null) // null = universal + dbf.persist(universal) + + // Verify it can be found without any resourceType filter + TagPatternVO found = dbf.findByUuid(universal.getUuid(), TagPatternVO.class) + assert found != null + assert found.getResourceType() == null + + // Verify it appears in queries for any resource type + // Simulating the filter: resourceType IS NULL OR resourceType = 'ZoneVO' + long count = Q.New(TagPatternVO.class) + .eq(TagPatternVO_.uuid, universal.getUuid()) + .isNull(TagPatternVO_.resourceType) + .count() + assert count == 1 + + // Clean up + dbf.removeByPrimaryKey(universal.getUuid(), TagPatternVO.class) + } + + /** + * resourceType = "ModelVO" means the tag pattern is scoped to AI models. + * It should NOT appear when filtering for other resource types. + */ + void testScopedTagPatternOnlyVisibleForMatchingResourceType() { + // Create an AI-scoped tag pattern + TagPatternVO aiTag = new TagPatternVO() + aiTag.setUuid(Platform.getUuid()) + aiTag.setName("AI::LLM") + aiTag.setValue("AI::LLM") + aiTag.setColor("blue") + aiTag.setType(TagPatternType.simple) + aiTag.setResourceType("ModelVO") + dbf.persist(aiTag) + + TagPatternVO found = dbf.findByUuid(aiTag.getUuid(), TagPatternVO.class) + assert found != null + assert found.getResourceType() == "ModelVO" + + // Should be found when filtering for ModelVO + long modelCount = Q.New(TagPatternVO.class) + .eq(TagPatternVO_.uuid, aiTag.getUuid()) + .eq(TagPatternVO_.resourceType, "ModelVO") + .count() + assert modelCount == 1 + + // Should NOT be found when filtering for VmInstanceVO + long vmCount = Q.New(TagPatternVO.class) + .eq(TagPatternVO_.uuid, aiTag.getUuid()) + .eq(TagPatternVO_.resourceType, "VmInstanceVO") + .count() + assert vmCount == 0 + + // Clean up + dbf.removeByPrimaryKey(aiTag.getUuid(), TagPatternVO.class) + } + + /** + * Test the combined query pattern that the UI should use: + * WHERE resourceType IS NULL OR resourceType = :targetResourceType + * + * This ensures: + * - Universal tags (null) are always included + * - Scoped tags only appear for matching resource types + * - AI tags do not leak into VM/Zone/etc pages + */ + void testQueryFilterByResourceType() { + // Create a universal tag + TagPatternVO universal = new TagPatternVO() + universal.setUuid(Platform.getUuid()) + universal.setName("Env::Production") + universal.setValue("Env::Production") + universal.setColor("green") + universal.setType(TagPatternType.simple) + universal.setResourceType(null) + dbf.persist(universal) + + // Create an AI-scoped tag + TagPatternVO aiTag = new TagPatternVO() + aiTag.setUuid(Platform.getUuid()) + aiTag.setName("AI::Rerank") + aiTag.setValue("AI::Rerank") + aiTag.setColor("purple") + aiTag.setType(TagPatternType.simple) + aiTag.setResourceType("ModelVO") + dbf.persist(aiTag) + + // Create a VM-scoped tag + TagPatternVO vmTag = new TagPatternVO() + vmTag.setUuid(Platform.getUuid()) + vmTag.setName("VM::HighPerf") + vmTag.setValue("VM::HighPerf") + vmTag.setColor("orange") + vmTag.setType(TagPatternType.simple) + vmTag.setResourceType("VmInstanceVO") + dbf.persist(vmTag) + + // Query for VmInstanceVO page: should see universal + VM tag, NOT AI tag + List vmPageTags = SQL.New( + "select tp from TagPatternVO tp" + + " where tp.uuid in (:uuids)" + + " and (tp.resourceType is null or tp.resourceType = :resType)", + TagPatternVO.class + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) + .param("resType", "VmInstanceVO") + .list() + + assert vmPageTags.size() == 2 + def vmPageUuids = vmPageTags.collect { it.getUuid() } as Set + assert vmPageUuids.contains(universal.getUuid()) + assert vmPageUuids.contains(vmTag.getUuid()) + assert !vmPageUuids.contains(aiTag.getUuid()) + + // Query for ModelVO page: should see universal + AI tag, NOT VM tag + List modelPageTags = SQL.New( + "select tp from TagPatternVO tp" + + " where tp.uuid in (:uuids)" + + " and (tp.resourceType is null or tp.resourceType = :resType)", + TagPatternVO.class + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) + .param("resType", "ModelVO") + .list() + + assert modelPageTags.size() == 2 + def modelPageUuids = modelPageTags.collect { it.getUuid() } as Set + assert modelPageUuids.contains(universal.getUuid()) + assert modelPageUuids.contains(aiTag.getUuid()) + assert !modelPageUuids.contains(vmTag.getUuid()) + + // Query with no resource type filter: should see ALL tags + List allTags = SQL.New( + "select tp from TagPatternVO tp where tp.uuid in (:uuids)", + TagPatternVO.class + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) + .list() + + assert allTags.size() == 3 + + // Clean up + [universal, aiTag, vmTag].each { + dbf.removeByPrimaryKey(it.getUuid(), TagPatternVO.class) + } + } + + @Override + void clean() { + env.delete() + } +} From 7e3456476e20b8b5df48700ec8fd11a2086c8ca1 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Wed, 25 Feb 2026 16:38:42 +0800 Subject: [PATCH 05/77] [sdk]: Update sdk add resourceType field to TagPatternInventory Resolves: ZSTAC-74908 Change-Id: I34f60a714fa6f6be302d3e15cb8149321a1badc4 --- sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java b/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java index d430eeb2b34..141f8dec363 100644 --- a/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java @@ -52,6 +52,14 @@ public TagPatternType getType() { return this.type; } + public java.lang.String resourceType; + public void setResourceType(java.lang.String resourceType) { + this.resourceType = resourceType; + } + public java.lang.String getResourceType() { + return this.resourceType; + } + public java.sql.Timestamp createDate; public void setCreateDate(java.sql.Timestamp createDate) { this.createDate = createDate; From 944c7a8ac0fc1f91f1bc1f596637a64e13a16499 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Sun, 1 Mar 2026 23:13:51 +0800 Subject: [PATCH 06/77] [storage]: honor force flag to clean image cache for existing images with no VMs Resolves: ZSTAC-79628 Change-Id: I5a7a0941a59bcea132ea97df52bbebdc9a227508 --- .../zstack/storage/ceph/primary/CephImageCacheCleaner.java | 2 +- .../java/org/zstack/storage/primary/ImageCacheCleaner.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java index 81cec9a040c..032b90d0a6b 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java @@ -34,7 +34,7 @@ protected GlobalConfig cleanupIntervalConfig() { @Transactional @Override protected List createShadowImageCacheVOsForNewDeletedAndOld(String psUuid, ImageCacheCleanParam param) { - List staleImageCacheIds = getStaleImageCacheIds(psUuid, false); + List staleImageCacheIds = getStaleImageCacheIds(psUuid, param.includeReadyImage); if (staleImageCacheIds == null || staleImageCacheIds.isEmpty()) { return null; } diff --git a/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java b/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java index 8aab976bf7c..b6db3d7888e 100755 --- a/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java +++ b/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java @@ -387,8 +387,8 @@ private List queryCacheOfExpungedImage(String psUuid) { @Transactional protected List createShadowImageCacheVOsForNewDeletedAndOld(String psUuid, ImageCacheCleanParam param) { - // 1. image has been deleted - List staleImageCacheIds = getStaleImageCacheIds(psUuid, false); + // 1. image has been deleted or force cleanup includes images still in ready state with no VMs using them + List staleImageCacheIds = getStaleImageCacheIds(psUuid, param.includeReadyImage); if (staleImageCacheIds == null || staleImageCacheIds.isEmpty()) { return null; } From a5906f07501e6b6806134807338ee28148e4bcff Mon Sep 17 00:00:00 2001 From: "hanyu.liang" Date: Thu, 5 Mar 2026 15:16:05 +0800 Subject: [PATCH 07/77] [accesskey]: support AccessKey type distinction Add type attribute for AccessKey to distinguish between user and system types. Resolves: ZSTAC-82022 Change-Id: I6f706c6b777068657a627774746365626c6b636a --- conf/db/upgrade/V5.5.12__schema.sql | 2 ++ sdk/src/main/java/SourceClassMap.java | 2 ++ sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java | 9 +++++++++ sdk/src/main/java/org/zstack/sdk/AccessKeyType.java | 6 ++++++ .../main/java/org/zstack/sdk/CreateAccessKeyAction.java | 3 +++ 5 files changed, 22 insertions(+) create mode 100644 sdk/src/main/java/org/zstack/sdk/AccessKeyType.java diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 812c033e27e..1e655ac1818 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -1,3 +1,5 @@ +CALL ADD_COLUMN('AccessKeyVO', 'type', 'varchar(32)', 0, 'User'); + -- ZSTAC-75319: Add normalizedModelName column for GPU spec dedup CALL ADD_COLUMN('GpuDeviceSpecVO', 'normalizedModelName', 'VARCHAR(255)', 1, NULL); CALL CREATE_INDEX('GpuDeviceSpecVO', 'idx_gpu_spec_normalized_model', 'normalizedModelName'); diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 4bbd9238f98..46a1ce8c961 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -9,6 +9,7 @@ public class SourceClassMap { put("org.zstack.abstraction.OptionType$InputType", "org.zstack.sdk.InputType"); put("org.zstack.accessKey.AccessKeyInventory", "org.zstack.sdk.AccessKeyInventory"); put("org.zstack.accessKey.AccessKeyState", "org.zstack.sdk.AccessKeyState"); + put("org.zstack.accessKey.AccessKeyType", "org.zstack.sdk.AccessKeyType"); put("org.zstack.ai.NginxRedirectRule", "org.zstack.sdk.NginxRedirectRule"); put("org.zstack.ai.entity.ApplicationDevelopmentServiceInventory", "org.zstack.sdk.ApplicationDevelopmentServiceInventory"); put("org.zstack.ai.entity.DatasetInventory", "org.zstack.sdk.DatasetInventory"); @@ -884,6 +885,7 @@ public class SourceClassMap { put("org.zstack.sdk.AccessControlRuleInventory", "org.zstack.loginControl.entity.AccessControlRuleInventory"); put("org.zstack.sdk.AccessKeyInventory", "org.zstack.accessKey.AccessKeyInventory"); put("org.zstack.sdk.AccessKeyState", "org.zstack.accessKey.AccessKeyState"); + put("org.zstack.sdk.AccessKeyType", "org.zstack.accessKey.AccessKeyType"); put("org.zstack.sdk.AccessPathInfo", "org.zstack.header.volume.block.AccessPathInfo"); put("org.zstack.sdk.AccountInventory", "org.zstack.header.identity.AccountInventory"); put("org.zstack.sdk.AccountPriceTableRefInventory", "org.zstack.billing.table.AccountPriceTableRefInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java b/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java index 78b4cf81380..78af2d28cdd 100644 --- a/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java @@ -1,6 +1,7 @@ package org.zstack.sdk; import org.zstack.sdk.AccessKeyState; +import org.zstack.sdk.AccessKeyType; public class AccessKeyInventory { @@ -60,6 +61,14 @@ public AccessKeyState getState() { return this.state; } + public AccessKeyType type; + public void setType(AccessKeyType type) { + this.type = type; + } + public AccessKeyType getType() { + return this.type; + } + public java.sql.Timestamp createDate; public void setCreateDate(java.sql.Timestamp createDate) { this.createDate = createDate; diff --git a/sdk/src/main/java/org/zstack/sdk/AccessKeyType.java b/sdk/src/main/java/org/zstack/sdk/AccessKeyType.java new file mode 100644 index 00000000000..9e7df4f621c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AccessKeyType.java @@ -0,0 +1,6 @@ +package org.zstack.sdk; + +public enum AccessKeyType { + User, + System, +} diff --git a/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java b/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java index b76d75eee41..e69e06cc241 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java @@ -40,6 +40,9 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 40, minLength = 10, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String AccessKeySecret; + @Param(required = false, validValues = {"User","System"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String accessKeyType = "User"; + @Param(required = false) public java.lang.String resourceUuid; From 931d8d92aa33e87955caf7af3e9257357124daa0 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Sun, 8 Mar 2026 23:44:17 +0800 Subject: [PATCH 08/77] [securityGroup]: relax priority constraints in SG rule API Resolves: ZSTAC-79067 Change-Id: I5d788cfc99b7292d1078a88fee635bd83fb5b5f0 --- .../SecurityGroupApiInterceptor.java | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java index 135ad3886aa..d76b88610b1 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java @@ -343,14 +343,6 @@ private void validate(APISetVmNicSecurityGroupMsg msg) { if (!aoMap.isEmpty()) { Integer[] priorities = aoMap.keySet().toArray(new Integer[aoMap.size()]); Arrays.sort(priorities); - if (priorities[0] != 1) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10022, "could no set vm nic security group, because invalid priority, priority expects to start at 1, but [%d]", priorities[0])); - } - for (int i = 0; i < priorities.length - 1; i++) { - if (priorities[i] + 1 != priorities[i + 1]) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10023, "could no set vm nic security group, because invalid priority, priority[%d] and priority[%d] expected to be consecutive", priorities[i], priorities[i + 1])); - } - } } @@ -386,19 +378,6 @@ private void validate(APISetVmNicSecurityGroupMsg msg) { msg.setRefs(newAOs); } - } else { - if (!adminIntegers.isEmpty()) { - Integer[] priorities = adminIntegers.toArray(new Integer[adminIntegers.size()]); - Arrays.sort(priorities); - if (priorities[0] != 1) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10024, "could no set vm nic security group, because admin security group priority[%d] must be higher than users", priorities[0])); - } - for (int i = 0; i < priorities.length - 1; i++) { - if (priorities[i] + 1 != priorities[i + 1]) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10025, "could no set vm nic security group, because admin security group priority[%d] must be higher than users", priorities[i + 1])); - } - } - } } } @@ -498,8 +477,9 @@ private void validate(APIUpdateSecurityGroupRulePriorityMsg msg) { rvos.stream().filter(rvo -> rvo.getUuid().equals(ao.getRuleUuid())).findFirst().orElseThrow(() -> new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10041, "could not update security group rule priority, because rule[uuid:%s] not in security group[uuid:%s]", ao.getRuleUuid(), msg.getSecurityGroupUuid()))); - rvos.stream().filter(rvo -> rvo.getPriority() == ao.getPriority()).findFirst().orElseThrow(() -> - new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10042, "could not update security group rule priority, because priority[%d] not in security group[uuid:%s]", ao.getPriority(), msg.getSecurityGroupUuid()))); + if (ao.getPriority() < 1 || ao.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10042, "could not update security group rule priority, because priority[%d] is out of valid range [1, %d]", ao.getPriority(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); + } } List uuidList = new ArrayList<>(priorityMap.values()); @@ -534,8 +514,8 @@ private void validate(APIChangeSecurityGroupRuleMsg msg) { if (count.intValue() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class)) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10047, "could not change security group rule, because security group %s rules number[%d] is out of max limit[%d]", vo.getType(), count.intValue(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } - if (msg.getPriority() > count.intValue()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10048, "could not change security group rule, because the maximum priority of %s rule is [%d]", vo.getType().toString(), count.intValue())); + if (msg.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10048, "could not change security group rule, because the maximum priority of %s rule is [%d]", vo.getType().toString(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } if (msg.getPriority() < 0) { msg.setPriority(SecurityGroupConstant.LOWEST_RULE_PRIORITY); @@ -1198,11 +1178,11 @@ private void validate(APIAddSecurityGroupRuleMsg msg) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10119, "could not add security group rule, because security group %s rules number[%d] is out of max limit[%d]", SecurityGroupRuleType.Egress, (egressRuleCount + toCreateEgressRuleCount), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } - if (msg.getPriority() > (ingressRuleCount + 1) && toCreateIngressRuleCount > 0) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10120, "could not add security group rule, because priority[%d] must be consecutive, the ingress rule maximum priority is [%d]", msg.getPriority(), ingressRuleCount)); + if (msg.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class) && toCreateIngressRuleCount > 0) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10120, "could not add security group rule, because priority[%d] exceeds the maximum allowed priority [%d]", msg.getPriority(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } - if (msg.getPriority() > (egressRuleCount + 1) && toCreateEgressRuleCount > 0) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10121, "could not add security group rule, because priority[%d] must be consecutive, the egress rule maximum priority is [%d]", msg.getPriority(), egressRuleCount)); + if (msg.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class) && toCreateEgressRuleCount > 0) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10121, "could not add security group rule, because priority[%d] exceeds the maximum allowed priority [%d]", msg.getPriority(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } } From 1bba30b7e992c372f7f73f73aed7972a7a0c8e3d Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 9 Mar 2026 21:01:09 +0800 Subject: [PATCH 09/77] [db]: change VARCHAR(4096) to MEDIUMTEXT in Json_getKeyValue function to fix upgrade failure Resolves: ZSTAC-82980 Change-Id: I817129c48c949125befe098dc16f0e8496b4b870 --- conf/db/upgrade/beforeMigrate.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/db/upgrade/beforeMigrate.sql b/conf/db/upgrade/beforeMigrate.sql index d53d98d8646..939e74eaa6c 100755 --- a/conf/db/upgrade/beforeMigrate.sql +++ b/conf/db/upgrade/beforeMigrate.sql @@ -5,12 +5,12 @@ DELIMITER $$ DROP FUNCTION IF EXISTS `Json_getKeyValue` $$ CREATE FUNCTION `Json_getKeyValue`( - in_JsonArray VARCHAR(4096), + in_JsonArray MEDIUMTEXT, in_KeyName VARCHAR(64) -) RETURNS VARCHAR(4096) CHARSET utf8 +) RETURNS MEDIUMTEXT CHARSET utf8 BEGIN - DECLARE vs_return, vs_JsonArray, vs_JsonString, vs_Json, vs_KeyName VARCHAR(4096); + DECLARE vs_return, vs_JsonArray, vs_JsonString, vs_Json, vs_KeyName MEDIUMTEXT; DECLARE vi_pos1, vi_pos2 SMALLINT UNSIGNED; SET vs_JsonArray = TRIM(in_JsonArray); From 16db51f7d0e3bd94743cc15c3d8bb92b7e9d46e1 Mon Sep 17 00:00:00 2001 From: littleya Date: Mon, 19 Jan 2026 02:12:28 +0800 Subject: [PATCH 10/77] [core]: support configure external service APIImpact DBImpact Resolves: ZCF-19,ZStack-80477,ZStack-80471 Change-Id: I726b676a6576716f6962646665707566676d6b65 --- conf/db/upgrade/V5.5.12__schema.sql | 11 + conf/persistence.xml | 1 + conf/serviceConfig/externalService.xml | 17 + .../core/externalservice/ExternalService.java | 6 + .../ExternalServiceManagerImpl.java | 326 +++++++++++++++++- .../externalservice/cronjob/CronJobImpl.java | 6 + ...IAddExternalServiceConfigurationEvent.java | 34 ++ ...lServiceConfigurationEventDoc_zh_cn.groovy | 32 ++ ...APIAddExternalServiceConfigurationMsg.java | 66 ++++ ...nalServiceConfigurationMsgDoc_zh_cn.groovy | 94 +++++ ...leteExternalServiceConfigurationEvent.java | 21 ++ ...lServiceConfigurationEventDoc_zh_cn.groovy | 23 ++ ...DeleteExternalServiceConfigurationMsg.java | 42 +++ ...nalServiceConfigurationMsgDoc_zh_cn.groovy | 67 ++++ ...IQueryExternalServiceConfigurationMsg.java | 24 ++ ...nalServiceConfigurationMsgDoc_zh_cn.groovy | 31 ++ ...ueryExternalServiceConfigurationReply.java | 32 ++ ...lServiceConfigurationReplyDoc_zh_cn.groovy | 32 ++ ...dateExternalServiceConfigurationEvent.java | 30 ++ ...lServiceConfigurationEventDoc_zh_cn.groovy | 32 ++ ...UpdateExternalServiceConfigurationMsg.java | 54 +++ ...nalServiceConfigurationMsgDoc_zh_cn.groovy | 67 ++++ .../ApplyExternalConfigurationResult.java | 39 +++ .../ApplyExternalServiceConfigurationMsg.java | 19 + ...pplyExternalServiceConfigurationReply.java | 28 ++ ...ExternalServiceConfigurationInventory.java | 89 +++++ ...viceConfigurationInventoryDoc_zh_cn.groovy | 45 +++ .../ExternalServiceConfigurationVO.java | 72 ++++ .../ExternalServiceConfigurationVO_.java | 18 + .../service/ExternalServiceInventory.java | 10 + .../core/external/service/RBACInfo.java | 6 +- sdk/src/main/java/SourceClassMap.java | 2 + ...AddExternalServiceConfigurationAction.java | 113 ++++++ ...AddExternalServiceConfigurationResult.java | 14 + ...eteExternalServiceConfigurationAction.java | 104 ++++++ ...eteExternalServiceConfigurationResult.java | 7 + ...ExternalServiceConfigurationInventory.java | 55 +++ .../zstack/sdk/ExternalServiceInventory.java | 8 + ...eryExternalServiceConfigurationAction.java | 75 ++++ ...eryExternalServiceConfigurationResult.java | 22 ++ ...ateExternalServiceConfigurationAction.java | 104 ++++++ ...ateExternalServiceConfigurationResult.java | 14 + .../java/org/zstack/testlib/ApiHelper.groovy | 110 ++++++ 43 files changed, 1993 insertions(+), 9 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java create mode 100644 header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java create mode 100644 sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 988b6ff5be3..74a1b143f9f 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -33,3 +33,14 @@ WHERE `opaque` IS NOT NULL AND `endTime` IS NULL AND Json_getKeyValue(`opaque`, 'end_time') IS NOT NULL AND Json_getKeyValue(`opaque`, 'end_time') != ''; + +-- Add ExternalServiceConfiguration table +CREATE TABLE IF NOT EXISTS `zstack`.`ExternalServiceConfigurationVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `serviceType` varchar(32) NOT NULL, + `configuration` text DEFAULT NULL, + `description` varchar(2048) DEFAULT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/conf/persistence.xml b/conf/persistence.xml index 30c36210dd6..b66d6319ff7 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -223,5 +223,6 @@ org.zstack.header.network.l3.L3NetworkSequenceNumberVO org.zstack.network.hostNetworkInterface.PhysicalSwitchVO org.zstack.network.hostNetworkInterface.PhysicalSwitchPortVO + org.zstack.header.core.external.service.ExternalServiceConfigurationVO diff --git a/conf/serviceConfig/externalService.xml b/conf/serviceConfig/externalService.xml index cb2f6c05d86..ae96366aa8a 100644 --- a/conf/serviceConfig/externalService.xml +++ b/conf/serviceConfig/externalService.xml @@ -9,4 +9,21 @@ org.zstack.header.core.external.service.APIReloadExternalServiceMsg + + + org.zstack.header.core.external.service.APIAddExternalServiceConfigurationMsg + + + + org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationMsg + query + + + + org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationMsg + + + + org.zstack.header.core.external.service.APIDeleteExternalServiceConfigurationMsg + diff --git a/core/src/main/java/org/zstack/core/externalservice/ExternalService.java b/core/src/main/java/org/zstack/core/externalservice/ExternalService.java index 2b816f00337..01f29f90f1e 100755 --- a/core/src/main/java/org/zstack/core/externalservice/ExternalService.java +++ b/core/src/main/java/org/zstack/core/externalservice/ExternalService.java @@ -16,4 +16,10 @@ public interface ExternalService { ExternalServiceCapabilities getExternalServiceCapabilities(); void reload(); + + String getServiceType(); + + default void externalConfig(String serviceType) { + // no-op by default + }; } diff --git a/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java b/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java index 4f4ad548dad..4d39ffc4888 100755 --- a/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java +++ b/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java @@ -1,19 +1,36 @@ package org.zstack.core.externalservice; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.CoreGlobalProperty; +import org.zstack.core.GlobalProperty; +import org.zstack.core.Platform; +import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.thread.ChainTask; +import org.zstack.core.thread.SyncTaskChain; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; -import org.zstack.header.core.external.service.APIGetExternalServicesMsg; -import org.zstack.header.core.external.service.APIGetExternalServicesReply; -import org.zstack.header.core.external.service.APIReloadExternalServiceEvent; -import org.zstack.header.core.external.service.APIReloadExternalServiceMsg; -import org.zstack.header.core.external.service.ExternalServiceInventory; -import org.zstack.header.core.external.service.ExternalServiceStatus; +import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.core.WhileDoneCompletion; +import org.zstack.header.core.external.service.*; +import org.zstack.header.core.workflow.*; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.ErrorCodeList; import org.zstack.header.errorcode.OperationFailureException; +import org.zstack.header.managementnode.ManagementNodeVO; +import org.zstack.header.managementnode.ManagementNodeVO_; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; +import org.zstack.header.message.MessageReply; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -24,6 +41,10 @@ public class ExternalServiceManagerImpl extends AbstractService implements ExternalServiceManager { @Autowired public CloudBus bus; + @Autowired + private DatabaseFacade dbf; + @Autowired + private ThreadFacade thdf; private final Map services = new ConcurrentHashMap<>(); @@ -72,8 +93,8 @@ public boolean stop() { public void handleMessage(Message msg) { if (msg instanceof APIMessage) { handleApiMessage((APIMessage) msg); - } else { - bus.dealWithUnknownMessage(msg); + } else { + handleLocalMessage(msg); } } @@ -82,6 +103,20 @@ public void handleApiMessage(APIMessage msg) { handle((APIGetExternalServicesMsg) msg); } else if (msg instanceof APIReloadExternalServiceMsg) { handle((APIReloadExternalServiceMsg) msg); + } else if (msg instanceof APIAddExternalServiceConfigurationMsg){ + handle((APIAddExternalServiceConfigurationMsg) msg); + } else if (msg instanceof APIUpdateExternalServiceConfigurationMsg) { + handle((APIUpdateExternalServiceConfigurationMsg) msg); + } else if (msg instanceof APIDeleteExternalServiceConfigurationMsg) { + handle((APIDeleteExternalServiceConfigurationMsg) msg); + } else { + bus.dealWithUnknownMessage(msg); + } + } + + private void handleLocalMessage(Message msg) { + if (msg instanceof ApplyExternalServiceConfigurationMsg) { + handle((ApplyExternalServiceConfigurationMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -118,12 +153,287 @@ private void handle(APIGetExternalServicesMsg msg) { inv.setName(name); inv.setStatus(service.isAlive() ? ExternalServiceStatus.RUNNING.toString() : ExternalServiceStatus.STOPPED.toString()); inv.setCapabilities(service.getExternalServiceCapabilities()); + inv.setServiceType(service.getServiceType()); reply.getInventories().add(inv); }); bus.reply(msg, reply); } + private void handle(APIAddExternalServiceConfigurationMsg msg ){ + APIAddExternalServiceConfigurationEvent event = new APIAddExternalServiceConfigurationEvent(msg.getId()); + + thdf.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + createExternalServiceConfiguration(msg, event, new Completion(chain) { + @Override + public void success() { + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + event.setError(errorCode); + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getSyncSignature() { + return String.format("create-update-delete-external-service-configuration-%s", msg.getExternalServiceType()); + } + + @Override + public String getName() { + return String.format("create-external-service-configuration-type-%s", msg.getExternalServiceType()); + } + }); + + } + + private void createExternalServiceConfiguration(APIAddExternalServiceConfigurationMsg msg, APIAddExternalServiceConfigurationEvent evt, Completion completion) { + // create db record + ExternalServiceConfigurationVO configurationVO = new ExternalServiceConfigurationVO(); + configurationVO.setUuid(msg.getResourceUuid() != null ? msg.getResourceUuid() : Platform.getUuid()); + configurationVO.setServiceType(msg.getExternalServiceType()); + configurationVO.setConfiguration(msg.getConfiguration()); + configurationVO.setDescription(msg.getDescription()); + configurationVO = dbf.persistAndRefresh(configurationVO); + + ExternalServiceConfigurationInventory inv = ExternalServiceConfigurationInventory.valueOf(configurationVO); + + applyExternalServiceConfigurationToAllNodes(configurationVO.getServiceType(), new ReturnValueCompletion>(completion) { + @Override + public void success(List returnValue) { + evt.setInventory(inv); + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + private void handle(APIUpdateExternalServiceConfigurationMsg msg){ + APIUpdateExternalServiceConfigurationEvent event = new APIUpdateExternalServiceConfigurationEvent(msg.getId()); + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + final String syncKey = vo != null ? vo.getServiceType() : msg.getUuid(); + + thdf.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + updateExternalServiceConfiguration(msg, event, new Completion(chain) { + @Override + public void success() { + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + event.setError(errorCode); + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getSyncSignature() { + return String.format("create-update-delete-external-service-configuration-%s", syncKey); + } + + @Override + public String getName() { + return String.format("update-external-service-configuration-%s", msg.getUuid()); + } + }); + } + + private void updateExternalServiceConfiguration(APIUpdateExternalServiceConfigurationMsg msg, APIUpdateExternalServiceConfigurationEvent evt, Completion completion) { + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + + if (vo == null) { + completion.fail(operr("unable to find external service configuration with uuid [%s]", msg.getUuid())); + return; + } + + if (msg.getDescription() != null) { + vo.setDescription(msg.getDescription()); + } + vo = dbf.updateAndRefresh(vo); + + evt.setInventory(ExternalServiceConfigurationInventory.valueOf(vo)); + completion.success(); + } + + private void handle(APIDeleteExternalServiceConfigurationMsg msg) { + APIDeleteExternalServiceConfigurationEvent event = new APIDeleteExternalServiceConfigurationEvent(msg.getId()); + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + final String syncKey = vo != null ? vo.getServiceType() : msg.getUuid(); + + thdf.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + deleteExternalServiceConfiguration(msg, event, new Completion(chain) { + @Override + public void success() { + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + event.setError(errorCode); + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getSyncSignature() { + return String.format("create-update-delete-external-service-configuration-%s", syncKey); + } + + @Override + public String getName() { + return String.format("delete-external-service-configuration-%s", msg.getUuid()); + } + }); + } + + private void deleteExternalServiceConfiguration(APIDeleteExternalServiceConfigurationMsg msg, APIDeleteExternalServiceConfigurationEvent evt, Completion completion) { + // delete db record + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + String serviceType; + if (vo != null) { + serviceType = vo.getServiceType(); + dbf.remove(vo); + } else { + completion.success(); + return; + } + + applyExternalServiceConfigurationToAllNodes(serviceType, new ReturnValueCompletion>(completion) { + @Override + public void success(List returnValue) { + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + private void applyExternalServiceConfigurationToAllNodes(String serviceType, ReturnValueCompletion> completion) { + final List results = Collections.synchronizedList(new ArrayList<>()); + + FlowChain chain = new SimpleFlowChain(); + chain.setName("apply-external-service-configuration-to-all-nodes"); + chain.then(new Flow() { + String __name__ = "apply-external-service-configuration"; + + @Override + public void run(FlowTrigger trigger, Map data) { + List mnUuids = Q.New(ManagementNodeVO.class).select(ManagementNodeVO_.uuid).listValues(); + + final ErrorCode[] errorCode = new ErrorCode[1]; + new While<>(mnUuids).each((mnUuid, whileCompletion) -> { + ApplyExternalServiceConfigurationMsg amsg = new ApplyExternalServiceConfigurationMsg(); + amsg.setServiceType(serviceType); + bus.makeServiceIdByManagementNodeId(amsg, SERVICE_ID, mnUuid); + bus.send(amsg, new CloudBusCallBack(whileCompletion) { + @Override + public void run(MessageReply reply) { + ApplyExternalConfigurationResult result = new ApplyExternalConfigurationResult(); + result.setManagementNodeUuid(mnUuid); + results.add(result); + + if (!reply.isSuccess()) { + result.setErrorCode(reply.getError()); + errorCode[0] = reply.getError(); + whileCompletion.allDone(); + return; + } + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (errorCode[0] != null) { + trigger.fail(errorCode[0]); + return; + } + trigger.next(); + } + }); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + trigger.rollback(); + } + }).done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(results); + } + }).error(new FlowErrorHandler(completion) { + @Override + public void handle(ErrorCode errCode, Map data) { + completion.fail(errCode); + } + }).start(); + } + + private void handle(ApplyExternalServiceConfigurationMsg msg) { + ApplyExternalServiceConfigurationReply reply = new ApplyExternalServiceConfigurationReply(); + + regenerateExternalServiceConfiguration(msg.getServiceType(), new ReturnValueCompletion(msg) { + @Override + public void success(String returnValue) { + reply.setValue(returnValue); + reply.setManagementNodeUuid(Platform.getManagementServerId()); + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + private void regenerateExternalServiceConfiguration(String serviceType, ReturnValueCompletion completion) { + if (CoreGlobalProperty.UNIT_TEST_ON) { + completion.success(serviceType); + return; + } + for (ExternalService service : services.values()) { + if (serviceType.equals(service.getServiceType())) { + try{ + service.externalConfig(serviceType); + completion.success(serviceType); + } catch (Exception e) { + completion.fail(operr("failed to apply external service configuration for type [%s]: %s", serviceType, e.getMessage())); + } + return; + } + } + completion.fail(operr("unable to find external service type [%s]", serviceType)); + } + @Override public String getId() { return bus.makeLocalServiceId(SERVICE_ID); diff --git a/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java b/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java index 38e061312d4..1ed1d889cce 100755 --- a/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java +++ b/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java @@ -34,6 +34,12 @@ public String getName() { return String.format("cron-job-on-machine-%s", Platform.getManagementServerIp()); } + + @Override + public String getServiceType() { + return "CronJob"; + } + @Override public void start() { if (isAlive()) { diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java new file mode 100644 index 00000000000..7341efed929 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java @@ -0,0 +1,34 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +/** + * @Author: ya.wang + * @Date: 1/15/26 12:48 AM + */ +@RestResponse(allTo = "inventory") +public class APIAddExternalServiceConfigurationEvent extends APIEvent { + + private ExternalServiceConfigurationInventory inventory; + + public APIAddExternalServiceConfigurationEvent() {} + + public APIAddExternalServiceConfigurationEvent(String apiId) { super(apiId);} + + public void setInventory(ExternalServiceConfigurationInventory inventory) {this.inventory = inventory;} + + public ExternalServiceConfigurationInventory getInventory() {return inventory;} + + public static APIAddExternalServiceConfigurationEvent __example__() { + APIAddExternalServiceConfigurationEvent event = new APIAddExternalServiceConfigurationEvent(); + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + + inv.setUuid(uuid()); + inv.setServiceType("Prometheus2"); + inv.setConfiguration("{}"); + event.setInventory(inv); + + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..db890f8acf4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.ExternalServiceConfigurationInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "添加外部服务配置返回" + + ref { + name "inventory" + path "org.zstack.header.core.external.service.APIAddExternalServiceConfigurationEvent.inventory" + desc "外部服务配置详情" + type "ExternalServiceConfigurationInventory" + since "5.5.12" + clz ExternalServiceConfigurationInventory.class + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIAddExternalServiceConfigurationEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..a6983f464db --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java @@ -0,0 +1,66 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APICreateMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; + +/** + * @Author: ya.wang + * @Date: 1/15/26 12:43 AM + */ +@RestRequest( + path = "/external/service/configuration", + method = HttpMethod.POST, + parameterName = "params", + responseClass = APIAddExternalServiceConfigurationEvent.class +) +public class APIAddExternalServiceConfigurationMsg extends APICreateMessage implements APIAuditor { + @APIParam + private String externalServiceType; + @APIParam(maxLength = 65535) + private String configuration; + @APIParam(maxLength = 2048, required = false) + private String description; + + public String getExternalServiceType() { + return externalServiceType; + } + + public void setExternalServiceType(String externalServiceType) { + this.externalServiceType = externalServiceType; + } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + APIAddExternalServiceConfigurationEvent evt = (APIAddExternalServiceConfigurationEvent) rsp; + return new Result(rsp.isSuccess() ? evt.getInventory().getUuid(): "", ExternalServiceConfigurationVO.class); + } + + public static APIAddExternalServiceConfigurationMsg __example__() { + APIAddExternalServiceConfigurationMsg msg = new APIAddExternalServiceConfigurationMsg(); + msg.setExternalServiceType("Prometheus2"); + msg.setConfiguration("{}"); + msg.setDescription("description"); + return msg; + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..2076550ce46 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,94 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIAddExternalServiceConfigurationEvent + +doc { + title "新建外部服务配置" + + category "externalService" + + desc """新建外部服务配置""" + + rest { + request { + url "POST /v1/external/service/configuration" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIAddExternalServiceConfigurationMsg.class + + desc """""" + + params { + + column { + name "externalServiceType" + enclosedIn "params" + desc "外部服务类型, 例如 Prometheus2" + location "body" + type "String" + optional false + since "5.5.12" + } + column { + name "configuration" + enclosedIn "params" + desc "外部服务配置, 使用 json 格式" + location "body" + type "String" + optional false + since "5.5.12" + } + column { + name "description" + enclosedIn "params" + desc "资源的详细描述" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "resourceUuid" + enclosedIn "params" + desc "资源UUID" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "tagUuids" + enclosedIn "params" + desc "标签UUID列表" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.12" + } + } + } + + response { + clz APIAddExternalServiceConfigurationEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java new file mode 100644 index 00000000000..d7ac0bf2f65 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java @@ -0,0 +1,21 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:47 AM + */ +@RestResponse +public class APIDeleteExternalServiceConfigurationEvent extends APIEvent { + public APIDeleteExternalServiceConfigurationEvent() {} + + public APIDeleteExternalServiceConfigurationEvent(String apiId) { super(apiId); } + + public static APIDeleteExternalServiceConfigurationEvent __example__() { + APIDeleteExternalServiceConfigurationEvent event = new APIDeleteExternalServiceConfigurationEvent(); + event.setSuccess(true); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..97e22e78bba --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy @@ -0,0 +1,23 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "删除外部服务配置返回" + + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIDeleteExternalServiceConfigurationEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..b04729437f2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java @@ -0,0 +1,42 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:44 AM + */ +@RestRequest( + path = "/external/service/configuration/{uuid}", + responseClass = APIDeleteExternalServiceConfigurationEvent.class, + method = HttpMethod.DELETE +) +public class APIDeleteExternalServiceConfigurationMsg extends APIDeleteMessage implements APIAuditor { + @APIParam(resourceType = ExternalServiceConfigurationVO.class, successIfResourceNotExisting = true) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new APIAuditor.Result(((APIDeleteExternalServiceConfigurationMsg)msg).getUuid(), ExternalServiceConfigurationVO.class); + } + + public static APIDeleteExternalServiceConfigurationMsg __example__() { + APIDeleteExternalServiceConfigurationMsg msg = new APIDeleteExternalServiceConfigurationMsg(); + msg.setUuid(uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..3b9c86b5ba2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,67 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIDeleteExternalServiceConfigurationEvent + +doc { + title "删除外部服务配置" + + category "externalService" + + desc """删除外部服务配置""" + + rest { + request { + url "DELETE /v1/external/service/configuration/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIDeleteExternalServiceConfigurationMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "" + desc "资源的UUID,唯一标示该资源" + location "url" + type "String" + optional false + since "5.5.12" + } + column { + name "deleteMode" + enclosedIn "" + desc "删除模式(Permissive / Enforcing,Permissive)" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.12" + } + } + } + + response { + clz APIDeleteExternalServiceConfigurationEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..1ec870e2b86 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java @@ -0,0 +1,24 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.query.APIQueryMessage; +import org.zstack.header.query.AutoQuery; +import org.zstack.header.rest.RestRequest; + +import java.util.Collections; +import java.util.List; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:36 AM + */ +@AutoQuery(replyClass = APIQueryExternalServiceConfigurationReply.class, inventoryClass = ExternalServiceConfigurationInventory.class) +@RestRequest( + path = "/external/service/configuration", + optionalPaths = {"/external/service/configuration/{uuid}"}, + method = HttpMethod.GET, + responseClass = APIQueryExternalServiceConfigurationReply.class +) +public class APIQueryExternalServiceConfigurationMsg extends APIQueryMessage { + public static List __example__() {return Collections.singletonList("uuid=" + uuid());} +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..b66929a1286 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,31 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationReply +import org.zstack.header.query.APIQueryMessage + +doc { + title "查询外部服务配置" + + category "externalService" + + desc """查询外部服务配置""" + + rest { + request { + url "GET /v1/external/service/configuration" + url "GET /v1/external/service/configuration/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIQueryExternalServiceConfigurationMsg.class + + desc """""" + + params APIQueryMessage.class + } + + response { + clz APIQueryExternalServiceConfigurationReply.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java new file mode 100644 index 00000000000..87456312eeb --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.query.APIQueryReply; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +import static org.zstack.utils.CollectionDSL.list; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:39 AM + */ +@RestResponse(allTo = "inventories") +public class APIQueryExternalServiceConfigurationReply extends APIQueryReply { + private List inventories; + + public List getInventories() {return inventories;} + + public void setInventories(List inventories) {this.inventories = inventories;} + + public static APIQueryExternalServiceConfigurationReply __example__() { + APIQueryExternalServiceConfigurationReply reply = new APIQueryExternalServiceConfigurationReply(); + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + + inv.setUuid(uuid()); + inv.setServiceType("Prometheus2"); + inv.setConfiguration("{}"); + reply.setInventories(list(inv)); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy new file mode 100644 index 00000000000..dfeb4e14316 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.ExternalServiceConfigurationInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "外部服务配置清单" + + ref { + name "inventories" + path "org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationReply.inventories" + desc "null" + type "List" + since "5.5.12" + clz ExternalServiceConfigurationInventory.class + } + field { + name "success" + desc "" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationReply.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java new file mode 100644 index 00000000000..c7e5dae73b5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java @@ -0,0 +1,30 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:53 AM + */ +@RestResponse(allTo = "inventory") +public class APIUpdateExternalServiceConfigurationEvent extends APIEvent { + private ExternalServiceConfigurationInventory inventory; + + public APIUpdateExternalServiceConfigurationEvent() {} + + public APIUpdateExternalServiceConfigurationEvent(String apiId) { super(apiId); } + + public ExternalServiceConfigurationInventory getInventory() {return inventory;} + + public void setInventory(ExternalServiceConfigurationInventory inventory) {this.inventory = inventory;} + + public static APIUpdateExternalServiceConfigurationEvent __example__() { + APIUpdateExternalServiceConfigurationEvent event = new APIUpdateExternalServiceConfigurationEvent(); + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + + inv.setUuid(uuid()); + event.setInventory(inv); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..43ef3bfc0fa --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.ExternalServiceConfigurationInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "更新外部服务配置" + + ref { + name "inventory" + path "org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationEvent.inventory" + desc "null" + type "ExternalServiceConfigurationInventory" + since "5.5.12" + clz ExternalServiceConfigurationInventory.class + } + field { + name "success" + desc "" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..1fd29bd06b4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java @@ -0,0 +1,54 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:49 AM + */ +@RestRequest( + path = "/external/service/configuration/{uuid}", + isAction = true, + method = HttpMethod.PUT, + responseClass = APIUpdateExternalServiceConfigurationEvent.class +) +public class APIUpdateExternalServiceConfigurationMsg extends APIMessage implements APIAuditor { + @APIParam(resourceType = ExternalServiceConfigurationVO.class, maxLength = 32, operationTarget = true) + private String uuid; + + @APIParam(maxLength = 2048, required = false) + private String description; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new APIAuditor.Result(((APIUpdateExternalServiceConfigurationMsg)msg).getUuid(), ExternalServiceConfigurationVO.class); + } + + public static APIUpdateExternalServiceConfigurationMsg __example__() { + APIUpdateExternalServiceConfigurationMsg msg = new APIUpdateExternalServiceConfigurationMsg(); + msg.setUuid(uuid()); + msg.setDescription("description"); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..b38094f7cb1 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,67 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationEvent + +doc { + title "UpdateExternalServiceConfiguration" + + category "externalService" + + desc """在这里填写API描述""" + + rest { + request { + url "PUT /v1/external/service/configuration/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIUpdateExternalServiceConfigurationMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "updateExternalServiceConfiguration" + desc "资源的UUID,唯一标示该资源" + location "url" + type "String" + optional false + since "5.5.12" + } + column { + name "description" + enclosedIn "updateExternalServiceConfiguration" + desc "资源的详细描述" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.12" + } + } + } + + response { + clz APIUpdateExternalServiceConfigurationEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java new file mode 100644 index 00000000000..66a75a20cc9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java @@ -0,0 +1,39 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.errorcode.ErrorCode; + +/** + * @Author: ya.wang + * @Date: 1/15/26 2:50 AM + */ +public class ApplyExternalConfigurationResult { + + private String managementNodeUuid; + private ErrorCode errorCode; + private boolean success = true; + + public String getManagementNodeUuid() { + return managementNodeUuid; + } + + public void setManagementNodeUuid(String managementNodeUuid) { + this.managementNodeUuid = managementNodeUuid; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public void setErrorCode(ErrorCode errorCode) { + this.success = false; + this.errorCode = errorCode; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..dd82db96f72 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java @@ -0,0 +1,19 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.NeedReplyMessage; + +/** + * @Author: ya.wang + * @Date: 1/15/26 2:59 AM + */ +public class ApplyExternalServiceConfigurationMsg extends NeedReplyMessage { + private String serviceType; + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java new file mode 100644 index 00000000000..08c25e04266 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java @@ -0,0 +1,28 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.MessageReply; + +/** + * @Author: ya.wang + * @Date: 1/15/26 3:26 AM + */ +public class ApplyExternalServiceConfigurationReply extends MessageReply { + private String managementNodeUuid; + private String value; + + public String getManagementNodeUuid() { + return managementNodeUuid; + } + + public void setManagementNodeUuid(String managementNodeUuid) { + this.managementNodeUuid = managementNodeUuid; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java new file mode 100644 index 00000000000..2ed907dc61d --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java @@ -0,0 +1,89 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.search.Inventory; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:31 AM + */ +@Inventory(mappingVOClass = ExternalServiceConfigurationVO.class) +public class ExternalServiceConfigurationInventory { + private String uuid; + private String serviceType; + private String configuration; + private String description; + private Timestamp createDate; + private Timestamp lastOpDate; + + public static ExternalServiceConfigurationInventory valueOf(ExternalServiceConfigurationVO vo) { + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + inv.setUuid(vo.getUuid()); + inv.setDescription(vo.getDescription()); + inv.setServiceType(vo.getServiceType()); + inv.setConfiguration(vo.getConfiguration()); + inv.setCreateDate(vo.getCreateDate()); + inv.setLastOpDate(vo.getLastOpDate()); + return inv; + } + + public static List valueOf(Collection vos) { + List invs = new ArrayList(); + for (ExternalServiceConfigurationVO vo : vos) { + invs.add(valueOf(vo)); + } + return invs; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..008bd93c094 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy @@ -0,0 +1,45 @@ +package org.zstack.header.core.external.service + +import java.sql.Timestamp + +doc { + + title "外部服务配置" + + field { + name "uuid" + desc "资源的UUID,唯一标示该资源" + type "String" + since "5.5.12" + } + field { + name "serviceType" + desc "外部服务类型, 如 Prometheus2, FluentBitServer" + type "String" + since "5.5.12" + } + field { + name "configuration" + desc "外部服务配置, 使用 json 格式" + type "String" + since "5.5.12" + } + field { + name "description" + desc "资源的详细描述" + type "String" + since "5.5.12" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.5.12" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.5.12" + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java new file mode 100644 index 00000000000..86f6ade106d --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java @@ -0,0 +1,72 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vo.ToInventory; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import java.sql.Timestamp; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:25 AM + */ +@Entity +@Table +public class ExternalServiceConfigurationVO extends ResourceVO implements ToInventory { + @Column + private String serviceType; + @Column + private String configuration; + @Column + private String description; + @Column + private Timestamp createDate; + @Column + private Timestamp lastOpDate; + + public String getServiceType() { + return serviceType; + } + + @PreUpdate + private void preUpdate() { lastOpDate = null; } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java new file mode 100644 index 00000000000..6636e32b1c9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java @@ -0,0 +1,18 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.vo.ResourceVO_; + +import javax.persistence.metamodel.SingularAttribute; +import java.sql.Timestamp; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:30 AM + */ +public class ExternalServiceConfigurationVO_ extends ResourceVO_ { + public static volatile SingularAttribute serviceType; + public static volatile SingularAttribute configuration; + public static volatile SingularAttribute description; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute lastOpDate; +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java index e1b14c73baa..bddfad86a84 100644 --- a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java @@ -4,6 +4,7 @@ public class ExternalServiceInventory { private String name; private String status; private ExternalServiceCapabilities capabilities; + private String serviceType; public String getName() { return name; @@ -29,6 +30,14 @@ public void setCapabilities(ExternalServiceCapabilities capabilities) { this.capabilities = capabilities; } + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + public static ExternalServiceInventory __example__() { ExternalServiceInventory inv = new ExternalServiceInventory(); inv.setName("prometheus"); @@ -36,6 +45,7 @@ public static ExternalServiceInventory __example__() { ExternalServiceCapabilities cap = new ExternalServiceCapabilities(); cap.setReloadConfig(true); inv.setCapabilities(cap); + inv.setServiceType("Prometheus2"); return inv; } } diff --git a/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java b/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java index 21b5c6b03e0..4537d62ee06 100644 --- a/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java +++ b/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java @@ -9,7 +9,11 @@ public void permissions() { permissionBuilder() .adminOnlyAPIs( APIGetExternalServicesMsg.class, - APIReloadExternalServiceMsg.class + APIReloadExternalServiceMsg.class, + APIAddExternalServiceConfigurationMsg.class, + APIQueryExternalServiceConfigurationMsg.class, + APIUpdateExternalServiceConfigurationMsg.class, + APIDeleteExternalServiceConfigurationMsg.class ).build(); } diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 46a1ce8c961..bb413e5ac31 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -245,6 +245,7 @@ public class SourceClassMap { put("org.zstack.header.console.ConsoleProxyAgentInventory", "org.zstack.sdk.ConsoleProxyAgentInventory"); put("org.zstack.header.core.external.plugin.PluginDriverInventory", "org.zstack.sdk.PluginDriverInventory"); put("org.zstack.header.core.external.service.ExternalServiceCapabilities", "org.zstack.sdk.ExternalServiceCapabilities"); + put("org.zstack.header.core.external.service.ExternalServiceConfigurationInventory", "org.zstack.sdk.ExternalServiceConfigurationInventory"); put("org.zstack.header.core.external.service.ExternalServiceInventory", "org.zstack.sdk.ExternalServiceInventory"); put("org.zstack.header.core.progress.ChainInfo", "org.zstack.sdk.ChainInfo"); put("org.zstack.header.core.progress.PendingTaskInfo", "org.zstack.sdk.PendingTaskInfo"); @@ -1064,6 +1065,7 @@ public class SourceClassMap { put("org.zstack.sdk.ExternalPrimaryStorageInventory", "org.zstack.header.storage.addon.primary.ExternalPrimaryStorageInventory"); put("org.zstack.sdk.ExternalServiceCapabilities", "org.zstack.header.core.external.service.ExternalServiceCapabilities"); put("org.zstack.sdk.ExternalServiceCapabilitiesBuilder", "org.zstack.core.externalservice.ExternalServiceCapabilitiesBuilder"); + put("org.zstack.sdk.ExternalServiceConfigurationInventory", "org.zstack.header.core.external.service.ExternalServiceConfigurationInventory"); put("org.zstack.sdk.ExternalServiceInventory", "org.zstack.header.core.external.service.ExternalServiceInventory"); put("org.zstack.sdk.FaultToleranceVmGroupInventory", "org.zstack.faulttolerance.entity.FaultToleranceVmGroupInventory"); put("org.zstack.sdk.FcHbaDeviceInventory", "org.zstack.storage.device.hba.FcHbaDeviceInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..2a7bf9f9df1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class AddExternalServiceConfigurationAction 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.AddExternalServiceConfigurationResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String externalServiceType; + + @Param(required = true, maxLength = 65535, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String configuration; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @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.AddExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.AddExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.AddExternalServiceConfigurationResult() : 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 = "/external/service/configuration"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..743bd847d7e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.ExternalServiceConfigurationInventory; + +public class AddExternalServiceConfigurationResult { + public ExternalServiceConfigurationInventory inventory; + public void setInventory(ExternalServiceConfigurationInventory inventory) { + this.inventory = inventory; + } + public ExternalServiceConfigurationInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..3f66ea0b137 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DeleteExternalServiceConfigurationAction 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.DeleteExternalServiceConfigurationResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @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.DeleteExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.DeleteExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.DeleteExternalServiceConfigurationResult() : 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 = "DELETE"; + info.path = "/external/service/configuration/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..22f9163d915 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class DeleteExternalServiceConfigurationResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java b/sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java new file mode 100644 index 00000000000..05d6ca9e276 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk; + + + +public class ExternalServiceConfigurationInventory { + + 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 serviceType; + public void setServiceType(java.lang.String serviceType) { + this.serviceType = serviceType; + } + public java.lang.String getServiceType() { + return this.serviceType; + } + + public java.lang.String configuration; + public void setConfiguration(java.lang.String configuration) { + this.configuration = configuration; + } + public java.lang.String getConfiguration() { + return this.configuration; + } + + 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.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/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java b/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java index 8882e2f1f49..7f57fa5da36 100644 --- a/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java @@ -28,4 +28,12 @@ public ExternalServiceCapabilities getCapabilities() { return this.capabilities; } + public java.lang.String serviceType; + public void setServiceType(java.lang.String serviceType) { + this.serviceType = serviceType; + } + public java.lang.String getServiceType() { + return this.serviceType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..069f3a8770f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryExternalServiceConfigurationAction extends QueryAction { + + 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.QueryExternalServiceConfigurationResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.QueryExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryExternalServiceConfigurationResult() : 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 = "GET"; + info.path = "/external/service/configuration"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..4697f0cc1fb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryExternalServiceConfigurationResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..c91d30ed13d --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateExternalServiceConfigurationAction 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.UpdateExternalServiceConfigurationResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, maxLength = 32, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @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.UpdateExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.UpdateExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.UpdateExternalServiceConfigurationResult() : 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 = "PUT"; + info.path = "/external/service/configuration/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateExternalServiceConfiguration"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..e00d4bc9fcb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.ExternalServiceConfigurationInventory; + +public class UpdateExternalServiceConfigurationResult { + public ExternalServiceConfigurationInventory inventory; + public void setInventory(ExternalServiceConfigurationInventory inventory) { + this.inventory = inventory; + } + public ExternalServiceConfigurationInventory getInventory() { + return this.inventory; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index f8470e35bd3..396e175189c 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -1421,6 +1421,33 @@ abstract class ApiHelper { } + def addExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.AddExternalServiceConfigurationAction() + 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 addFiSecSecurityMachine(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddFiSecSecurityMachineAction.class) Closure c) { def a = new org.zstack.sdk.AddFiSecSecurityMachineAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -14489,6 +14516,33 @@ abstract class ApiHelper { } + def deleteExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DeleteExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.DeleteExternalServiceConfigurationAction() + 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 deleteFirewall(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DeleteFirewallAction.class) Closure c) { def a = new org.zstack.sdk.DeleteFirewallAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -30778,6 +30832,35 @@ abstract class ApiHelper { } + def queryExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.QueryExternalServiceConfigurationAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + 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 queryFaultToleranceVm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryFaultToleranceVmAction.class) Closure c) { def a = new org.zstack.sdk.QueryFaultToleranceVmAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -44154,6 +44237,33 @@ abstract class ApiHelper { } + def updateExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.UpdateExternalServiceConfigurationAction() + 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 updateFactoryModeState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateFactoryModeStateAction.class) Closure c) { def a = new org.zstack.sdk.UpdateFactoryModeStateAction() a.sessionId = Test.currentEnvSpec?.session?.uuid From 241090b39a4988e735b8cfc3a5fbca92db55f528 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 10 Mar 2026 15:28:42 +0800 Subject: [PATCH 11/77] [longjob]: standardize LongJob progress detail format Add LongJobProgressDetail POJO and LongJobProgressDetailBuilder to normalize opaque progress data into unified typed structure. Three opaque formats parsed: VM migration, AI download, unknown. DB schema unchanged. Agent-side migration deferred to Phase 2. Resolves: ZSTAC-82318 Change-Id: I70d60ff5e6c8f659f55770e2fbbe56781b238fd5 --- .../core/progress/ProgressReportService.java | 3 + .../progress/APIGetTaskProgressReply.java | 12 + .../core/progress/LongJobProgressDetail.java | 135 ++++++++++ .../LongJobProgressDetailBuilder.java | 249 ++++++++++++++++++ .../core/progress/TaskProgressInventory.java | 10 + .../LongJobProgressNotificationMessage.java | 14 + .../org/zstack/sdk/LongJobProgressDetail.java | 95 +++++++ .../org/zstack/sdk/TaskProgressInventory.java | 8 + 8 files changed, 526 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java create mode 100644 header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java create mode 100644 sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java diff --git a/core/src/main/java/org/zstack/core/progress/ProgressReportService.java b/core/src/main/java/org/zstack/core/progress/ProgressReportService.java index bab9acc163a..dc9d93ea241 100755 --- a/core/src/main/java/org/zstack/core/progress/ProgressReportService.java +++ b/core/src/main/java/org/zstack/core/progress/ProgressReportService.java @@ -225,6 +225,9 @@ private TaskProgressInventory inventory(TaskProgressVO vo) { if (!StringUtils.isEmpty(vo.getArguments())) { inv.setArguments(vo.getArguments()); } + + inv.setProgressDetail(LongJobProgressDetailBuilder.fromTaskProgressVO(vo)); + return inv; } diff --git a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java index 44e27d0b8e3..880e8cda1a8 100755 --- a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java +++ b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java @@ -6,6 +6,7 @@ import java.util.List; import static java.util.Arrays.asList; + /** * Created by xing5 on 2017/3/21. */ @@ -29,6 +30,17 @@ public static APIGetTaskProgressReply __example__() { inv.setTaskUuid("931102503f64436ea649939ff3957406"); inv.setTime(DocUtils.date); inv.setType("Task"); + + LongJobProgressDetail detail = new LongJobProgressDetail(); + detail.setPercent(42); + detail.setStage("downloading"); + detail.setState("running"); + detail.setProcessedBytes(440401920L); + detail.setTotalBytes(1073741824L); + detail.setSpeedBytesPerSecond(10485760L); + detail.setEstimatedRemainingSeconds(60L); + inv.setProgressDetail(detail); + msg.setInventories(asList(inv)); return msg; } diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java new file mode 100644 index 00000000000..d27dc05cc4c --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java @@ -0,0 +1,135 @@ +package org.zstack.header.core.progress; + +import java.util.Map; + +/** + * Standardized LongJob progress detail, parsed from TaskProgressVO.opaque. + * + * All fields are optional (nullable). Callers should null-check before use. + * This is a pure read-only view — the database schema (TaskProgressVO) is unchanged. + */ +public class LongJobProgressDetail { + /** Progress percentage 0-100, if known. */ + private Integer percent; + + /** Human-readable stage label, e.g. "downloading", "extracting". */ + private String stage; + + /** State identifier, e.g. "running", "paused". */ + private String state; + + /** Human-readable reason for current state. */ + private String stateReason; + + /** Bytes already processed. */ + private Long processedBytes; + + /** Total bytes to process. */ + private Long totalBytes; + + /** Items already processed (e.g. files, chunks). */ + private Long processedItems; + + /** Total items to process. */ + private Long totalItems; + + /** Transfer speed in bytes/s. */ + private Long speedBytesPerSecond; + + /** Estimated remaining time in seconds. */ + private Long estimatedRemainingSeconds; + + /** + * Catch-all for any opaque fields that don't map to the standard schema. + * Preserves unknown keys so no data is silently dropped. + */ + private Map extra; + + public Integer getPercent() { + return percent; + } + + public void setPercent(Integer percent) { + this.percent = percent; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getStateReason() { + return stateReason; + } + + public void setStateReason(String stateReason) { + this.stateReason = stateReason; + } + + public Long getProcessedBytes() { + return processedBytes; + } + + public void setProcessedBytes(Long processedBytes) { + this.processedBytes = processedBytes; + } + + public Long getTotalBytes() { + return totalBytes; + } + + public void setTotalBytes(Long totalBytes) { + this.totalBytes = totalBytes; + } + + public Long getProcessedItems() { + return processedItems; + } + + public void setProcessedItems(Long processedItems) { + this.processedItems = processedItems; + } + + public Long getTotalItems() { + return totalItems; + } + + public void setTotalItems(Long totalItems) { + this.totalItems = totalItems; + } + + public Long getSpeedBytesPerSecond() { + return speedBytesPerSecond; + } + + public void setSpeedBytesPerSecond(Long speedBytesPerSecond) { + this.speedBytesPerSecond = speedBytesPerSecond; + } + + public Long getEstimatedRemainingSeconds() { + return estimatedRemainingSeconds; + } + + public void setEstimatedRemainingSeconds(Long estimatedRemainingSeconds) { + this.estimatedRemainingSeconds = estimatedRemainingSeconds; + } + + public Map getExtra() { + return extra; + } + + public void setExtra(Map extra) { + this.extra = extra; + } +} diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java new file mode 100644 index 00000000000..18a436fc280 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java @@ -0,0 +1,249 @@ +package org.zstack.header.core.progress; + +import org.zstack.utils.Utils; +import org.zstack.utils.gson.JSONObjectUtil; +import org.zstack.utils.logging.CLogger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parses TaskProgressVO.opaque (free-form JSON) into a typed LongJobProgressDetail. + * + * Three known opaque formats are handled: + * Format 1 — VM migration: {"remain":N, "total":N, "speed":N, "remaining_migration_time":N} + * Format 2 — AI download: {"data": ""} where the inner JSON has + * {state, progress:{percent,downloaded_bytes,total_bytes, + * speed_bytes_per_second,estimated_remaining_seconds, + * downloaded_files,total_files,stage}, state_reason} + * Format 3 — unknown: entire map goes into LongJobProgressDetail.extra + * + * Each format is tried independently. Failures in one format don't affect others. + */ +public class LongJobProgressDetailBuilder { + private static final CLogger logger = Utils.getLogger(LongJobProgressDetailBuilder.class); + + private LongJobProgressDetailBuilder() {} + + /** + * Build a LongJobProgressDetail from a TaskProgressVO. + * Returns null if opaque is null/empty or all parsers fail. + */ + public static LongJobProgressDetail fromTaskProgressVO(TaskProgressVO vo) { + if (vo == null || vo.getOpaque() == null || vo.getOpaque().isEmpty()) { + return null; + } + + Map raw; + try { + raw = JSONObjectUtil.toObject(vo.getOpaque(), HashMap.class); + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: opaque is not a JSON object, skipping: " + vo.getOpaque(), e); + return null; + } + + if (raw == null || raw.isEmpty()) { + return null; + } + + // Try Format 2 first: AI download wraps everything under "data" key + if (raw.containsKey("data")) { + LongJobProgressDetail detail = tryParseAiDownloadFormat(raw); + if (detail != null) { + return detail; + } + } + + // Try Format 1: VM migration with remain/total/speed keys + if (raw.containsKey("remain") && raw.containsKey("total")) { + LongJobProgressDetail detail = tryParseVmMigrationFormat(raw); + if (detail != null) { + return detail; + } + } + + // Format 3: unknown — put everything into extra + return parseAsExtra(raw); + } + + /** + * Format 1: VM migration opaque + * {"remain": 1234567, "total": 9999999, "speed": 102400, "remaining_migration_time": 30} + * remain = bytes still to transfer; processed = total - remain + */ + private static LongJobProgressDetail tryParseVmMigrationFormat(Map raw) { + try { + LongJobProgressDetail detail = new LongJobProgressDetail(); + detail.setStage("migrating"); + + Number total = toNumber(raw.get("total")); + Number remain = toNumber(raw.get("remain")); + Number speed = toNumber(raw.get("speed")); + Number remainingTime = toNumber(raw.get("remaining_migration_time")); + + if (total != null) { + detail.setTotalBytes(total.longValue()); + } + if (total != null && remain != null) { + long processed = Math.max(0L, total.longValue() - remain.longValue()); + detail.setProcessedBytes(processed); + if (total.longValue() > 0) { + detail.setPercent((int) Math.min(100, Math.round(processed * 100.0 / total.longValue()))); + } + } + if (speed != null) { + detail.setSpeedBytesPerSecond(speed.longValue()); + } + if (remainingTime != null) { + detail.setEstimatedRemainingSeconds(remainingTime.longValue()); + } + + // Carry over any unrecognized keys into extra + Map extra = new HashMap<>(raw); + extra.remove("remain"); + extra.remove("total"); + extra.remove("speed"); + extra.remove("remaining_migration_time"); + if (!extra.isEmpty()) { + detail.setExtra(extra); + } + + return detail; + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: failed to parse VM migration format", e); + return null; + } + } + + /** + * Format 2: AI download opaque + * {"data": "{\"state\":\"downloading\", \"progress\":{\"percent\":42, \"processedBytes\":N, ...}}"} + * The "data" value is a JSON string (double-encoded). + */ + private static LongJobProgressDetail tryParseAiDownloadFormat(Map raw) { + try { + Object dataVal = raw.get("data"); + if (dataVal == null) { + return null; + } + + Map inner; + if (dataVal instanceof String) { + // double-encoded JSON string + inner = JSONObjectUtil.toObject((String) dataVal, HashMap.class); + } else if (dataVal instanceof Map) { + inner = (Map) dataVal; + } else { + return null; + } + + if (inner == null) { + return null; + } + + LongJobProgressDetail detail = new LongJobProgressDetail(); + + // state field + Object stateVal = inner.get("state"); + if (stateVal instanceof String) { + detail.setState((String) stateVal); + } + + // progress sub-object + Object progressVal = inner.get("progress"); + if (progressVal instanceof Map) { + Map progress = (Map) progressVal; + + Number percent = toNumber(progress.get("percent")); + if (percent != null) { + detail.setPercent((int) Math.round(percent.doubleValue())); + } + + // AI agent uses snake_case field names + Number processedBytes = toNumber(progress.get("downloaded_bytes")); + if (processedBytes != null) { + detail.setProcessedBytes(processedBytes.longValue()); + } + + Number totalBytes = toNumber(progress.get("total_bytes")); + if (totalBytes != null) { + detail.setTotalBytes(totalBytes.longValue()); + } + + Number speed = toNumber(progress.get("speed_bytes_per_second")); + if (speed != null) { + detail.setSpeedBytesPerSecond(speed.longValue()); + } + + Number eta = toNumber(progress.get("estimated_remaining_seconds")); + if (eta != null) { + detail.setEstimatedRemainingSeconds(eta.longValue()); + } + + Number processedFiles = toNumber(progress.get("downloaded_files")); + if (processedFiles != null) { + detail.setProcessedItems(processedFiles.longValue()); + } + + Number totalFiles = toNumber(progress.get("total_files")); + if (totalFiles != null) { + detail.setTotalItems(totalFiles.longValue()); + } + + Object stage = progress.get("stage"); + if (stage instanceof String) { + detail.setStage((String) stage); + } + + // remaining progress fields go into extra + Map extraProgress = new HashMap<>(progress); + extraProgress.remove("percent"); + extraProgress.remove("downloaded_bytes"); + extraProgress.remove("total_bytes"); + extraProgress.remove("speed_bytes_per_second"); + extraProgress.remove("estimated_remaining_seconds"); + extraProgress.remove("downloaded_files"); + extraProgress.remove("total_files"); + extraProgress.remove("stage"); + if (!extraProgress.isEmpty()) { + detail.setExtra(extraProgress); + } + } + + // stateReason field — can be String or Map (structured reason with code/description) + Object stateReason = inner.get("state_reason"); + if (stateReason instanceof String) { + detail.setStateReason((String) stateReason); + } else if (stateReason instanceof Map) { + detail.setStateReason(JSONObjectUtil.toJsonString(stateReason)); + } + + return detail; + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: failed to parse AI download format", e); + return null; + } + } + + /** + * Format 3: unknown — preserve the entire map as extra for UI passthrough. + */ + private static LongJobProgressDetail parseAsExtra(Map raw) { + LongJobProgressDetail detail = new LongJobProgressDetail(); + detail.setExtra(new HashMap<>(raw)); + return detail; + } + + private static Number toNumber(Object val) { + if (val instanceof Number) { + return (Number) val; + } + if (val instanceof String) { + try { + return Double.parseDouble((String) val); + } catch (NumberFormatException ignored) { + } + } + return null; + } +} diff --git a/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java b/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java index aa8c0f6db7e..b86dcb50eef 100755 --- a/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java +++ b/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java @@ -18,6 +18,8 @@ public class TaskProgressInventory { private Long time; private List subTasks; private String arguments; + /** Typed progress detail parsed from opaque. Null when opaque is absent or unrecognized. */ + private LongJobProgressDetail progressDetail; public TaskProgressInventory() { } @@ -105,4 +107,12 @@ public Long getTime() { public void setTime(Long time) { this.time = time; } + + public LongJobProgressDetail getProgressDetail() { + return progressDetail; + } + + public void setProgressDetail(LongJobProgressDetail progressDetail) { + this.progressDetail = progressDetail; + } } diff --git a/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java b/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java index 802f3b2b3fa..84b9aaf6ef9 100644 --- a/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java +++ b/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java @@ -1,5 +1,7 @@ package org.zstack.header.longjob; +import org.zstack.header.core.progress.LongJobProgressDetail; +import org.zstack.header.core.progress.LongJobProgressDetailBuilder; import org.zstack.header.core.progress.TaskProgressInventory; import org.zstack.header.core.progress.TaskProgressVO; @@ -23,6 +25,8 @@ public enum EventType { private Integer progress; /** Full progress detail; optional, BFF current version only needs {@link #getProgress()}. */ private TaskProgressInventory taskProgress; + /** Standardized progress detail parsed from opaque; null when opaque is absent. */ + private LongJobProgressDetail progressDetail; private EventType eventType; private Long timestamp; @@ -68,6 +72,14 @@ public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } + public LongJobProgressDetail getProgressDetail() { + return progressDetail; + } + + public void setProgressDetail(LongJobProgressDetail progressDetail) { + this.progressDetail = progressDetail; + } + public static LongJobProgressNotificationMessage stateChanged(LongJobVO vo) { LongJobProgressNotificationMessage msg = new LongJobProgressNotificationMessage(); msg.longJob = LongJobInventory.valueOf(vo); @@ -85,7 +97,9 @@ public static LongJobProgressNotificationMessage progressUpdated(LongJobVO vo, T if (progressVO.getContent() != null) { inv.setContent(progressVO.getContent()); } + inv.setProgressDetail(LongJobProgressDetailBuilder.fromTaskProgressVO(progressVO)); msg.taskProgress = inv; + msg.progressDetail = inv.getProgressDetail(); msg.eventType = EventType.PROGRESS_UPDATED; msg.timestamp = System.currentTimeMillis(); return msg; diff --git a/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java new file mode 100644 index 00000000000..365f535bc45 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java @@ -0,0 +1,95 @@ +package org.zstack.sdk; + + + +public class LongJobProgressDetail { + + public java.lang.Integer percent; + public void setPercent(java.lang.Integer percent) { + this.percent = percent; + } + public java.lang.Integer getPercent() { + return this.percent; + } + + public java.lang.String stage; + public void setStage(java.lang.String stage) { + this.stage = stage; + } + public java.lang.String getStage() { + return this.stage; + } + + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + + public java.lang.String stateReason; + public void setStateReason(java.lang.String stateReason) { + this.stateReason = stateReason; + } + public java.lang.String getStateReason() { + return this.stateReason; + } + + public java.lang.Long processedBytes; + public void setProcessedBytes(java.lang.Long processedBytes) { + this.processedBytes = processedBytes; + } + public java.lang.Long getProcessedBytes() { + return this.processedBytes; + } + + public java.lang.Long totalBytes; + public void setTotalBytes(java.lang.Long totalBytes) { + this.totalBytes = totalBytes; + } + public java.lang.Long getTotalBytes() { + return this.totalBytes; + } + + public java.lang.Long processedItems; + public void setProcessedItems(java.lang.Long processedItems) { + this.processedItems = processedItems; + } + public java.lang.Long getProcessedItems() { + return this.processedItems; + } + + public java.lang.Long totalItems; + public void setTotalItems(java.lang.Long totalItems) { + this.totalItems = totalItems; + } + public java.lang.Long getTotalItems() { + return this.totalItems; + } + + public java.lang.Long speedBytesPerSecond; + public void setSpeedBytesPerSecond(java.lang.Long speedBytesPerSecond) { + this.speedBytesPerSecond = speedBytesPerSecond; + } + public java.lang.Long getSpeedBytesPerSecond() { + return this.speedBytesPerSecond; + } + + public java.lang.Long estimatedRemainingSeconds; + public void setEstimatedRemainingSeconds(java.lang.Long estimatedRemainingSeconds) { + this.estimatedRemainingSeconds = estimatedRemainingSeconds; + } + public java.lang.Long getEstimatedRemainingSeconds() { + return this.estimatedRemainingSeconds; + } + + public java.util.LinkedHashMap extra; + public void setExtra(java.util.LinkedHashMap extra) { + this.extra = extra; + } + public java.util.LinkedHashMap getExtra() { + return this.extra; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java b/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java index 31d427d23d4..e22f11655f7 100644 --- a/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java @@ -76,4 +76,12 @@ public java.lang.String getArguments() { return this.arguments; } + public org.zstack.sdk.LongJobProgressDetail progressDetail; + public void setProgressDetail(org.zstack.sdk.LongJobProgressDetail progressDetail) { + this.progressDetail = progressDetail; + } + public org.zstack.sdk.LongJobProgressDetail getProgressDetail() { + return this.progressDetail; + } + } From 12a4db35acfeef5c8edd7d91e9f371f5b2a1ac0a Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Sun, 8 Mar 2026 23:52:21 +0800 Subject: [PATCH 12/77] [lb]: intercept httpCompressAlgos::disable tag Resolves: ZSTAC-81706 Change-Id: I6877637970626766686473657579617664657474 Signed-off-by: AlanJager --- .../network/service/lb/LoadBalancerApiInterceptor.java | 8 ++++++++ .../utils/clouderrorcode/CloudOperationsErrorCode.java | 2 ++ 2 files changed, 10 insertions(+) diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java index 0d9946d5320..c8a1ba8c08c 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java @@ -865,6 +865,14 @@ private void validate(APICreateLoadBalancerListenerMsg msg) { statusCode = LoadBalancerSystemTags.STATUS_CODE.getTokenByTag(tag, LoadBalancerSystemTags.STATUS_CODE_TOKEN); } + if (LoadBalancerSystemTags.HTTP_COMPRESS_ALGOS.isMatch(tag)) { + String compressAlgos = LoadBalancerSystemTags.HTTP_COMPRESS_ALGOS.getTokenByTag(tag, + LoadBalancerSystemTags.HTTP_COMPRESS_ALGOS_TOKEN); + if (DisableLbSupportHttpCompressAlgos.equals(compressAlgos)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10172, + "could not create the loadbalancer listener with systemTag httpCompressAlgos::disable, please remove this tag")); + } + } } if ((redirectPort != null || statusCode != null) && (httpRedirectHttps == null || HttpRedirectHttps.disable.toString().equals(httpRedirectHttps))) { 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 6a4087cae00..aed0e710251 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -13612,6 +13612,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10171 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10171"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10172 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10172"; + public static final String ORG_ZSTACK_IPSEC_10000 = "ORG_ZSTACK_IPSEC_10000"; public static final String ORG_ZSTACK_IPSEC_10001 = "ORG_ZSTACK_IPSEC_10001"; From 708fc5e23c752d0d64bf5aa69cec6d163552cb14 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 10 Mar 2026 21:37:37 +0800 Subject: [PATCH 13/77] [errorcode]: fix i18n gaps in copy ctor and SDK - ErrorCode copy constructor: add missing cost and opaque fields - ZSClient: deserialize message field from JSON response - Add ErrorCodeI18nCase gate test (12 cases) Change-Id: I0002errorcode0i18n0gaps0fix Co-Authored-By: Claude Opus 4.6 --- .../zstack/header/errorcode/ErrorCode.java | 2 + .../main/java/org/zstack/sdk/ZSClient.java | 4 + .../integration/core/ErrorCodeI18nCase.groovy | 130 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy diff --git a/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java b/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java index c7ff98024b3..d180b6b4ccb 100755 --- a/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java +++ b/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java @@ -104,6 +104,8 @@ public ErrorCode(ErrorCode other) { this.message = other.message; this.formatArgs = other.formatArgs == null ? null : other.formatArgs.clone(); this.globalErrorCode = other.globalErrorCode; + this.cost = other.cost; + this.opaque = other.opaque; } public void setCode(String code) { diff --git a/sdk/src/main/java/org/zstack/sdk/ZSClient.java b/sdk/src/main/java/org/zstack/sdk/ZSClient.java index 650369b19ac..3cab7f19134 100755 --- a/sdk/src/main/java/org/zstack/sdk/ZSClient.java +++ b/sdk/src/main/java/org/zstack/sdk/ZSClient.java @@ -121,6 +121,10 @@ public ErrorCode deserialize(JsonElement jsonElement, Type type, JsonDeserializa if (item != null && item.isJsonPrimitive()) { wrapper.setGlobalErrorCode(item.getAsString()); } + item = object.get("message"); + if (item != null && item.isJsonPrimitive()) { + wrapper.setMessage(item.getAsString()); + } return wrapper; } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy new file mode 100644 index 00000000000..a437b1a0a79 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy @@ -0,0 +1,130 @@ +package org.zstack.test.integration.core + +import org.zstack.core.errorcode.LocaleUtils +import org.zstack.header.errorcode.ErrorCode +import org.zstack.testlib.SubCase + +class ErrorCodeI18nCase extends SubCase { + + @Override + void setup() { + INCLUDE_CORE_SERVICES = false + } + + @Override + void environment() { + } + + @Override + void test() { + testLocaleUtilsExactMatch() + testLocaleUtilsBaseLanguageFallback() + testLocaleUtilsQValueSorting() + testLocaleUtilsNullAndEmpty() + testLocaleUtilsNoMatch() + testLocaleUtilsCaseInsensitive() + testLocaleUtilsMalformedHeader() + testErrorCodeCopyConstructor() + testErrorCodeCopyConstructorWithNulls() + } + + @Override + void clean() { + } + + // ---- LocaleUtils ---- + + void testLocaleUtilsExactMatch() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("zh-CN", available) == "zh_CN" + assert LocaleUtils.resolveLocale("en-US", available) == "en_US" + } + + void testLocaleUtilsBaseLanguageFallback() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("en", available) == "en_US" + assert LocaleUtils.resolveLocale("zh", available) == "zh_CN" + } + + void testLocaleUtilsQValueSorting() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("zh-CN,en;q=0.8", available) == "zh_CN" + assert LocaleUtils.resolveLocale("en-US,zh-CN;q=0.5", available) == "en_US" + // q-value should override header order + assert LocaleUtils.resolveLocale("en;q=0.8,zh-CN;q=1.0", available) == "zh_CN" + } + + void testLocaleUtilsNullAndEmpty() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale(null, available) == "en_US" + assert LocaleUtils.resolveLocale("", available) == "en_US" + assert LocaleUtils.resolveLocale(" ", available) == "en_US" + } + + void testLocaleUtilsNoMatch() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("ja-JP,ko-KR", available) == "en_US" + } + + void testLocaleUtilsCaseInsensitive() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("ZH-CN", available) == "zh_CN" + assert LocaleUtils.resolveLocale("EN-US", available) == "en_US" + } + + void testLocaleUtilsMalformedHeader() { + def available = ["zh_CN", "en_US"] as Set + // malformed header should fall back to default + assert LocaleUtils.resolveLocale(";;;,,,", available) == "en_US" + } + + // ---- ErrorCode copy constructor ---- + + void testErrorCodeCopyConstructor() { + def original = new ErrorCode("SYS.1000", "System Error", "something failed") + original.setElaboration("elaboration text") + original.setLocation("org.zstack.Foo:123") + original.setCost("50ms") + original.setGlobalErrorCode("ORG_ZSTACK_FOO_10000") + original.setMessage("系统错误") + original.setFormatArgs(["arg1", "arg2"] as String[]) + + def opaque = new LinkedHashMap() + opaque.put("key1", "value1") + original.setOpaque(opaque) + + def cause = new ErrorCode("INTERNAL.1001", "Internal Error") + original.setCause(cause) + + def copy = new ErrorCode(original) + + assert copy.code == original.code + assert copy.description == original.description + assert copy.details == original.details + assert copy.elaboration == original.elaboration + assert copy.location == original.location + assert copy.cost == original.cost + assert copy.globalErrorCode == original.globalErrorCode + assert copy.message == original.message + assert copy.opaque.is(original.opaque) + assert copy.cause.is(original.cause) + // formatArgs should be cloned, not shared + assert copy.formatArgs == original.formatArgs + assert !copy.formatArgs.is(original.formatArgs) + } + + void testErrorCodeCopyConstructorWithNulls() { + def original = new ErrorCode("SYS.1000", "System Error") + def copy = new ErrorCode(original) + + assert copy.code == original.code + assert copy.description == original.description + assert copy.details == null + assert copy.cost == null + assert copy.opaque == null + assert copy.message == null + assert copy.globalErrorCode == null + assert copy.formatArgs == null + } + +} From 7b302df4bc696b7f8f5266c3fdad9c139eb25cc5 Mon Sep 17 00:00:00 2001 From: "shan.wu" Date: Tue, 11 Nov 2025 14:22:29 +0800 Subject: [PATCH 14/77] [sharedblock]: convert memory snapshot install path from absolute path to protocol path convert memory snapshot install path from absolute path to protocol path Resolves/Related: ZSTAC-79756 Change-Id: I6e626d68626461627a737765786a676e6b617064 --- conf/db/upgrade/V5.5.12__schema.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 74a1b143f9f..bbba706c287 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -44,3 +44,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`ExternalServiceConfigurationVO` ( `createDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`uuid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +UPDATE VolumeSnapshotVO AS sp, PrimaryStorageVO AS ps +SET sp.primaryStorageInstallPath = REPLACE(sp.primaryStorageInstallPath, '/dev/', 'sharedblock://') +WHERE sp.primaryStorageUuid = ps.uuid AND ps.type = 'SharedBlock' AND sp.volumeType = 'Memory' AND sp.primaryStorageInstallPath LIKE '/dev/%'; From f94eff0abdd9c38f90810329cb2ab325965afa49 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Wed, 11 Mar 2026 11:16:08 +0800 Subject: [PATCH 15/77] [core]: handle malformed Accept-Language header in LocaleUtils Java's String.split(";") on ";;;" returns an empty array, causing ArrayIndexOutOfBoundsException in parseAcceptLanguage. Add bounds check before accessing tagAndParams[0]. Change-Id: I$(openssl rand -hex 20) --- .../main/java/org/zstack/core/errorcode/LocaleUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java index 8820eadbfbe..bb1bcbea3a6 100644 --- a/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java +++ b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java @@ -88,7 +88,13 @@ private static List parseAcceptLanguage(String header) { continue; } String[] tagAndParams = part.split(";"); + if (tagAndParams.length == 0) { + continue; + } String tag = tagAndParams[0].trim(); + if (tag.isEmpty()) { + continue; + } double quality = 1.0; for (int i = 1; i < tagAndParams.length; i++) { String param = tagAndParams[i].trim(); From 0bfbf4544c99462ef85235d83c834b4b9b965780 Mon Sep 17 00:00:00 2001 From: "lin.ma" Date: Wed, 4 Feb 2026 18:06:11 +0800 Subject: [PATCH 16/77] [zwatch]: VPC Router CPU alarm use external monitoring Resolves: ZSTAC-81171 Change-Id: I7778676171646874706164777869707279776172 --- conf/db/upgrade/V5.5.12__schema.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index bbba706c287..ea21c75f925 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -48,3 +48,18 @@ CREATE TABLE IF NOT EXISTS `zstack`.`ExternalServiceConfigurationVO` ( UPDATE VolumeSnapshotVO AS sp, PrimaryStorageVO AS ps SET sp.primaryStorageInstallPath = REPLACE(sp.primaryStorageInstallPath, '/dev/', 'sharedblock://') WHERE sp.primaryStorageUuid = ps.uuid AND ps.type = 'SharedBlock' AND sp.volumeType = 'Memory' AND sp.primaryStorageInstallPath LIKE '/dev/%'; + +UPDATE `zstack`.`ActiveAlarmTemplateVO` +SET `metricName` = 'CPUUsedUtilization' +WHERE `uuid` = 'c9e6cdca107140bea62b4ca919ff9e88' + AND `metricName` = 'VRouterCPUAverageUsedUtilization'; + +UPDATE `zstack`.`AlarmVO` +SET `metricName` = 'CPUUsedUtilization' +WHERE `uuid` IN ( + SELECT `alarmUuid` FROM `zstack`.`ActiveAlarmVO` + WHERE `templateUuid` = 'c9e6cdca107140bea62b4ca919ff9e88' +) + AND `metricName` = 'VRouterCPUAverageUsedUtilization'; + + From 2e7d673002cc046c1620a149acd07e8fe6d83c75 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Wed, 11 Mar 2026 20:02:13 +0800 Subject: [PATCH 17/77] [pciDevice]: add Kunlunxin to SDK GpuVendor enum Resolves: ZSTAC-82350 Change-Id: I8770dbf76538bee8c632d06fd4c228368c0612f5 --- sdk/src/main/java/org/zstack/sdk/GpuVendor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/main/java/org/zstack/sdk/GpuVendor.java b/sdk/src/main/java/org/zstack/sdk/GpuVendor.java index 0918b690289..9013b3048ed 100644 --- a/sdk/src/main/java/org/zstack/sdk/GpuVendor.java +++ b/sdk/src/main/java/org/zstack/sdk/GpuVendor.java @@ -9,4 +9,5 @@ public enum GpuVendor { TianShu, Other, Alibaba, + Kunlunxin, } From c7e7de8fe672b0650d9ff6e575dc98eef22fbf26 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Wed, 11 Mar 2026 21:50:35 +0800 Subject: [PATCH 18/77] [test]: fix SG test cases: relax priority consecutive constraints to match new validation Resolves: ZSTAC-79067 Change-Id: I6f134853bf7e16834a3d38df34334ebafd589167 --- .../AddSecurityGroupRuleOptimizedCase.groovy | 23 ++++++++----------- .../ChangeSecurityGroupRuleCase.groovy | 18 ++++++--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy index ed9a2736c5f..5160f0c2d9a 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy @@ -540,7 +540,7 @@ class AddSecurityGroupRuleOptimizedCase extends SubCase { addSecurityGroupRule { securityGroupUuid = sg3.uuid rules = [rule_82] - priority = 82 + priority = 101 } } } @@ -591,13 +591,12 @@ class AddSecurityGroupRuleOptimizedCase extends SubCase { rule_13.protocol = "ALL" rule_13.startPort = -1 rule_13.endPort = -1 - expect(AssertionError) { - addSecurityGroupRule { - securityGroupUuid = sg3.uuid - rules = [rule_13] - priority = 13 - } + sg3 = addSecurityGroupRule { + securityGroupUuid = sg3.uuid + rules = [rule_13] + priority = 13 } + assert sg3.rules.find { it.allowedCidr == rule_13.allowedCidr && it.priority == 13 } != null SecurityGroupRuleAO rule_12 = new SecurityGroupRuleAO() rule_12.dstIpRange = "2.2.2.2-2.2.2.10" @@ -609,12 +608,10 @@ class AddSecurityGroupRuleOptimizedCase extends SubCase { ingressRule.protocol = "TCP" ingressRule.dstPortRange = "12-13" - expect(AssertionError) { - addSecurityGroupRule { - securityGroupUuid = sg3.uuid - rules = [rule_12, ingressRule] - priority = 12 - } + sg3 = addSecurityGroupRule { + securityGroupUuid = sg3.uuid + rules = [rule_12, ingressRule] + priority = 12 } } diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy index ad29787ef1e..844b1ab35d6 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy @@ -231,17 +231,15 @@ class ChangeSecurityGroupRuleCase extends SubCase { assert sg3 != null SecurityGroupRuleInventory rule_1 = sg3.rules.find { it.type == "Ingress" && it.priority == 1 && it.ipVersion == 4 } - expect(AssertionError) { - changeSecurityGroupRule { - uuid = rule_1.uuid - priority = 6 - } + changeSecurityGroupRule { + uuid = rule_1.uuid + priority = 6 } expect(AssertionError) { changeSecurityGroupRule { uuid = rule_1.uuid - priority = 7 + priority = 101 } } } @@ -307,11 +305,9 @@ class ChangeSecurityGroupRuleCase extends SubCase { } } - expect(AssertionError) { - changeSecurityGroupRule { - uuid = rule1.uuid - priority = 3 - } + changeSecurityGroupRule { + uuid = rule1.uuid + priority = 3 } expect(AssertionError) { From 67acc0a0dbeefee159a22b702c804ba3cef7c57c Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Thu, 12 Mar 2026 11:38:27 +0800 Subject: [PATCH 19/77] [core]: support resnotify webhook infrastructure Add CopyOnWriteArrayList for thread-safe entity lifecycle callbacks in DatabaseFacadeImpl. Add uninstallEntityLifeCycleCallback method. Add ResNotify DB schema with FK and indexes. Change-Id: Ia3667e35d74c06796946bb805aba4db5c87a3052 --- conf/db/upgrade/V5.5.12__schema.sql | 26 +++++ .../zstack/core/db/DatabaseFacadeImpl.java | 102 +++++++++++++----- 2 files changed, 101 insertions(+), 27 deletions(-) diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index ea21c75f925..58d0e6ee187 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -62,4 +62,30 @@ WHERE `uuid` IN ( ) AND `metricName` = 'VRouterCPUAverageUsedUtilization'; +-- ZSTAC-80472: Resource notification webhook tables +CREATE TABLE IF NOT EXISTS `zstack`.`ResNotifySubscriptionVO` ( + `uuid` VARCHAR(32) NOT NULL UNIQUE, + `name` VARCHAR(255) DEFAULT NULL, + `description` VARCHAR(2048) DEFAULT NULL, + `resourceTypes` TEXT DEFAULT NULL, + `eventTypes` VARCHAR(256) DEFAULT NULL, + `type` VARCHAR(32) NOT NULL DEFAULT 'WEBHOOK', + `state` VARCHAR(32) NOT NULL DEFAULT 'Enabled', + `accountUuid` VARCHAR(32) DEFAULT NULL, + `lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`uuid`), + INDEX `idx_ResNotifySubscriptionVO_accountUuid` (`accountUuid`), + INDEX `idx_ResNotifySubscriptionVO_type_state` (`type`, `state`), + CONSTRAINT `fkResNotifySubscriptionVOResourceVO` FOREIGN KEY (`uuid`) REFERENCES `ResourceVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE IF NOT EXISTS `zstack`.`ResNotifyWebhookRefVO` ( + `uuid` VARCHAR(32) NOT NULL UNIQUE, + `webhookUrl` TEXT NOT NULL, + `secret` VARCHAR(256) DEFAULT NULL, + `customHeaders` TEXT, + PRIMARY KEY (`uuid`), + CONSTRAINT `fk_ResNotifyWebhookRefVO_ResNotifySubscriptionVO` + FOREIGN KEY (`uuid`) REFERENCES `ResNotifySubscriptionVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java b/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java index d0a92426313..5a7b0c34c31 100755 --- a/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java @@ -1,5 +1,7 @@ package org.zstack.core.db; +import com.google.common.collect.Maps; +import java.util.concurrent.CopyOnWriteArrayList; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.NestedExceptionUtils; @@ -66,7 +68,7 @@ class EntityInfo { Field eoSoftDeleteColumn; Class eoClass; Class voClass; - Map listeners = new HashMap(); + Map> listeners = Maps.newConcurrentMap(); EntityInfo(Class voClazz) { voClass = voClazz; @@ -82,12 +84,16 @@ class EntityInfo { EO at = (EO) voClazz.getAnnotation(EO.class); if (at != null) { eoClass = at.EOClazz(); - DebugUtils.Assert(eoClass != null, String.format("cannot find EO entity specified by VO entity[%s]", voClazz.getName())); + DebugUtils.Assert(eoClass != null, + String.format("cannot find EO entity specified by VO entity[%s]", voClazz.getName())); eoPrimaryKeyField = FieldUtils.getAnnotatedField(Id.class, eoClass); - DebugUtils.Assert(eoPrimaryKeyField != null, String.format("cannot find primary key field(@Id annotated) in EO entity[%s]", eoClass.getName())); + DebugUtils.Assert(eoPrimaryKeyField != null, String + .format("cannot find primary key field(@Id annotated) in EO entity[%s]", eoClass.getName())); eoPrimaryKeyField.setAccessible(true); eoSoftDeleteColumn = FieldUtils.getField(at.softDeletedColumn(), eoClass); - DebugUtils.Assert(eoSoftDeleteColumn != null, String.format("cannot find soft delete column[%s] in EO entity[%s]", at.softDeletedColumn(), eoClass.getName())); + DebugUtils.Assert(eoSoftDeleteColumn != null, + String.format("cannot find soft delete column[%s] in EO entity[%s]", at.softDeletedColumn(), + eoClass.getName())); eoSoftDeleteColumn.setAccessible(true); } @@ -108,7 +114,8 @@ private void buildSoftDeletionCascade() { for (final SoftDeletionCascade at : ats.value()) { final Class parent = at.parent(); if (!parent.isAnnotationPresent(Entity.class)) { - throw new CloudRuntimeException(String.format("class[%s] has annotation @SoftDeletionCascade but its parent class[%s] is not annotated by @Entity", + throw new CloudRuntimeException(String.format( + "class[%s] has annotation @SoftDeletionCascade but its parent class[%s] is not annotated by @Entity", voClass, parent)); } @@ -131,7 +138,8 @@ public List getEntityClassForSoftDeleteEntityExtension() { @Override @Transactional public void postSoftDelete(Collection entityIds, Class entityClass) { - String sql = String.format("delete from %s me where me.%s in (:ids)", voClass.getSimpleName(), at.joinColumn()); + String sql = String.format("delete from %s me where me.%s in (:ids)", voClass.getSimpleName(), + at.joinColumn()); Query q = getEntityManager().createQuery(sql); q.setParameter("ids", entityIds); q.executeUpdate(); @@ -148,7 +156,8 @@ private void buildInheritanceDeletionExtension() { final Class parent = voClass.getSuperclass(); if (!parent.isAnnotationPresent(Entity.class)) { - throw new CloudRuntimeException(String.format("class[%s] has annotation @PrimaryKeyJoinColumn but its parent class[%s] is not annotated by @Entity", + throw new CloudRuntimeException(String.format( + "class[%s] has annotation @PrimaryKeyJoinColumn but its parent class[%s] is not annotated by @Entity", voClass, parent)); } @@ -245,7 +254,8 @@ private void updateEO(Object entity, RuntimeException de) { } SQLIntegrityConstraintViolationException me = (SQLIntegrityConstraintViolationException) rootCause; - if (!(me.getErrorCode() == 1062 && "23000".equals(me.getSQLState()) && me.getMessage().contains("PRIMARY"))) { + if (!(me.getErrorCode() == 1062 && "23000".equals(me.getSQLState()) + && me.getMessage().contains("PRIMARY"))) { throw de; } @@ -253,9 +263,12 @@ private void updateEO(Object entity, RuntimeException de) { throw de; } - // at this point, the error is caused by a update tried on VO entity which has been soft deleted. This is mostly - // caused by a deletion cascade(e.g deleting host will cause vm running on it to be deleted, and deleting vm is trying to return capacity - // to host which has been soft deleted, because vm deletion is executed in async manner). In this case, we make the update to EO table + // at this point, the error is caused by a update tried on VO entity which has + // been soft deleted. This is mostly + // caused by a deletion cascade(e.g deleting host will cause vm running on it to + // be deleted, and deleting vm is trying to return capacity + // to host which has been soft deleted, because vm deletion is executed in async + // manner). In this case, we make the update to EO table Object idval = getEOPrimaryKeyValue(entity); Object eo = getEntityManager().find(eoClass, idval); @@ -360,8 +373,10 @@ private void hardDelete(Collection ids) { @Transactional private void nativeSqlDelete(Collection ids) { - // native sql can avoid JPA cascades a deletion to parent entity when deleting a child entity - String sql = String.format("delete from %s where %s in (:ids)", voClass.getSimpleName(), voPrimaryKeyField.getName()); + // native sql can avoid JPA cascades a deletion to parent entity when deleting a + // child entity + String sql = String.format("delete from %s where %s in (:ids)", voClass.getSimpleName(), + voPrimaryKeyField.getName()); Query q = getEntityManager().createNativeQuery(sql); q.setParameter("ids", ids); q.executeUpdate(); @@ -418,7 +433,8 @@ List listByPrimaryKeys(Collection ids, int offset, int length) { sql = String.format("select e from %s e", voClass.getSimpleName()); query = getEntityManager().createQuery(sql, voClass); } else { - sql = String.format("select e from %s e where e.%s in (:ids)", voClass.getSimpleName(), voPrimaryKeyField.getName()); + sql = String.format("select e from %s e where e.%s in (:ids)", voClass.getSimpleName(), + voPrimaryKeyField.getName()); query = getEntityManager().createQuery(sql, voClass); query.setParameter("ids", ids); } @@ -429,7 +445,8 @@ List listByPrimaryKeys(Collection ids, int offset, int length) { @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) boolean isExist(Object id) { - String sql = String.format("select count(*) from %s ref where ref.%s = :id", voClass.getSimpleName(), voPrimaryKeyField.getName()); + String sql = String.format("select count(*) from %s ref where ref.%s = :id", voClass.getSimpleName(), + voPrimaryKeyField.getName()); TypedQuery q = getEntityManager().createQuery(sql, Long.class); q.setParameter("id", id); q.setMaxResults(1); @@ -438,13 +455,25 @@ boolean isExist(Object id) { } void installLifeCycleCallback(EntityEvent evt, EntityLifeCycleCallback l) { - listeners.put(evt, l); + List cbs = listeners.computeIfAbsent(evt, k -> new CopyOnWriteArrayList<>()); + if (!cbs.contains(l)) { + cbs.add(l); + } + } + + void uninstallLifeCycleCallback(EntityEvent evt, EntityLifeCycleCallback l) { + List cbs = listeners.get(evt); + if (cbs != null) { + cbs.remove(l); + } } void fireLifeCycleEvent(EntityEvent evt, Object o) { - EntityLifeCycleCallback cb = listeners.get(evt); - if (cb != null) { - cb.entityLifeCycleEvent(evt, o); + List cbs = listeners.get(evt); + if (cbs != null) { + for (EntityLifeCycleCallback cb : cbs) { + cb.entityLifeCycleEvent(evt, o); + } } } } @@ -491,7 +520,8 @@ public CriteriaBuilder getCriteriaBuilder() { @Override public SimpleQuery createQuery(Class entityClass) { - assert entityClass.isAnnotationPresent(Entity.class) : entityClass.getName() + " is not annotated by JPA @Entity"; + assert entityClass.isAnnotationPresent(Entity.class) + : entityClass.getName() + " is not annotated by JPA @Entity"; return new SimpleQueryImpl(entityClass); } @@ -530,7 +560,6 @@ public void removeByPrimaryKeys(Collection priKeys, Class entityClazz) { getEntityInfo(entityClazz).removeByPrimaryKeys(priKeys); } - @Override public T updateAndRefresh(T entity) { return (T) getEntityInfo(entity.getClass()).updateAndRefresh(entity); @@ -636,7 +665,9 @@ public void entityForTranscationCallback(Operation op, Class... entityClass) sb.append(c.getName()).append(","); } - String err = String.format("entityForTranscationCallback is called but transcation is not active. Did you forget adding @Transactional to method??? [operation: %s, entity classes: %s]", op, sb.toString()); + String err = String.format( + "entityForTranscationCallback is called but transcation is not active. Did you forget adding @Transactional to method??? [operation: %s, entity classes: %s]", + op, sb.toString()); logger.warn(err); } } @@ -668,7 +699,8 @@ public long generateSequenceNumber(Class seqTable) { try { Field id = seqTable.getDeclaredField("id"); if (id == null) { - throw new CloudRuntimeException(String.format("sequence VO[%s] must have 'id' field", seqTable.getName())); + throw new CloudRuntimeException( + String.format("sequence VO[%s] must have 'id' field", seqTable.getName())); } Object vo = seqTable.newInstance(); vo = persistAndRefresh(vo); @@ -770,7 +802,7 @@ public void eoCleanup(Class VOClazz) { @Override @DeadlockAutoRestart public void eoCleanup(Class VOClazz, Object id) { - if(id == null) { + if (id == null) { throw new RuntimeException(String.format("Cleanup %s EO fail, id is null", VOClazz.getSimpleName())); } @@ -798,7 +830,7 @@ public boolean start() { } private void buildEntityInfo() { - BeanUtils.reflections.getTypesAnnotatedWith(Entity.class).forEach(clz-> { + BeanUtils.reflections.getTypesAnnotatedWith(Entity.class).forEach(clz -> { entityInfoMap.put(clz, new EntityInfo(clz)); }); } @@ -820,7 +852,8 @@ private void populateExtensions() { } } - for (SoftDeleteEntityByEOExtensionPoint ext : pluginRgty.getExtensionList(SoftDeleteEntityByEOExtensionPoint.class)) { + for (SoftDeleteEntityByEOExtensionPoint ext : pluginRgty + .getExtensionList(SoftDeleteEntityByEOExtensionPoint.class)) { for (Class eoClass : ext.getEOClassForSoftDeleteEntityExtension()) { List exts = softDeleteByEOExtensions.get(eoClass); if (exts == null) { @@ -873,6 +906,20 @@ public void installEntityLifeCycleCallback(Class clz, EntityEvent evt, EntityLif } } + @Override + public void uninstallEntityLifeCycleCallback(Class clz, EntityEvent evt, EntityLifeCycleCallback cb) { + if (clz != null) { + EntityInfo info = entityInfoMap.get(clz); + if (info != null) { + info.uninstallLifeCycleCallback(evt, cb); + } + } else { + for (EntityInfo info : entityInfoMap.values()) { + info.uninstallLifeCycleCallback(evt, cb); + } + } + } + @Override public boolean stop() { return true; @@ -881,7 +928,8 @@ public boolean stop() { void entityEvent(EntityEvent evt, Object entity) { EntityInfo info = entityInfoMap.get(entity.getClass()); if (info == null) { - logger.warn(String.format("cannot find EntityInfo for the class[%s], not entity events will be fired", entity.getClass())); + logger.warn(String.format("cannot find EntityInfo for the class[%s], not entity events will be fired", + entity.getClass())); return; } From d30e0864de00c0e6f8ed54c5760c443474397949 Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Thu, 12 Mar 2026 16:40:01 +0800 Subject: [PATCH 20/77] [i18n]: fix error code 10049/10050 translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix remaining i18n issues from MR !9224 that were caught during QA verification: 1. Why is this change necessary? Error code 10049 zh_CN still shows machine-translated text and zh_TW has garbled characters. Error code 10050 incorrectly changed "instance offering" to "compute offering" in 8 non-Chinese locales, but ZStack UI uses "instance offering" as the product term. 2. How does it address the problem? - 10049 zh_CN: replace "实例提供" with "计算规格" - 10049 zh_TW: fix "叢叢" to "叢集", replace "实例提供" with "計算規格" - 10050: revert 8 non-Chinese locales back to "instance offering" variants (en_US, fr-FR, id-ID, th-TH, de-DE, ja-JP, ko-KR, ru-RU) - 10050 zh_CN/zh_TW: no change (already correct) 3. Are there any side effects? None. Pure i18n text correction, no logic changes. # Summary of changes (by module): - i18n: fix 10049 zh_CN/zh_TW translations - i18n: revert 10050 non-Chinese locale changes Related: ZSTAC-72656 Change-Id: Ib5f42b1a972d9ab890b0b8b7f9c605aec9ffe4f0 --- conf/i18n/globalErrorCodeMapping/global-error-de-DE.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-en_US.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-id-ID.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-th-TH.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json index 81ae4d88c46..1e24c52ea11 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "Volume[uuid:%s] kann nicht auf Snapshot[uuid:%s] zurückgesetzt werden, das Volume der VM[uuid:%s] befindet sich nicht im Status \"Stopped\", aktueller Status ist %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "Kein qualifizierter primärer Speicher gefunden; Fehler sind %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Aktualisierung des Gruppenstatus fehlgeschlagen: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid-Konflikt: Der durch das Compute-Angebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid-Konflikt: Der durch das Instanzangebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuid-Konflikt: Der durch das Root-Disk-Angebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", "ORG_ZSTACK_V2V_10008": "Dieselbe MAC-Adresse [%s] ist im Netzwerk[%s] nicht erlaubt", "ORG_ZSTACK_V2V_10009": "Doppelte MAC-Adresse [%s] im Netzwerk[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index f45787d6344..7df63d4098c 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "unable to reset volume[uuid:%s] to snapshot[uuid:%s], the vm[uuid:%s] volume is not in Stopped state, current state is %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "cannot find any qualified primary storage; errors are %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Failed to update group status: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid Conflict: The primary storage specified by the compute offering is %s, while the primary storage specified in the creation parameter is %s.", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid Conflict: The primary storage specified by the instance offering is %s, while the primary storage specified in the creation parameter is %s.", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuid Conflict: The primary storage specified by the root disk offering is %s, while the primary storage specified in the creation parameter is %s.", "ORG_ZSTACK_V2V_10008": "Not allowed the same MAC address [%s] in network[%s]", "ORG_ZSTACK_V2V_10009": "Duplicate MAC address [%s] in network[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json index 6a6ece44837..8fb00fa5776 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "impossible de réinitialiser le volume[uuid:%s] vers l'instantané[uuid:%s], le volume vm[uuid:%s] n'est pas dans l'état Arrêté, l'état actuel est %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "impossible de trouver un stockage principal qualifié ; les erreurs sont %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Échec de la mise à jour du statut du groupe : %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre de calcul est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre d'instance est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre de disque racine est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", "ORG_ZSTACK_V2V_10008": "L'adresse MAC [%s] identique n'est pas autorisée dans le réseau[%s]", "ORG_ZSTACK_V2V_10009": "Adresse MAC [%s] en double dans le réseau[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json index c2bf216dea4..7e41658f314 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "tidak dapat mengatur ulang volume[uuid:%s] ke snapshot[uuid:%s], volume vm[uuid:%s] tidak dalam keadaan Berhenti, keadaan saat ini adalah %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "tidak dapat menemukan primary storage yang memenuhi syarat; error adalah %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Gagal memperbarui status grup: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh compute offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh instance offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh root disk offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", "ORG_ZSTACK_V2V_10008": "Alamat MAC [%s] yang sama tidak diperbolehkan dalam jaringan[%s]", "ORG_ZSTACK_V2V_10009": "Alamat MAC duplikat [%s] dalam jaringan[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json index 5bd506c86a6..bf73aa503ff 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "ボリューム[uuid:%s]をスナップショット[uuid:%s]にリセットできません。vm[uuid:%s]のボリュームは停止状態ではありません。現在のステータスは%sです", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "適切なプライマリストレージが見つかりません。エラー: %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "グループのステータスの更新に失敗しました: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuidの競合。コンピュートオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuidの競合。インスタンスオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuidの競合。ルートディスクオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", "ORG_ZSTACK_V2V_10008": "ネットワーク[%s]で同じMACアドレス[%s]は許可されていません", "ORG_ZSTACK_V2V_10009": "ネットワーク[%s]でMACアドレス[%s]が重複しています", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json index 5d58f6618b1..d9323871208 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "볼륨[uuid:%s]을 스냅샷[uuid:%s]으로 재설정할 수 없습니다, vm[uuid:%s] 볼륨이 Stopped 상태가 아니며, 현재 상태는 %s입니다", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "적합한 기본 스토리지를 찾을 수 없습니다; 오류: %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "그룹 상태 업데이트 실패: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid 충돌: 컴퓨트 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid 충돌: 인스턴스 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuid 충돌: 루트 디스크 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", "ORG_ZSTACK_V2V_10008": "네트워크[%s]에서 동일한 MAC 주소 [%s]가 허용되지 않습니다", "ORG_ZSTACK_V2V_10009": "네트워크[%s]에서 중복된 MAC 주소 [%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json index 51a125842af..55bde5ee0c5 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "невозможно сбросить том[uuid:%s] к снимку[uuid:%s], том ВМ[uuid:%s] не находится в состоянии Stopped, текущее состояние: %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "невозможно найти подходящее основное хранилище; ошибки: %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "не удалось обновить статус группы: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении вычислений: %s, а основное хранилище, указанное в параметре создания: %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении экземпляра: %s, а основное хранилище, указанное в параметре создания: %s", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении корневого диска: %s, а основное хранилище, указанное в параметре создания: %s", "ORG_ZSTACK_V2V_10008": "Не допускается одинаковый MAC-адрес [%s] в сети[%s]", "ORG_ZSTACK_V2V_10009": "Дублирующийся MAC-адрес [%s] в сети[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json index d8b7abf66d5..3118556b6d7 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "ไม่สามารถรีเซ็ต volume[uuid:%s] ไปยัง snapshot[uuid:%s], volume ของ vm[uuid:%s] ไม่อยู่ในสถานะ Stopped, สถานะปัจจุบันคือ %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "ไม่พบ primary storage ที่มีคุณสมบัติเหมาะสม; ข้อผิดพลาดคือ %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "ไม่สามารถอัปเดตสถานะกลุ่ม: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย compute offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย instance offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย root disk offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", "ORG_ZSTACK_V2V_10008": "ไม่อนุญาตให้มี MAC address [%s] ซ้ำกันใน network[%s]", "ORG_ZSTACK_V2V_10009": "MAC address [%s] ซ้ำกันใน network[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 5235832dccf..a6a01a7abe3 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -29,7 +29,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_10047": "cidr[%s] 输入格式错误", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10050": "ZSha2 显示存在另一个管理节点,但数据库中未找到该节点", "ORG_ZSTACK_STORAGE_PRIMARY_10046": "仅允许一个主要存储 CIDR 系统标签,但获得了 %d 个", - "ORG_ZSTACK_STORAGE_PRIMARY_10049": "集群Uuid冲突,实例提供中指定的集群是%s,而创建参数中指定的集群是%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10049": "集群Uuid冲突,计算规格中指定的集群是%s,而创建参数中指定的集群是%s", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10052": "未找到解注册的appId!", "ORG_ZSTACK_STORAGE_PRIMARY_10042": "请指定分配空间的目的", "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "无法将卷[uuid:%s]恢复至快照[uuid:%s],关联的虚拟机[uuid:%s]卷当前不在已停止状态,当前状态是%s", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json index 40aa862f414..b696d46d059 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json @@ -29,7 +29,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_10047": "cidr[%s] 輸入格式錯誤誤", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10050": "ZSha2 显示儲在另一個管理節點,但數据库中未找到該節點", "ORG_ZSTACK_STORAGE_PRIMARY_10046": "仅允許一個主要儲儲 CIDR 系統統標签,但獲得了 %d 個", - "ORG_ZSTACK_STORAGE_PRIMARY_10049": "叢叢Uuid冲突,实例提供中指定的叢叢是%s,而創建參數中指定的叢叢是%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10049": "叢集Uuid衝突,計算規格中指定的叢集是%s,而創建參數中指定的叢集是%s", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10052": "未找到解注册的appId!", "ORG_ZSTACK_STORAGE_PRIMARY_10042": "請指定分配空間的目的", "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "無法将卷[uuid:%s]恢复至快照[uuid:%s],關聯的虚拟機[uuid:%s]卷當前不在已停止状态,當前状态是%s", From 648fc77435a68c24af437bd9c05ec74784ef170b Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 10 Mar 2026 23:45:21 +0800 Subject: [PATCH 21/77] [sdk]: add Kunlunxin to GpuVendor enum for P800 GPU support Resolves: ZSTAC-82259 Change-Id: I20d0de1968bab046c3e2882ca2d64c3926bfca74 --- sdk/src/main/java/org/zstack/sdk/GpuVendor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/org/zstack/sdk/GpuVendor.java b/sdk/src/main/java/org/zstack/sdk/GpuVendor.java index 9013b3048ed..22e6066d894 100644 --- a/sdk/src/main/java/org/zstack/sdk/GpuVendor.java +++ b/sdk/src/main/java/org/zstack/sdk/GpuVendor.java @@ -7,7 +7,7 @@ public enum GpuVendor { Haiguang, Huawei, TianShu, + Kunlunxin, Other, Alibaba, - Kunlunxin, } From f5459df20d36e5cad404ab4b32684181a3d62eea Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Fri, 13 Mar 2026 00:41:46 +0800 Subject: [PATCH 22/77] [core]: add resnotify webhook SDK and test support Add uninstallEntityLifeCycleCallback to DatabaseFacade interface. Generate SDK action classes and ApiHelper methods for resnotify webhook subscription CRUD APIs. Change-Id: I9191952ca0a0959644bc2d90d539a818cf13d30a --- .../org/zstack/core/db/DatabaseFacade.java | 2 + sdk/src/main/java/SourceClassMap.java | 8 ++ .../DeleteResNotifySubscriptionAction.java | 104 ++++++++++++++ .../DeleteResNotifySubscriptionResult.java | 7 + .../QueryResNotifySubscriptionAction.java | 75 ++++++++++ .../QueryResNotifySubscriptionResult.java | 22 +++ .../ResNotifySubscriptionInventory.java | 97 +++++++++++++ .../resnotify/ResNotifySubscriptionState.java | 6 + .../sdk/zwatch/resnotify/ResNotifyType.java | 6 + .../ResNotifyWebhookRefInventory.java | 31 +++++ .../resnotify/SubscribeResNotifyAction.java | 128 ++++++++++++++++++ .../resnotify/SubscribeResNotifyResult.java | 14 ++ .../UpdateResNotifySubscriptionAction.java | 125 +++++++++++++++++ .../UpdateResNotifySubscriptionResult.java | 14 ++ .../java/org/zstack/testlib/ApiHelper.groovy | 110 +++++++++++++++ 15 files changed, 749 insertions(+) create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java diff --git a/core/src/main/java/org/zstack/core/db/DatabaseFacade.java b/core/src/main/java/org/zstack/core/db/DatabaseFacade.java index 155aca5162b..efb4e22b307 100755 --- a/core/src/main/java/org/zstack/core/db/DatabaseFacade.java +++ b/core/src/main/java/org/zstack/core/db/DatabaseFacade.java @@ -84,4 +84,6 @@ public interface DatabaseFacade { String getDbVersion(); void installEntityLifeCycleCallback(Class entityClass, EntityEvent evt, EntityLifeCycleCallback cb); + + void uninstallEntityLifeCycleCallback(Class entityClass, EntityEvent evt, EntityLifeCycleCallback cb); } diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index bb413e5ac31..33aff694e9d 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -872,6 +872,10 @@ public class SourceClassMap { put("org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory", "org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory"); put("org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO", "org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO"); put("org.zstack.zwatch.monitorgroup.entity.MonitorTemplateInventory", "org.zstack.sdk.zwatch.monitorgroup.entity.MonitorTemplateInventory"); + put("org.zstack.zwatch.resnotify.ResNotifySubscriptionInventory", "org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory"); + put("org.zstack.zwatch.resnotify.ResNotifySubscriptionState", "org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionState"); + put("org.zstack.zwatch.resnotify.ResNotifyType", "org.zstack.sdk.zwatch.resnotify.ResNotifyType"); + put("org.zstack.zwatch.resnotify.ResNotifyWebhookRefInventory", "org.zstack.sdk.zwatch.resnotify.ResNotifyWebhookRefInventory"); put("org.zstack.zwatch.ruleengine.ComparisonOperator", "org.zstack.sdk.zwatch.ruleengine.ComparisonOperator"); put("org.zstack.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory", "org.zstack.sdk.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory"); put("org.zstack.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory", "org.zstack.sdk.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory"); @@ -1748,6 +1752,10 @@ public class SourceClassMap { put("org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory", "org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory"); put("org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO", "org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO"); put("org.zstack.sdk.zwatch.monitorgroup.entity.MonitorTemplateInventory", "org.zstack.zwatch.monitorgroup.entity.MonitorTemplateInventory"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory", "org.zstack.zwatch.resnotify.ResNotifySubscriptionInventory"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionState", "org.zstack.zwatch.resnotify.ResNotifySubscriptionState"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifyType", "org.zstack.zwatch.resnotify.ResNotifyType"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifyWebhookRefInventory", "org.zstack.zwatch.resnotify.ResNotifyWebhookRefInventory"); put("org.zstack.sdk.zwatch.ruleengine.ComparisonOperator", "org.zstack.zwatch.ruleengine.ComparisonOperator"); put("org.zstack.sdk.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory", "org.zstack.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory"); put("org.zstack.sdk.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory", "org.zstack.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java new file mode 100644 index 00000000000..9266477e583 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DeleteResNotifySubscriptionAction 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.zwatch.resnotify.DeleteResNotifySubscriptionResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @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.zwatch.resnotify.DeleteResNotifySubscriptionResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionResult() : 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 = "DELETE"; + info.path = "/zwatch/resnotify/subscriptions/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java new file mode 100644 index 00000000000..01e30f49416 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.zwatch.resnotify; + + + +public class DeleteResNotifySubscriptionResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java new file mode 100644 index 00000000000..5f4451557aa --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryResNotifySubscriptionAction extends QueryAction { + + 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.zwatch.resnotify.QueryResNotifySubscriptionResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionResult() : 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 = "GET"; + info.path = "/zwatch/resnotify/subscriptions"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java new file mode 100644 index 00000000000..93cd0d2caeb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.zwatch.resnotify; + + + +public class QueryResNotifySubscriptionResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java new file mode 100644 index 00000000000..550d258854b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java @@ -0,0 +1,97 @@ +package org.zstack.sdk.zwatch.resnotify; + +import org.zstack.sdk.zwatch.resnotify.ResNotifyType; +import org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionState; +import org.zstack.sdk.zwatch.resnotify.ResNotifyWebhookRefInventory; + +public class ResNotifySubscriptionInventory { + + 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 resourceTypes; + public void setResourceTypes(java.lang.String resourceTypes) { + this.resourceTypes = resourceTypes; + } + public java.lang.String getResourceTypes() { + return this.resourceTypes; + } + + public java.lang.String eventTypes; + public void setEventTypes(java.lang.String eventTypes) { + this.eventTypes = eventTypes; + } + public java.lang.String getEventTypes() { + return this.eventTypes; + } + + public ResNotifyType type; + public void setType(ResNotifyType type) { + this.type = type; + } + public ResNotifyType getType() { + return this.type; + } + + public ResNotifySubscriptionState state; + public void setState(ResNotifySubscriptionState state) { + this.state = state; + } + public ResNotifySubscriptionState getState() { + return this.state; + } + + public java.lang.String accountUuid; + public void setAccountUuid(java.lang.String accountUuid) { + this.accountUuid = accountUuid; + } + public java.lang.String getAccountUuid() { + return this.accountUuid; + } + + 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; + } + + public ResNotifyWebhookRefInventory webhookRef; + public void setWebhookRef(ResNotifyWebhookRefInventory webhookRef) { + this.webhookRef = webhookRef; + } + public ResNotifyWebhookRefInventory getWebhookRef() { + return this.webhookRef; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java new file mode 100644 index 00000000000..18351896246 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java @@ -0,0 +1,6 @@ +package org.zstack.sdk.zwatch.resnotify; + +public enum ResNotifySubscriptionState { + Enabled, + Disabled, +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java new file mode 100644 index 00000000000..5a00aa5b80e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java @@ -0,0 +1,6 @@ +package org.zstack.sdk.zwatch.resnotify; + +public enum ResNotifyType { + WEBHOOK, + WEBSOCKET, +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java new file mode 100644 index 00000000000..cf2c73bc674 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java @@ -0,0 +1,31 @@ +package org.zstack.sdk.zwatch.resnotify; + + + +public class ResNotifyWebhookRefInventory { + + 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 webhookUrl; + public void setWebhookUrl(java.lang.String webhookUrl) { + this.webhookUrl = webhookUrl; + } + public java.lang.String getWebhookUrl() { + return this.webhookUrl; + } + + public java.lang.String customHeaders; + public void setCustomHeaders(java.lang.String customHeaders) { + this.customHeaders = customHeaders; + } + public java.lang.String getCustomHeaders() { + return this.customHeaders; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java new file mode 100644 index 00000000000..05e5af5d9de --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java @@ -0,0 +1,128 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SubscribeResNotifyAction 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.zwatch.resnotify.SubscribeResNotifyResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List resourceTypes; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List eventTypes; + + @Param(required = false, validValues = {"WEBHOOK"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String type = "WEBHOOK"; + + @Param(required = true, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String webhookUrl; + + @Param(required = false, maxLength = 256, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String secret; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String customHeaders; + + @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.zwatch.resnotify.SubscribeResNotifyResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyResult() : 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 = "/zwatch/resnotify/subscriptions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java new file mode 100644 index 00000000000..60ba0b47c8e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.zwatch.resnotify; + +import org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory; + +public class SubscribeResNotifyResult { + public ResNotifySubscriptionInventory inventory; + public void setInventory(ResNotifySubscriptionInventory inventory) { + this.inventory = inventory; + } + public ResNotifySubscriptionInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java new file mode 100644 index 00000000000..3ec79ae0a35 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java @@ -0,0 +1,125 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateResNotifySubscriptionAction 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.zwatch.resnotify.UpdateResNotifySubscriptionResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List resourceTypes; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List eventTypes; + + @Param(required = false, validValues = {"Enabled","Disabled"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String state; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String webhookUrl; + + @Param(required = false, maxLength = 256, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String secret; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String customHeaders; + + @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.zwatch.resnotify.UpdateResNotifySubscriptionResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionResult() : 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 = "PUT"; + info.path = "/zwatch/resnotify/subscriptions/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateResNotifySubscription"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java new file mode 100644 index 00000000000..8f5b2f62157 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.zwatch.resnotify; + +import org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory; + +public class UpdateResNotifySubscriptionResult { + public ResNotifySubscriptionInventory inventory; + public void setInventory(ResNotifySubscriptionInventory inventory) { + this.inventory = inventory; + } + public ResNotifySubscriptionInventory getInventory() { + return this.inventory; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 396e175189c..af8379f56e4 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -56760,6 +56760,116 @@ abstract class ApiHelper { } + def deleteResNotifySubscription(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionAction() + 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 queryResNotifySubscription(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + 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 subscribeResNotify(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyAction() + 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 updateResNotifySubscription(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionAction() + 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 addThirdpartyPlatform(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.thirdparty.api.AddThirdpartyPlatformAction.class) Closure c) { def a = new org.zstack.sdk.zwatch.thirdparty.api.AddThirdpartyPlatformAction() a.sessionId = Test.currentEnvSpec?.session?.uuid From cb554df47283c96023ebbe917333e1f24e5199f1 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Wed, 11 Mar 2026 20:24:28 +0800 Subject: [PATCH 23/77] [sdk]: update SDK files for LongJobProgressDetail Resolves: ZSTAC-82318 Change-Id: I8931c4207547b836b522c9b5cea2db807a032d5c --- .../LongJobProgressDetailBuilder.java | 23 +++++++++++++++---- .../core/progress/TaskProgressInventory.java | 2 +- sdk/src/main/java/SourceClassMap.java | 2 ++ .../org/zstack/sdk/LongJobProgressDetail.java | 6 ++--- .../org/zstack/sdk/TaskProgressInventory.java | 8 +++---- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java index 18a436fc280..4c9a09b2580 100644 --- a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java @@ -142,6 +142,7 @@ private static LongJobProgressDetail tryParseAiDownloadFormat(Map extra = new HashMap<>(); // state field Object stateVal = inner.get("state"); @@ -156,7 +157,7 @@ private static LongJobProgressDetail tryParseAiDownloadFormat(Map extraInner = new HashMap<>(inner); + extraInner.remove("state"); + extraInner.remove("progress"); + extraInner.remove("state_reason"); + extra.putAll(extraInner); + + // preserve unknown keys from raw outer-level + Map extraRaw = new HashMap<>(raw); + extraRaw.remove("data"); + extra.putAll(extraRaw); + + if (!extra.isEmpty()) { + detail.setExtra(extra); + } + return detail; } catch (Exception e) { logger.trace("LongJobProgressDetailBuilder: failed to parse AI download format", e); diff --git a/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java b/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java index b86dcb50eef..b945d270292 100755 --- a/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java +++ b/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java @@ -18,7 +18,7 @@ public class TaskProgressInventory { private Long time; private List subTasks; private String arguments; - /** Typed progress detail parsed from opaque. Null when opaque is absent or unrecognized. */ + /** Typed progress detail parsed from opaque. Null when opaque is absent or parsing fails. */ private LongJobProgressDetail progressDetail; public TaskProgressInventory() { diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 46a1ce8c961..b5ea0d48dea 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -247,6 +247,7 @@ public class SourceClassMap { put("org.zstack.header.core.external.service.ExternalServiceCapabilities", "org.zstack.sdk.ExternalServiceCapabilities"); put("org.zstack.header.core.external.service.ExternalServiceInventory", "org.zstack.sdk.ExternalServiceInventory"); put("org.zstack.header.core.progress.ChainInfo", "org.zstack.sdk.ChainInfo"); + put("org.zstack.header.core.progress.LongJobProgressDetail", "org.zstack.sdk.LongJobProgressDetail"); put("org.zstack.header.core.progress.PendingTaskInfo", "org.zstack.sdk.PendingTaskInfo"); put("org.zstack.header.core.progress.RunningTaskInfo", "org.zstack.sdk.RunningTaskInfo"); put("org.zstack.header.core.progress.TaskInfo", "org.zstack.sdk.TaskInfo"); @@ -1208,6 +1209,7 @@ public class SourceClassMap { put("org.zstack.sdk.LogType", "org.zstack.log.server.LogType"); put("org.zstack.sdk.LoginAuthenticationProcedureDesc", "org.zstack.header.identity.login.LoginAuthenticationProcedureDesc"); put("org.zstack.sdk.LongJobInventory", "org.zstack.header.longjob.LongJobInventory"); + put("org.zstack.sdk.LongJobProgressDetail", "org.zstack.header.core.progress.LongJobProgressDetail"); put("org.zstack.sdk.LongJobState", "org.zstack.header.longjob.LongJobState"); put("org.zstack.sdk.LunInventory", "org.zstack.header.storageDevice.LunInventory"); put("org.zstack.sdk.MaaSUsage", "org.zstack.ai.message.MaaSUsage"); diff --git a/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java index 365f535bc45..c37bc20795b 100644 --- a/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java +++ b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java @@ -84,11 +84,11 @@ public java.lang.Long getEstimatedRemainingSeconds() { return this.estimatedRemainingSeconds; } - public java.util.LinkedHashMap extra; - public void setExtra(java.util.LinkedHashMap extra) { + public java.util.Map extra; + public void setExtra(java.util.Map extra) { this.extra = extra; } - public java.util.LinkedHashMap getExtra() { + public java.util.Map getExtra() { return this.extra; } diff --git a/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java b/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java index e22f11655f7..6bec468e66f 100644 --- a/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java @@ -1,6 +1,6 @@ package org.zstack.sdk; - +import org.zstack.sdk.LongJobProgressDetail; public class TaskProgressInventory { @@ -76,11 +76,11 @@ public java.lang.String getArguments() { return this.arguments; } - public org.zstack.sdk.LongJobProgressDetail progressDetail; - public void setProgressDetail(org.zstack.sdk.LongJobProgressDetail progressDetail) { + public LongJobProgressDetail progressDetail; + public void setProgressDetail(LongJobProgressDetail progressDetail) { this.progressDetail = progressDetail; } - public org.zstack.sdk.LongJobProgressDetail getProgressDetail() { + public LongJobProgressDetail getProgressDetail() { return this.progressDetail; } From 2318947e1edd1caad90443259e9658a6fce7a743 Mon Sep 17 00:00:00 2001 From: "shan.wu" Date: Tue, 13 Jan 2026 14:54:15 +0800 Subject: [PATCH 24/77] [dpu-bm2]: support dpu baremetal2 instance support dpu baremetal2 instance Resolves/Related: ZSTAC-12345 Change-Id: I626d637a7168656a6c726c6769777a726e616973 --- .../zstack/compute/host/HostManagerImpl.java | 4 +- conf/db/upgrade/V5.5.12__schema.sql | 33 +++++ .../header/cluster/APICreateClusterMsg.java | 2 +- .../APICreateClusterMsgDoc_zh_cn.groovy | 2 +- sdk/src/main/java/SourceClassMap.java | 4 + .../sdk/AddBareMetal2DpuChassisAction.java | 125 ++++++++++++++++++ .../sdk/BareMetal2DpuChassisInventory.java | 23 ++++ .../sdk/BareMetal2DpuHostInventory.java | 31 +++++ .../sdk/CreateBareMetal2InstanceAction.java | 3 + .../org/zstack/sdk/CreateClusterAction.java | 2 +- .../sdk/StartBareMetal2InstanceAction.java | 3 + .../java/org/zstack/testlib/ApiHelper.groovy | 27 ++++ .../CloudOperationsErrorCode.java | 16 +++ 13 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java diff --git a/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java b/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java index a15ac71e227..3e478c319e8 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java @@ -102,6 +102,8 @@ public class HostManagerImpl extends AbstractService implements HostManager, Man private Future reportHostCapacityTask; private Future refreshHostPowerStatusTask; + private static final List SKIP_ARCH_CHECK_HYPERVISOR_TYPES = Arrays.asList("baremetal2", "baremetal2Dpu"); + static { allowedMessageAfterSoftDeletion.add(HostDeletionMsg.class); } @@ -472,7 +474,7 @@ public void run(MessageReply reply) { @Override public boolean skip(Map data) { // no need to check baremetal2 gateway architecture with the cluster architecture - return vo.getHypervisorType().equals("baremetal2"); + return SKIP_ARCH_CHECK_HYPERVISOR_TYPES.contains(cluster.getHypervisorType()); } @Override diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 58d0e6ee187..33709a74abc 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -89,3 +89,36 @@ CREATE TABLE IF NOT EXISTS `zstack`.`ResNotifyWebhookRefVO` ( CONSTRAINT `fk_ResNotifyWebhookRefVO_ResNotifySubscriptionVO` FOREIGN KEY (`uuid`) REFERENCES `ResNotifySubscriptionVO`(`uuid`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`BareMetal2DpuChassisVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `config` TEXT DEFAULT NULL, + `hostUuid` varchar(32) DEFAULT NULL, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkBareMetal2DpuChassisVOChassisVO` FOREIGN KEY (`uuid`) REFERENCES `BareMetal2ChassisVO` (`uuid`) ON DELETE CASCADE, + CONSTRAINT `fkBareMetal2DpuChassisVOHostEO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`BareMetal2DpuHostVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `chassisUuid` VARCHAR(32) NOT NULL, + `vendorType` VARCHAR(255) NOT NULL, + `url` VARCHAR(255) NOT NULL, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkBareMetal2DpuHostVOHostVO` FOREIGN KEY (`uuid`) REFERENCES `HostEO` (`uuid`) ON DELETE CASCADE, + CONSTRAINT `fkBareMetal2DpuHostVOChassisVO` FOREIGN KEY (`chassisUuid`) REFERENCES `BareMetal2DpuChassisVO` (`uuid`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE `zstack`.`BareMetal2InstanceVO` +DROP FOREIGN KEY `fkBareMetal2InstanceVOGatewayVO`, +DROP FOREIGN KEY `fkBareMetal2InstanceVOGatewayVO1`; + +ALTER TABLE `zstack`.`BareMetal2InstanceVO` + ADD CONSTRAINT `fkBareMetal2InstanceVOGatewayVO` + FOREIGN KEY (`gatewayUuid`) + REFERENCES `HostEO` (`uuid`) + ON DELETE SET NULL, + ADD CONSTRAINT `fkBareMetal2InstanceVOGatewayVO1` + FOREIGN KEY (`lastGatewayUuid`) + REFERENCES `HostEO` (`uuid`) + ON DELETE SET NULL; \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java index 18d1727a07f..7d740b7d152 100755 --- a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java @@ -70,7 +70,7 @@ public class APICreateClusterMsg extends APICreateMessage implements CreateClust * - Simulator * - baremetal */ - @APIParam(validValues = {"KVM", "Simulator", "baremetal", "baremetal2", "xdragon"}) + @APIParam(validValues = {"KVM", "Simulator", "baremetal", "baremetal2", "xdragon", "baremetal2Dpu"}) private String hypervisorType; /** * @desc see field 'type' of :ref:`ClusterInventory` for details diff --git a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy index 90c01a79fa8..b4faf3d122e 100644 --- a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy @@ -56,7 +56,7 @@ doc { type "String" optional false since "0.6" - values ("KVM","Simulator","baremetal","baremetal2","xdragon") + values ("KVM","Simulator","baremetal","baremetal2","xdragon","baremetal2Dpu") } column { name "type" diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 33aff694e9d..e76a0eae9db 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -66,8 +66,10 @@ public class SourceClassMap { put("org.zstack.baremetal2.chassis.BareMetal2ChassisInventory", "org.zstack.sdk.BareMetal2ChassisInventory"); put("org.zstack.baremetal2.chassis.BareMetal2ChassisNicInventory", "org.zstack.sdk.BareMetal2ChassisNicInventory"); put("org.zstack.baremetal2.chassis.BareMetal2ChassisPciDeviceInventory", "org.zstack.sdk.BareMetal2ChassisPciDeviceInventory"); + put("org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisInventory", "org.zstack.sdk.BareMetal2DpuChassisInventory"); put("org.zstack.baremetal2.chassis.ipmi.BareMetal2IpmiChassisInventory", "org.zstack.sdk.BareMetal2IpmiChassisInventory"); put("org.zstack.baremetal2.configuration.BareMetal2ChassisOfferingInventory", "org.zstack.sdk.BareMetal2ChassisOfferingInventory"); + put("org.zstack.baremetal2.dpu.BareMetal2DpuHostInventory", "org.zstack.sdk.BareMetal2DpuHostInventory"); put("org.zstack.baremetal2.gateway.BareMetal2GatewayInventory", "org.zstack.sdk.BareMetal2GatewayInventory"); put("org.zstack.baremetal2.gateway.BareMetal2GatewayProvisionNicInventory", "org.zstack.sdk.BareMetal2GatewayProvisionNicInventory"); put("org.zstack.baremetal2.instance.BareMetal2InstanceInventory", "org.zstack.sdk.BareMetal2InstanceInventory"); @@ -957,6 +959,8 @@ public class SourceClassMap { put("org.zstack.sdk.BareMetal2ChassisNicInventory", "org.zstack.baremetal2.chassis.BareMetal2ChassisNicInventory"); put("org.zstack.sdk.BareMetal2ChassisOfferingInventory", "org.zstack.baremetal2.configuration.BareMetal2ChassisOfferingInventory"); put("org.zstack.sdk.BareMetal2ChassisPciDeviceInventory", "org.zstack.baremetal2.chassis.BareMetal2ChassisPciDeviceInventory"); + put("org.zstack.sdk.BareMetal2DpuChassisInventory", "org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisInventory"); + put("org.zstack.sdk.BareMetal2DpuHostInventory", "org.zstack.baremetal2.dpu.BareMetal2DpuHostInventory"); put("org.zstack.sdk.BareMetal2GatewayInventory", "org.zstack.baremetal2.gateway.BareMetal2GatewayInventory"); put("org.zstack.sdk.BareMetal2GatewayProvisionNicInventory", "org.zstack.baremetal2.gateway.BareMetal2GatewayProvisionNicInventory"); put("org.zstack.sdk.BareMetal2InstanceInventory", "org.zstack.baremetal2.instance.BareMetal2InstanceInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java b/sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java new file mode 100644 index 00000000000..56f877caf5c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java @@ -0,0 +1,125 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class AddBareMetal2DpuChassisAction 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.AddBareMetal2ChassisResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String url; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vendorType; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String config; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, 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 clusterUuid; + + @Param(required = false, validValues = {"Remote","Local","Direct"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String provisionType = "Remote"; + + @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.AddBareMetal2ChassisResult value = res.getResult(org.zstack.sdk.AddBareMetal2ChassisResult.class); + ret.value = value == null ? new org.zstack.sdk.AddBareMetal2ChassisResult() : 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 = "/baremetal2/chassis/dpu"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java new file mode 100644 index 00000000000..a68225a7c45 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java @@ -0,0 +1,23 @@ +package org.zstack.sdk; + + + +public class BareMetal2DpuChassisInventory extends org.zstack.sdk.BareMetal2ChassisInventory { + + public java.lang.String config; + public void setConfig(java.lang.String config) { + this.config = config; + } + public java.lang.String getConfig() { + return this.config; + } + + public java.lang.String hostUuid; + public void setHostUuid(java.lang.String hostUuid) { + this.hostUuid = hostUuid; + } + public java.lang.String getHostUuid() { + return this.hostUuid; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java new file mode 100644 index 00000000000..09864707a7a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java @@ -0,0 +1,31 @@ +package org.zstack.sdk; + + + +public class BareMetal2DpuHostInventory extends org.zstack.sdk.HostInventory { + + public java.lang.String url; + public void setUrl(java.lang.String url) { + this.url = url; + } + public java.lang.String getUrl() { + return this.url; + } + + public java.lang.String vendorType; + public void setVendorType(java.lang.String vendorType) { + this.vendorType = vendorType; + } + public java.lang.String getVendorType() { + return this.vendorType; + } + + public java.lang.String chassisUuid; + public void setChassisUuid(java.lang.String chassisUuid) { + this.chassisUuid = chassisUuid; + } + public java.lang.String getChassisUuid() { + return this.chassisUuid; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java b/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java index 755fb104476..a0fac8d2d9f 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java @@ -70,6 +70,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String gatewayAllocatorStrategy; + @Param(required = false, validValues = {"IPMI","DPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String chassisType; + @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java b/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java index 4d489de2ea8..5975cf8f9fd 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java @@ -34,7 +34,7 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String description; - @Param(required = true, validValues = {"KVM","Simulator","baremetal","baremetal2","xdragon"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, validValues = {"KVM","Simulator","baremetal","baremetal2","xdragon","baremetal2Dpu"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String hypervisorType; @Param(required = false, validValues = {"zstack","baremetal","baremetal2"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) diff --git a/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java b/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java index 4563ac62512..7b9f3459f9a 100644 --- a/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java @@ -40,6 +40,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String chassisOfferingUuid; + @Param(required = false, validValues = {"IPMI","DPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String chassisType; + @Param(required = false) public java.util.List systemTags; diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index af8379f56e4..8a8f2b91416 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -1016,6 +1016,33 @@ 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() + 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 addBlockPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBlockPrimaryStorageAction.class) Closure c) { def a = new org.zstack.sdk.AddBlockPrimaryStorageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index aed0e710251..0deb26d677d 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -5482,6 +5482,14 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_GATEWAY_10083 = "ORG_ZSTACK_BAREMETAL2_GATEWAY_10083"; + public static final String ORG_ZSTACK_BAREMETAL2_GATEWAY_10084 = "ORG_ZSTACK_BAREMETAL2_GATEWAY_10084"; + + public static final String ORG_ZSTACK_BAREMETAL2_GATEWAY_10085 = "ORG_ZSTACK_BAREMETAL2_GATEWAY_10085"; + + public static final String ORG_ZSTACK_BAREMETAL2_DPU_10000 = "ORG_ZSTACK_BAREMETAL2_DPU_10000"; + + public static final String ORG_ZSTACK_BAREMETAL2_DPU_10001 = "ORG_ZSTACK_BAREMETAL2_DPU_10001"; + public static final String ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10000 = "ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10000"; public static final String ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10001 = "ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10001"; @@ -7700,6 +7708,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10089 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10089"; + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10090 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10090"; + + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10091 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10091"; + + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10092 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10092"; + public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10000 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10000"; public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10001 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10001"; @@ -13744,6 +13758,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_CHASSIS_10025 = "ORG_ZSTACK_BAREMETAL2_CHASSIS_10025"; + public static final String ORG_ZSTACK_BAREMETAL2_CHASSIS_10026 = "ORG_ZSTACK_BAREMETAL2_CHASSIS_10026"; + public static final String ORG_ZSTACK_BAREMETAL2_CLUSTER_10000 = "ORG_ZSTACK_BAREMETAL2_CLUSTER_10000"; public static final String ORG_ZSTACK_BAREMETAL2_CLUSTER_10001 = "ORG_ZSTACK_BAREMETAL2_CLUSTER_10001"; From ecc93ebd9b3e7b5059e540c3eac503be504cf283 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 5 Mar 2026 20:34:25 +0800 Subject: [PATCH 25/77] [iscsi]: use platform compact hostId instead of storage bdc id for heartbeat offset ZSTAC-81797: iSCSI fencer used HeartbeatVolumeTO.hostId (storage bdc id, incremental up to 2000+) as heartbeat write offset multiplier. With 1MB heartbeat_required_space, host_id=2000 causes 2000MB offset, exceeding the 2GB heartbeat volume and failing heartbeat writes. Fix: use ExternalPrimaryStorageHostRefVO.hostId (platform-managed compact ID in [1, 999]) instead, same as CBD fencer already does. Resolves: ZSTAC-81797 Change-Id: Icac5ee2059df7cf93ca4bc23829ce855d6a7184c Co-Authored-By: Claude Opus 4.6 --- .../zstack/iscsi/kvm/KvmIscsiNodeServer.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java b/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java index 66936e05dc0..0f773d193e7 100644 --- a/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java +++ b/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java @@ -20,10 +20,8 @@ import org.zstack.header.host.HostInventory; import org.zstack.header.host.HostVO; import org.zstack.header.message.MessageReply; -import org.zstack.header.storage.addon.primary.BaseVolumeInfo; -import org.zstack.header.storage.addon.primary.HeartbeatVolumeTO; -import org.zstack.header.storage.addon.primary.HeartbeatVolumeTopology; -import org.zstack.header.storage.addon.primary.PrimaryStorageNodeSvc; +import org.zstack.header.storage.addon.primary.*; +import org.zstack.storage.addon.primary.ExternalHostIdGetter; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; @@ -38,6 +36,8 @@ import org.zstack.kvm.*; import org.zstack.storage.addon.primary.ExternalPrimaryStorageFactory; import org.zstack.utils.DebugUtils; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; import java.util.ArrayList; import java.util.List; @@ -49,6 +49,8 @@ public class KvmIscsiNodeServer implements Component, KVMStartVmExtensionPoint, VmInstanceMigrateExtensionPoint, KVMConvertVolumeExtensionPoint, KVMDetachVolumeExtensionPoint, KVMAttachVolumeExtensionPoint, KVMPreAttachIsoExtensionPoint, KvmSetupSelfFencerExtensionPoint { + private static final CLogger logger = Utils.getLogger(KvmIscsiNodeServer.class); + @Autowired private ExternalPrimaryStorageFactory extPsFactory; @@ -235,13 +237,24 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger trigger, Map data) { + ExternalPrimaryStorageHostRefVO ref = Q.New(ExternalPrimaryStorageHostRefVO.class) + .eq(ExternalPrimaryStorageHostRefVO_.hostUuid, param.getHostUuid()) + .eq(ExternalPrimaryStorageHostRefVO_.primaryStorageUuid, param.getPrimaryStorage().getUuid()) + .find(); + if (ref == null || ref.getHostId() == 0) { + logger.warn(String.format("not found hostId for hostUuid[%s] and primaryStorageUuid[%s]", + param.getHostUuid(), param.getPrimaryStorage().getUuid())); + ref = new ExternalHostIdGetter(999).getOrAllocateHostIdRef( + param.getHostUuid(), param.getPrimaryStorage().getUuid()); + } + KvmSetupSelfFencerCmd cmd = new KvmSetupSelfFencerCmd(); cmd.interval = param.getInterval(); cmd.maxAttempts = param.getMaxAttempts(); cmd.coveringPaths = heartbeatVol.getCoveringPaths(); cmd.heartbeatUrl = heartbeatVol.getInstallPath(); cmd.storageCheckerTimeout = param.getStorageCheckerTimeout(); - cmd.hostId = heartbeatVol.getHostId(); + cmd.hostId = ref.getHostId(); cmd.heartbeatRequiredSpace = heartbeatVol.getHeartbeatRequiredSpace(); cmd.hostUuid = param.getHostUuid(); cmd.strategy = param.getStrategy(); From 62cd8822b20c40868d4a76c7561a8f920eccf067 Mon Sep 17 00:00:00 2001 From: lianghy Date: Mon, 16 Mar 2026 10:37:41 +0800 Subject: [PATCH 26/77] [conf]: bump version to 5.5.12 DBImpact Resolves/Related: ZSTAC-82347 Change-Id: I6f6c616f6a656264696e7a7673636b656c737072 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index f36c3dfd747..6d937c2fb57 100755 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ MAJOR=5 MINOR=5 -UPDATE=6 +UPDATE=12 From aaf4e344182a794540f2cf1b8092291457c0dad9 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 16 Mar 2026 11:53:23 +0800 Subject: [PATCH 27/77] =?UTF-8?q?[errorcode]:=20simplify=20i18n?= =?UTF-8?q?=20=E2=80=94=20guarantee=20message=20is=20never=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture changes: - Platform.err(): populate ErrorCode.message at creation time with default locale, so message is guaranteed non-null from the start - RestServer: centralize localize in sendResponse(ApiResponse), remove 3 scattered localize calls and unused resolveLocaleFromRequest - sendReplyResponse: use ApiResponse overload instead of String overload for consistent serialization and centralized localization Robustness improvements: - GlobalErrorCodeI18nServiceImpl.localizeErrorCode: if i18n template not found, fall back to details then description (message never null) - Inner locale message maps wrapped with Collections.unmodifiableMap - LocaleUtils: replace manual parseAcceptLanguage with standard Locale.LanguageRange.parse() for RFC 7231 compliant q-value sorting Frontend contract: UI can now always read error.message for display. Change-Id: I169cfc1cd80c8061a6c59dbefe3fe9c034cdb538 Co-Authored-By: Claude Opus 4.6 --- .../main/java/org/zstack/core/Platform.java | 15 +++++ .../GlobalErrorCodeI18nServiceImpl.java | 17 +++-- .../zstack/core/errorcode/LocaleUtils.java | 66 +++++-------------- .../main/java/org/zstack/rest/RestServer.java | 22 +++---- .../integration/core/ErrorCodeI18nCase.groovy | 52 +++++++++++++++ .../errorcode/TestGlobalErrorCodeI18n.java | 23 ++++++- 6 files changed, 128 insertions(+), 67 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index ce5bada0d6c..dc7f6697150 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -17,6 +17,7 @@ import org.zstack.core.db.DatabaseGlobalProperty; import org.zstack.core.encrypt.EncryptRSA; import org.zstack.core.errorcode.ErrorFacade; +import org.zstack.core.errorcode.GlobalErrorCodeI18nService; import org.zstack.core.propertyvalidator.ValidatorTool; import org.zstack.core.search.SearchGlobalProperty; import org.zstack.core.statemachine.StateMachine; @@ -988,6 +989,20 @@ public static ErrorCode err(String globalErrorCode, Enum errCode, ErrorCode caus .toArray(String[]::new)); } + // populate message at creation time with default locale; + // RestServer will override with client's Accept-Language if different + try { + GlobalErrorCodeI18nService i18nService = getComponentLoader().getComponent(GlobalErrorCodeI18nService.class); + if (i18nService != null) { + i18nService.localizeErrorCode(result, org.zstack.core.errorcode.LocaleUtils.DEFAULT_LOCALE); + } + } catch (Exception e) { + // i18n service not initialized during early startup, use details as fallback + if (result.getMessage() == null) { + result.setMessage(details); + } + } + return result; } diff --git a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java index f4def1fc071..00b05e4330c 100644 --- a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java +++ b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java @@ -56,7 +56,7 @@ private void loadAllJsonFiles() { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); @SuppressWarnings("unchecked") Map messages = JSONObjectUtil.toObject(content, LinkedHashMap.class); - localeMessages.put(locale, messages); + localeMessages.put(locale, Collections.unmodifiableMap(messages)); logger.info(String.format("loaded %d i18n error messages for locale [%s]", messages.size(), locale)); } catch (Exception e) { @@ -125,26 +125,33 @@ private String formatTemplate(String template, String[] formatArgs) { @Override public void localizeErrorCode(ErrorCode error, String locale) { - if (error == null || locale == null) { + if (error == null) { return; } + String resolvedLocale = locale != null ? locale : LocaleUtils.DEFAULT_LOCALE; + if (error.getGlobalErrorCode() != null) { - String message = getLocalizedMessage(error.getGlobalErrorCode(), locale, error.getFormatArgs()); + String message = getLocalizedMessage(error.getGlobalErrorCode(), resolvedLocale, error.getFormatArgs()); if (message != null) { error.setMessage(message); } } + // guarantee: message is never null + if (error.getMessage() == null) { + error.setMessage(error.getDetails() != null ? error.getDetails() : error.getDescription()); + } + if (error.getCause() != null) { - localizeErrorCode(error.getCause(), locale); + localizeErrorCode(error.getCause(), resolvedLocale); } if (error instanceof ErrorCodeList) { List causes = ((ErrorCodeList) error).getCauses(); if (causes != null) { for (ErrorCode cause : causes) { - localizeErrorCode(cause, locale); + localizeErrorCode(cause, resolvedLocale); } } } diff --git a/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java index bb1bcbea3a6..d97e9c0f0d6 100644 --- a/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java +++ b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java @@ -30,6 +30,9 @@ public class LocaleUtils { * Parse Accept-Language header and return the best matching locale key * from the set of available locales. * + * Uses {@link Locale.LanguageRange#parse(String)} for RFC 7231 compliant + * parsing with proper q-value priority sorting. + * * @param acceptLanguage the Accept-Language header value * @param availableLocales the set of locale keys loaded from JSON files * @return the best matching locale key, or en_US as fallback @@ -39,18 +42,27 @@ public static String resolveLocale(String acceptLanguage, Set availableL return DEFAULT_LOCALE; } - List entries = parseAcceptLanguage(acceptLanguage); - for (LocaleEntry entry : entries) { - if (entry.quality <= 0) { + List ranges; + try { + ranges = Locale.LanguageRange.parse(acceptLanguage); + } catch (IllegalArgumentException e) { + logger.debug(String.format("failed to parse Accept-Language [%s]: %s", acceptLanguage, e.getMessage())); + return DEFAULT_LOCALE; + } + + // ranges are already sorted by q-value descending + for (Locale.LanguageRange range : ranges) { + if (range.getWeight() <= 0) { continue; } - String normalized = normalizeTag(entry.tag); + String tag = range.getRange(); + String normalized = normalizeTag(tag); if (availableLocales.contains(normalized)) { return normalized; } - String lang = entry.tag.split("[-_]")[0].toLowerCase(); + String lang = tag.split("[-_]")[0].toLowerCase(); String mapped = LANGUAGE_TO_LOCALE.get(lang); if (mapped != null && availableLocales.contains(mapped)) { return mapped; @@ -78,48 +90,4 @@ static String normalizeTag(String tag) { } return tag; } - - private static List parseAcceptLanguage(String header) { - List entries = new ArrayList<>(); - String[] parts = header.split(","); - for (String part : parts) { - part = part.trim(); - if (part.isEmpty()) { - continue; - } - String[] tagAndParams = part.split(";"); - if (tagAndParams.length == 0) { - continue; - } - String tag = tagAndParams[0].trim(); - if (tag.isEmpty()) { - continue; - } - double quality = 1.0; - for (int i = 1; i < tagAndParams.length; i++) { - String param = tagAndParams[i].trim(); - if (param.startsWith("q=")) { - try { - quality = Double.parseDouble(param.substring(2).trim()); - } catch (NumberFormatException e) { - logger.debug(String.format("failed to parse quality value [%s]: %s", param, e.getMessage())); - quality = 0; - } - } - } - entries.add(new LocaleEntry(tag, quality)); - } - entries.sort((a, b) -> Double.compare(b.quality, a.quality)); - return entries; - } - - private static class LocaleEntry { - final String tag; - final double quality; - - LocaleEntry(String tag, double quality) { - this.tag = tag; - this.quality = quality; - } - } } diff --git a/rest/src/main/java/org/zstack/rest/RestServer.java b/rest/src/main/java/org/zstack/rest/RestServer.java index 917ffaadcdf..cea6416e6c7 100755 --- a/rest/src/main/java/org/zstack/rest/RestServer.java +++ b/rest/src/main/java/org/zstack/rest/RestServer.java @@ -408,8 +408,11 @@ private void callWebHook(RequestData d) throws IllegalAccessException, NoSuchMet writeResponse(response, w, ret.getResult()); } else { + // localize with webhook caller's locale (message already populated by Platform.err) String locale = resolveLocale(); - i18nService.localizeErrorCode(evt.getError(), locale); + if (!LocaleUtils.DEFAULT_LOCALE.equals(locale)) { + i18nService.localizeErrorCode(evt.getError(), locale); + } response.setError(evt.getError()); } @@ -917,14 +920,18 @@ private void handleJobQuery(HttpServletRequest req, HttpServletResponse rsp) thr writeResponse(response, w, ret.getResult()); sendResponse(HttpStatus.OK.value(), response, rsp); } else { - String locale = resolveLocaleFromRequest(req); - i18nService.localizeErrorCode(evt.getError(), locale); response.setError(evt.getError()); sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), response, rsp); } } private void sendResponse(int statusCode, ApiResponse response, HttpServletResponse rsp) throws IOException { + // centralized localization: override message with client's preferred locale + if (response.getError() != null) { + String locale = resolveLocale(); + i18nService.localizeErrorCode(response.getError(), locale); + } + RequestInfo info = requestInfo.get(); if (requestLogger.isTraceEnabled() && needLog(info)) { String body = CloudBusGson.toJson(response); @@ -1428,19 +1435,12 @@ private String resolveLocale() { return LocaleUtils.resolveLocale(acceptLanguage, i18nService.getAvailableLocales()); } - private String resolveLocaleFromRequest(HttpServletRequest req) { - String acceptLanguage = req.getHeader("Accept-Language"); - return LocaleUtils.resolveLocale(acceptLanguage, i18nService.getAvailableLocales()); - } - private void sendReplyResponse(MessageReply reply, Api api, HttpServletResponse rsp) throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { ApiResponse response = new ApiResponse(); if (!reply.isSuccess()) { - String locale = resolveLocale(); - i18nService.localizeErrorCode(reply.getError(), locale); response.setError(reply.getError()); - sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), JSONObjectUtil.toJsonString(response), rsp); + sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), response, rsp); return; } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy index a437b1a0a79..53c7f67c864 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy @@ -1,7 +1,9 @@ package org.zstack.test.integration.core +import org.zstack.core.errorcode.GlobalErrorCodeI18nServiceImpl import org.zstack.core.errorcode.LocaleUtils import org.zstack.header.errorcode.ErrorCode +import org.zstack.header.errorcode.ErrorCodeList import org.zstack.testlib.SubCase class ErrorCodeI18nCase extends SubCase { @@ -24,8 +26,12 @@ class ErrorCodeI18nCase extends SubCase { testLocaleUtilsNoMatch() testLocaleUtilsCaseInsensitive() testLocaleUtilsMalformedHeader() + testLocaleUtilsComplexBrowserHeader() testErrorCodeCopyConstructor() testErrorCodeCopyConstructorWithNulls() + testMessageGuaranteeFallbackToDetails() + testMessageGuaranteeFallbackToDescription() + testMessageGuaranteeOnCauseChain() } @Override @@ -78,6 +84,14 @@ class ErrorCodeI18nCase extends SubCase { assert LocaleUtils.resolveLocale(";;;,,,", available) == "en_US" } + void testLocaleUtilsComplexBrowserHeader() { + def available = ["zh_CN", "en_US"] as Set + // real Chrome header: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 + assert LocaleUtils.resolveLocale("zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", available) == "zh_CN" + // q=0 means "not acceptable" — should be skipped + assert LocaleUtils.resolveLocale("zh-CN;q=0,en-US;q=1.0", available) == "en_US" + } + // ---- ErrorCode copy constructor ---- void testErrorCodeCopyConstructor() { @@ -127,4 +141,42 @@ class ErrorCodeI18nCase extends SubCase { assert copy.formatArgs == null } + // ---- message guarantee (localizeErrorCode always populates message) ---- + + void testMessageGuaranteeFallbackToDetails() { + def i18n = new GlobalErrorCodeI18nServiceImpl() + // no i18n JSON loaded, so getLocalizedMessage returns null + // localizeErrorCode should fall back to details + def error = new ErrorCode("SYS.1000", "System Error", "disk full on /dev/sda1") + assert error.getMessage() == null + + i18n.localizeErrorCode(error, "en_US") + + assert error.getMessage() == "disk full on /dev/sda1" + } + + void testMessageGuaranteeFallbackToDescription() { + def i18n = new GlobalErrorCodeI18nServiceImpl() + // no details, should fall back to description + def error = new ErrorCode("SYS.1000", "System Error") + assert error.getMessage() == null + + i18n.localizeErrorCode(error, "en_US") + + assert error.getMessage() == "System Error" + } + + void testMessageGuaranteeOnCauseChain() { + def i18n = new GlobalErrorCodeI18nServiceImpl() + def root = new ErrorCode("INTERNAL.1001", "Internal Error", "root cause detail") + def mid = new ErrorCode("SYS.1000", "System Error") + mid.setCause(root) + + i18n.localizeErrorCode(mid, "en_US") + + // both should have message populated + assert mid.getMessage() == "System Error" + assert root.getMessage() == "root cause detail" + } + } diff --git a/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java b/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java index 729e841c6d2..917cf44cbc2 100644 --- a/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java +++ b/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java @@ -128,8 +128,27 @@ public void testLocalizeErrorCodeList() { @Test public void testNoGlobalErrorCode() { ErrorCode error = new ErrorCode("SYS.1000", "test error"); - // no globalErrorCode set + // no globalErrorCode set — message should fall back to description i18nService.localizeErrorCode(error, "zh_CN"); - Assert.assertNull("message should remain null", error.getMessage()); + Assert.assertEquals("message should fall back to description", + "test error", error.getMessage()); + } + + @Test + public void testMessageGuaranteeFallbackToDetails() { + ErrorCode error = new ErrorCode("SYS.1000", "System Error", "disk full on /dev/sda1"); + i18nService.localizeErrorCode(error, "en_US"); + Assert.assertEquals("message should fall back to details", + "disk full on /dev/sda1", error.getMessage()); + } + + @Test + public void testMessageNeverNull() { + ErrorCode error = new ErrorCode("SYS.1000", "System Error"); + error.setDetails(null); + i18nService.localizeErrorCode(error, "en_US"); + Assert.assertNotNull("message must never be null after localizeErrorCode", + error.getMessage()); + Assert.assertEquals("System Error", error.getMessage()); } } \ No newline at end of file From 1d8a0593bba9fc0c96297533a409ff3f679c3f5b Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 9 Mar 2026 16:31:04 +0800 Subject: [PATCH 28/77] [ai]: add targetQueueKey column for eval task queuing Resolves: ZSTAC-68709 Change-Id: I66b2038656387bc3b3555db20cdfddb31d82d914 --- conf/db/upgrade/V5.5.12__schema.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 33709a74abc..8d29c9f95ec 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -121,4 +121,7 @@ ALTER TABLE `zstack`.`BareMetal2InstanceVO` ADD CONSTRAINT `fkBareMetal2InstanceVOGatewayVO1` FOREIGN KEY (`lastGatewayUuid`) REFERENCES `HostEO` (`uuid`) - ON DELETE SET NULL; \ No newline at end of file + ON DELETE SET NULL; + +-- ZSTAC-68709: Add targetQueueKey for evaluation task queuing per service endpoint +CALL ADD_COLUMN('ModelEvaluationTaskVO', 'targetQueueKey', 'TEXT', 1, NULL); From 34366a151f4e94935fb5352458111efdc70cc950 Mon Sep 17 00:00:00 2001 From: "jin.ma" Date: Mon, 16 Mar 2026 13:29:26 +0800 Subject: [PATCH 29/77] [conf]: use absolute path for ansible version check during upgrade upgrade from py2 venv to py3 venv, ensure_python3_venv recreates an empty venv with no ansible binary. The bare 'ansible --version' falls back to /usr/bin/ansible old wrapper which uses relative path, causing infinite recursion on kylin arm environments. Use absolute path $SYS_VIRENV_PATH/bin/ansible with existence check to avoid hitting the old wrapper. Resolves: ZSTAC-82619 Change-Id: I666461626876787762626b6f6261776d76767262 --- conf/tools/install.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/conf/tools/install.sh b/conf/tools/install.sh index ca15a67650f..3c6d218c41f 100755 --- a/conf/tools/install.sh +++ b/conf/tools/install.sh @@ -94,13 +94,8 @@ elif [ $tool = 'zstack-ctl' ]; then elif [ $tool = 'zstack-sys' ]; then SYS_VIRENV_PATH=/var/lib/zstack/virtualenv/zstacksys ensure_python3_venv "$SYS_VIRENV_PATH" - RE_INSTALL=false - . $SYS_VIRENV_PATH/bin/activate - if ! ansible --version | grep -q 'core 2.16.14'; then - deactivate - RE_INSTALL=true - fi - if $RE_INSTALL; then + # RE_INSTALL + if [ ! -x "$SYS_VIRENV_PATH/bin/ansible" ] || ! "$SYS_VIRENV_PATH/bin/ansible" --version 2>/dev/null | grep -q 'core 2.16.14'; then rm -rf $SYS_VIRENV_PATH && python3.11 -m venv $SYS_VIRENV_PATH || exit 1 . $SYS_VIRENV_PATH/bin/activate cd $cwd From 39a9a8489e957e8256c6c774831337c7b56f2ce3 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 16 Mar 2026 13:47:00 +0800 Subject: [PATCH 30/77] [longjob]: standardize progress detail fields to be unit-agnostic Resolves: ZSTAC-82318 Change-Id: If68d507a3eeb3f48458d9fbf61c357339688d43b --- .../core/progress/LongJobProgressDetail.java | 47 +- .../LongJobProgressDetailBuilder.java | 406 ++++++------------ .../org/zstack/sdk/LongJobProgressDetail.java | 38 +- 3 files changed, 194 insertions(+), 297 deletions(-) diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java index d27dc05cc4c..d4d5e1d56bd 100644 --- a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java @@ -21,11 +21,11 @@ public class LongJobProgressDetail { /** Human-readable reason for current state. */ private String stateReason; - /** Bytes already processed. */ - private Long processedBytes; + /** Amount already processed (unit described by the {@code unit} field). */ + private Long processed; - /** Total bytes to process. */ - private Long totalBytes; + /** Total amount to process (unit described by the {@code unit} field). */ + private Long total; /** Items already processed (e.g. files, chunks). */ private Long processedItems; @@ -33,8 +33,11 @@ public class LongJobProgressDetail { /** Total items to process. */ private Long totalItems; - /** Transfer speed in bytes/s. */ - private Long speedBytesPerSecond; + /** Processing speed per second (unit described by the {@code unit} field). */ + private Long speed; + + /** Unit for processed/total/speed, e.g. "bytes", "items", "steps". */ + private String unit; /** Estimated remaining time in seconds. */ private Long estimatedRemainingSeconds; @@ -77,20 +80,20 @@ public void setStateReason(String stateReason) { this.stateReason = stateReason; } - public Long getProcessedBytes() { - return processedBytes; + public Long getProcessed() { + return processed; } - public void setProcessedBytes(Long processedBytes) { - this.processedBytes = processedBytes; + public void setProcessed(Long processed) { + this.processed = processed; } - public Long getTotalBytes() { - return totalBytes; + public Long getTotal() { + return total; } - public void setTotalBytes(Long totalBytes) { - this.totalBytes = totalBytes; + public void setTotal(Long total) { + this.total = total; } public Long getProcessedItems() { @@ -109,12 +112,20 @@ public void setTotalItems(Long totalItems) { this.totalItems = totalItems; } - public Long getSpeedBytesPerSecond() { - return speedBytesPerSecond; + public Long getSpeed() { + return speed; + } + + public void setSpeed(Long speed) { + this.speed = speed; + } + + public String getUnit() { + return unit; } - public void setSpeedBytesPerSecond(Long speedBytesPerSecond) { - this.speedBytesPerSecond = speedBytesPerSecond; + public void setUnit(String unit) { + this.unit = unit; } public Long getEstimatedRemainingSeconds() { diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java index 4c9a09b2580..3de92b7c473 100644 --- a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java @@ -1,264 +1,142 @@ -package org.zstack.header.core.progress; - -import org.zstack.utils.Utils; -import org.zstack.utils.gson.JSONObjectUtil; -import org.zstack.utils.logging.CLogger; - -import java.util.HashMap; -import java.util.Map; - -/** - * Parses TaskProgressVO.opaque (free-form JSON) into a typed LongJobProgressDetail. - * - * Three known opaque formats are handled: - * Format 1 — VM migration: {"remain":N, "total":N, "speed":N, "remaining_migration_time":N} - * Format 2 — AI download: {"data": ""} where the inner JSON has - * {state, progress:{percent,downloaded_bytes,total_bytes, - * speed_bytes_per_second,estimated_remaining_seconds, - * downloaded_files,total_files,stage}, state_reason} - * Format 3 — unknown: entire map goes into LongJobProgressDetail.extra - * - * Each format is tried independently. Failures in one format don't affect others. - */ -public class LongJobProgressDetailBuilder { - private static final CLogger logger = Utils.getLogger(LongJobProgressDetailBuilder.class); - - private LongJobProgressDetailBuilder() {} - - /** - * Build a LongJobProgressDetail from a TaskProgressVO. - * Returns null if opaque is null/empty or all parsers fail. - */ - public static LongJobProgressDetail fromTaskProgressVO(TaskProgressVO vo) { - if (vo == null || vo.getOpaque() == null || vo.getOpaque().isEmpty()) { - return null; - } - - Map raw; - try { - raw = JSONObjectUtil.toObject(vo.getOpaque(), HashMap.class); - } catch (Exception e) { - logger.trace("LongJobProgressDetailBuilder: opaque is not a JSON object, skipping: " + vo.getOpaque(), e); - return null; - } - - if (raw == null || raw.isEmpty()) { - return null; - } - - // Try Format 2 first: AI download wraps everything under "data" key - if (raw.containsKey("data")) { - LongJobProgressDetail detail = tryParseAiDownloadFormat(raw); - if (detail != null) { - return detail; - } - } - - // Try Format 1: VM migration with remain/total/speed keys - if (raw.containsKey("remain") && raw.containsKey("total")) { - LongJobProgressDetail detail = tryParseVmMigrationFormat(raw); - if (detail != null) { - return detail; - } - } - - // Format 3: unknown — put everything into extra - return parseAsExtra(raw); - } - - /** - * Format 1: VM migration opaque - * {"remain": 1234567, "total": 9999999, "speed": 102400, "remaining_migration_time": 30} - * remain = bytes still to transfer; processed = total - remain - */ - private static LongJobProgressDetail tryParseVmMigrationFormat(Map raw) { - try { - LongJobProgressDetail detail = new LongJobProgressDetail(); - detail.setStage("migrating"); - - Number total = toNumber(raw.get("total")); - Number remain = toNumber(raw.get("remain")); - Number speed = toNumber(raw.get("speed")); - Number remainingTime = toNumber(raw.get("remaining_migration_time")); - - if (total != null) { - detail.setTotalBytes(total.longValue()); - } - if (total != null && remain != null) { - long processed = Math.max(0L, total.longValue() - remain.longValue()); - detail.setProcessedBytes(processed); - if (total.longValue() > 0) { - detail.setPercent((int) Math.min(100, Math.round(processed * 100.0 / total.longValue()))); - } - } - if (speed != null) { - detail.setSpeedBytesPerSecond(speed.longValue()); - } - if (remainingTime != null) { - detail.setEstimatedRemainingSeconds(remainingTime.longValue()); - } - - // Carry over any unrecognized keys into extra - Map extra = new HashMap<>(raw); - extra.remove("remain"); - extra.remove("total"); - extra.remove("speed"); - extra.remove("remaining_migration_time"); - if (!extra.isEmpty()) { - detail.setExtra(extra); - } - - return detail; - } catch (Exception e) { - logger.trace("LongJobProgressDetailBuilder: failed to parse VM migration format", e); - return null; - } - } - - /** - * Format 2: AI download opaque - * {"data": "{\"state\":\"downloading\", \"progress\":{\"percent\":42, \"processedBytes\":N, ...}}"} - * The "data" value is a JSON string (double-encoded). - */ - private static LongJobProgressDetail tryParseAiDownloadFormat(Map raw) { - try { - Object dataVal = raw.get("data"); - if (dataVal == null) { - return null; - } - - Map inner; - if (dataVal instanceof String) { - // double-encoded JSON string - inner = JSONObjectUtil.toObject((String) dataVal, HashMap.class); - } else if (dataVal instanceof Map) { - inner = (Map) dataVal; - } else { - return null; - } - - if (inner == null) { - return null; - } - - LongJobProgressDetail detail = new LongJobProgressDetail(); - Map extra = new HashMap<>(); - - // state field - Object stateVal = inner.get("state"); - if (stateVal instanceof String) { - detail.setState((String) stateVal); - } - - // progress sub-object - Object progressVal = inner.get("progress"); - if (progressVal instanceof Map) { - Map progress = (Map) progressVal; - - Number percent = toNumber(progress.get("percent")); - if (percent != null) { - detail.setPercent(Math.max(0, Math.min(100, (int) Math.round(percent.doubleValue())))); - } - - // AI agent uses snake_case field names - Number processedBytes = toNumber(progress.get("downloaded_bytes")); - if (processedBytes != null) { - detail.setProcessedBytes(processedBytes.longValue()); - } - - Number totalBytes = toNumber(progress.get("total_bytes")); - if (totalBytes != null) { - detail.setTotalBytes(totalBytes.longValue()); - } - - Number speed = toNumber(progress.get("speed_bytes_per_second")); - if (speed != null) { - detail.setSpeedBytesPerSecond(speed.longValue()); - } - - Number eta = toNumber(progress.get("estimated_remaining_seconds")); - if (eta != null) { - detail.setEstimatedRemainingSeconds(eta.longValue()); - } - - Number processedFiles = toNumber(progress.get("downloaded_files")); - if (processedFiles != null) { - detail.setProcessedItems(processedFiles.longValue()); - } - - Number totalFiles = toNumber(progress.get("total_files")); - if (totalFiles != null) { - detail.setTotalItems(totalFiles.longValue()); - } - - Object stage = progress.get("stage"); - if (stage instanceof String) { - detail.setStage((String) stage); - } - - // remaining progress fields go into extra - Map extraProgress = new HashMap<>(progress); - extraProgress.remove("percent"); - extraProgress.remove("downloaded_bytes"); - extraProgress.remove("total_bytes"); - extraProgress.remove("speed_bytes_per_second"); - extraProgress.remove("estimated_remaining_seconds"); - extraProgress.remove("downloaded_files"); - extraProgress.remove("total_files"); - extraProgress.remove("stage"); - extra.putAll(extraProgress); - } - - // stateReason field — can be String or Map (structured reason with code/description) - Object stateReason = inner.get("state_reason"); - if (stateReason instanceof String) { - detail.setStateReason((String) stateReason); - } else if (stateReason instanceof Map) { - detail.setStateReason(JSONObjectUtil.toJsonString(stateReason)); - } - - // preserve unknown keys from inner top-level - Map extraInner = new HashMap<>(inner); - extraInner.remove("state"); - extraInner.remove("progress"); - extraInner.remove("state_reason"); - extra.putAll(extraInner); - - // preserve unknown keys from raw outer-level - Map extraRaw = new HashMap<>(raw); - extraRaw.remove("data"); - extra.putAll(extraRaw); - - if (!extra.isEmpty()) { - detail.setExtra(extra); - } - - return detail; - } catch (Exception e) { - logger.trace("LongJobProgressDetailBuilder: failed to parse AI download format", e); - return null; - } - } - - /** - * Format 3: unknown — preserve the entire map as extra for UI passthrough. - */ - private static LongJobProgressDetail parseAsExtra(Map raw) { - LongJobProgressDetail detail = new LongJobProgressDetail(); - detail.setExtra(new HashMap<>(raw)); - return detail; - } - - private static Number toNumber(Object val) { - if (val instanceof Number) { - return (Number) val; - } - if (val instanceof String) { - try { - return Double.parseDouble((String) val); - } catch (NumberFormatException ignored) { - } - } - return null; - } -} +package org.zstack.header.core.progress; + +import org.zstack.utils.Utils; +import org.zstack.utils.gson.JSONObjectUtil; +import org.zstack.utils.logging.CLogger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parses TaskProgressVO.opaque (free-form JSON) into a typed LongJobProgressDetail. + * + * All agents now send a standardized camelCase format: + * {"processed":N, "total":N, "percent":N, "stage":"migrating", + * "speed":N, "estimatedRemainingSeconds":N, "state":"running", + * "stateReason":"...", "processedItems":N, "totalItems":N, "unit":"bytes"} + * + * The "unit" field tells the UI how to format processed/total/speed: + * "bytes" — byte quantities (format as KB/MB/GB) + * "items" — discrete items (format as count) + * "steps" — workflow steps + * + * Unknown keys are preserved in LongJobProgressDetail.extra so no data is silently dropped. + */ +public class LongJobProgressDetailBuilder { + private static final CLogger logger = Utils.getLogger(LongJobProgressDetailBuilder.class); + + private static final String[] KNOWN_KEYS = { + "processed", "total", "percent", "stage", "state", "stateReason", + "speed", "estimatedRemainingSeconds", "processedItems", "totalItems", "unit" + }; + + private LongJobProgressDetailBuilder() {} + + /** + * Build a LongJobProgressDetail from a TaskProgressVO. + * Returns null if opaque is null/empty or not valid JSON. + */ + public static LongJobProgressDetail fromTaskProgressVO(TaskProgressVO vo) { + if (vo == null || vo.getOpaque() == null || vo.getOpaque().isEmpty()) { + return null; + } + + Map raw; + try { + raw = JSONObjectUtil.toObject(vo.getOpaque(), HashMap.class); + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: opaque is not a JSON object, skipping: " + vo.getOpaque(), e); + return null; + } + + if (raw == null || raw.isEmpty()) { + return null; + } + + try { + LongJobProgressDetail detail = new LongJobProgressDetail(); + + Number processed = toNumber(raw.get("processed")); + if (processed != null) { + detail.setProcessed(processed.longValue()); + } + + Number total = toNumber(raw.get("total")); + if (total != null) { + detail.setTotal(total.longValue()); + } + + Number percent = toNumber(raw.get("percent")); + if (percent != null) { + detail.setPercent(Math.max(0, Math.min(100, (int) Math.round(percent.doubleValue())))); + } + + Object stage = raw.get("stage"); + if (stage instanceof String) { + detail.setStage((String) stage); + } + + Object state = raw.get("state"); + if (state instanceof String) { + detail.setState((String) state); + } + + Object stateReason = raw.get("stateReason"); + if (stateReason instanceof String) { + detail.setStateReason((String) stateReason); + } + + Number speed = toNumber(raw.get("speed")); + if (speed != null) { + detail.setSpeed(speed.longValue()); + } + + Number eta = toNumber(raw.get("estimatedRemainingSeconds")); + if (eta != null) { + detail.setEstimatedRemainingSeconds(eta.longValue()); + } + + Number processedItems = toNumber(raw.get("processedItems")); + if (processedItems != null) { + detail.setProcessedItems(processedItems.longValue()); + } + + Number totalItems = toNumber(raw.get("totalItems")); + if (totalItems != null) { + detail.setTotalItems(totalItems.longValue()); + } + + Object unit = raw.get("unit"); + if (unit instanceof String) { + detail.setUnit((String) unit); + } + + // Carry over any unrecognized keys into extra + Map extra = new HashMap<>(raw); + for (String key : KNOWN_KEYS) { + extra.remove(key); + } + if (!extra.isEmpty()) { + detail.setExtra(extra); + } + + return detail; + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: failed to parse standard format", e); + return null; + } + } + + private static Number toNumber(Object val) { + if (val instanceof Number) { + return (Number) val; + } + if (val instanceof String) { + try { + return Double.parseDouble((String) val); + } catch (NumberFormatException ignored) { + } + } + return null; + } +} diff --git a/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java index c37bc20795b..788c12fa136 100644 --- a/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java +++ b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java @@ -36,20 +36,20 @@ public java.lang.String getStateReason() { return this.stateReason; } - public java.lang.Long processedBytes; - public void setProcessedBytes(java.lang.Long processedBytes) { - this.processedBytes = processedBytes; + public java.lang.Long processed; + public void setProcessed(java.lang.Long processed) { + this.processed = processed; } - public java.lang.Long getProcessedBytes() { - return this.processedBytes; + public java.lang.Long getProcessed() { + return this.processed; } - public java.lang.Long totalBytes; - public void setTotalBytes(java.lang.Long totalBytes) { - this.totalBytes = totalBytes; + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; } - public java.lang.Long getTotalBytes() { - return this.totalBytes; + public java.lang.Long getTotal() { + return this.total; } public java.lang.Long processedItems; @@ -68,12 +68,20 @@ public java.lang.Long getTotalItems() { return this.totalItems; } - public java.lang.Long speedBytesPerSecond; - public void setSpeedBytesPerSecond(java.lang.Long speedBytesPerSecond) { - this.speedBytesPerSecond = speedBytesPerSecond; + public java.lang.Long speed; + public void setSpeed(java.lang.Long speed) { + this.speed = speed; } - public java.lang.Long getSpeedBytesPerSecond() { - return this.speedBytesPerSecond; + public java.lang.Long getSpeed() { + return this.speed; + } + + public java.lang.String unit; + public void setUnit(java.lang.String unit) { + this.unit = unit; + } + public java.lang.String getUnit() { + return this.unit; } public java.lang.Long estimatedRemainingSeconds; From dc67d7293f34a32970ed5f654c035b46c55a3193 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 16 Mar 2026 16:04:48 +0800 Subject: [PATCH 31/77] [header]: fix __example__ method names for LongJobProgressDetail setProcessedBytes/setTotalBytes/setSpeedBytesPerSecond renamed to setProcessed/setTotal/setSpeed in unit-agnostic refactor. Add setUnit("bytes") to preserve semantic clarity. Related: ZSTAC-82318 Change-Id: Ied29ac01d488062a03d792378432522fbf0a523f Co-Authored-By: Claude Opus 4.6 (1M context) --- .../header/core/progress/APIGetTaskProgressReply.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java index 880e8cda1a8..3479c83e909 100755 --- a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java +++ b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java @@ -35,9 +35,10 @@ public static APIGetTaskProgressReply __example__() { detail.setPercent(42); detail.setStage("downloading"); detail.setState("running"); - detail.setProcessedBytes(440401920L); - detail.setTotalBytes(1073741824L); - detail.setSpeedBytesPerSecond(10485760L); + detail.setProcessed(440401920L); + detail.setTotal(1073741824L); + detail.setUnit("bytes"); + detail.setSpeed(10485760L); detail.setEstimatedRemainingSeconds(60L); inv.setProgressDetail(detail); From ed8c99b321a93a4bf7af5e999cfadb72ccb78c13 Mon Sep 17 00:00:00 2001 From: "jin.ma" Date: Mon, 16 Mar 2026 17:25:35 +0800 Subject: [PATCH 32/77] [conf]: retry rm -rf virtualenv to avoid race with zstack_service_exporter Resolves: ZSTAC-82619 Change-Id: I581385808fe4d942369b98f175eb8fbbdb382a0d --- conf/tools/install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conf/tools/install.sh b/conf/tools/install.sh index 3c6d218c41f..8a759869850 100755 --- a/conf/tools/install.sh +++ b/conf/tools/install.sh @@ -39,7 +39,9 @@ ensure_python3_venv() { if [ -d "$venv_path" ] && [ -x "$venv_path/bin/python3.11" ]; then return 0 fi - rm -rf "$venv_path" && python3.11 -m venv "$venv_path" || exit 1 + # retry once: rm -rf may fail if zstack_service_exporter is regenerating .pyc concurrently + rm -rf "$venv_path" || rm -rf "$venv_path" || exit 1 + python3.11 -m venv "$venv_path" || exit 1 } From d27439a012aa4d778cb04448b2650bae3b90dadc Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 16 Mar 2026 23:38:21 +0800 Subject: [PATCH 33/77] [vmScheduling]: change GET scheduling APIs from POST to GET method (ZSTAC-71075) Resolves: ZSTAC-71075 Change-Id: I21ebd76ba332995252f67f4cacf78022587a215a --- .../zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java | 4 ++-- .../sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java b/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java index 6a15d7c436e..78650d6e7f9 100644 --- a/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java +++ b/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java @@ -84,11 +84,11 @@ protected Map getNonAPIParameterMap() { protected RestInfo getRestInfo() { RestInfo info = new RestInfo(); - info.httpMethod = "POST"; + info.httpMethod = "GET"; info.path = "/get/vmSchedulingRules/conflict/state"; info.needSession = true; info.needPoll = false; - info.parameterName = "params"; + info.parameterName = ""; return info; } diff --git a/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java b/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java index 83ae01b33e5..1c9c6a69ab0 100644 --- a/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java +++ b/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java @@ -87,11 +87,11 @@ protected Map getNonAPIParameterMap() { protected RestInfo getRestInfo() { RestInfo info = new RestInfo(); - info.httpMethod = "POST"; + info.httpMethod = "GET"; info.path = "/get/vms/schedulingState/from/SchedulingRule"; info.needSession = true; info.needPoll = false; - info.parameterName = "params"; + info.parameterName = ""; return info; } From 1dc68d2e465f0021b42db340bdadea786b60b35a Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 17 Mar 2026 11:01:26 +0800 Subject: [PATCH 34/77] [errorcode]: revert sendReplyResponse to use JSONObjectUtil serialization CloudBusGson.httpGson does not disableHtmlEscaping(), causing single quotes in error details to be escaped as \u0027. While valid JSON, the SDK deserializes these literally, breaking string assertions in tests like BatchAddBareMetal2ChassisCase that check error.details with contains("'reboot' must be 'No'"). Restore JSONObjectUtil.toJsonString() for the sendReplyResponse error path while keeping the centralized localization logic. Co-Authored-By: ye.zou --- rest/src/main/java/org/zstack/rest/RestServer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest/src/main/java/org/zstack/rest/RestServer.java b/rest/src/main/java/org/zstack/rest/RestServer.java index cea6416e6c7..ee3dbb0b355 100755 --- a/rest/src/main/java/org/zstack/rest/RestServer.java +++ b/rest/src/main/java/org/zstack/rest/RestServer.java @@ -1439,8 +1439,13 @@ private void sendReplyResponse(MessageReply reply, Api api, HttpServletResponse ApiResponse response = new ApiResponse(); if (!reply.isSuccess()) { + String locale = resolveLocale(); + i18nService.localizeErrorCode(reply.getError(), locale); response.setError(reply.getError()); - sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), response, rsp); + // use JSONObjectUtil (which disables HTML escaping) to keep the same + // serialization behavior as before; CloudBusGson.httpGson escapes '\'' to + // '\u0027' which breaks SDK-side string assertions (ZSTAC-71075 etc.) + sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), JSONObjectUtil.toJsonString(response), rsp); return; } From 5ffa56e570bbc43627b280493824202e7cbc728a Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 5 Feb 2026 19:43:22 +0800 Subject: [PATCH 35/77] [network]: set nic ip out of l3 cidr scope APIImpact DBImpact GlobalConfigImpact Resolves: ZSTAC-81969 Change-Id: I736e77646266696e646271766d7170627378796b --- .gitignore | 2 + .../zstack/compute/vm/StaticIpOperator.java | 126 ++- .../zstack/compute/vm/VmCascadeExtension.java | 8 +- .../compute/vm/VmInstanceApiInterceptor.java | 594 ++++++----- .../org/zstack/compute/vm/VmInstanceBase.java | 144 ++- .../org/zstack/compute/vm/VmSystemTags.java | 5 + conf/db/upgrade/V5.5.12__schema.sql | 53 + .../header/network/l3/AllocateIpMsg.java | 40 + .../header/network/l3/IpAllocateMessage.java | 16 + .../header/network/l3/UsedIpInventory.java | 10 + .../zstack/header/network/l3/UsedIpVO.java | 13 +- .../zstack/header/network/l3/UsedIpVO_.java | 2 + .../header/vm/APIChangeVmNicNetworkMsg.java | 27 +- .../APIChangeVmNicNetworkMsgDoc_zh_cn.groovy | 9 + .../zstack/header/vm/APISetVmStaticIpMsg.java | 12 + .../vm/APISetVmStaticIpMsgDoc_zh_cn.groovy | 9 + .../header/vm/ChangeVmNicNetworkMsg.java | 10 + .../zstack/header/vm/SetVmStaticIpMsg.java | 11 + .../zstack/header/vm/VmInstanceConstant.java | 1 + .../org/zstack/network/l3/IpRangeHelper.java | 89 ++ .../org/zstack/network/l3/L3BasicNetwork.java | 55 +- .../network/l3/L3NetworkApiInterceptor.java | 42 + .../network/l3/L3NetworkManagerImpl.java | 5 +- .../network/l3/L3NetworkSystemTags.java | 1 - .../network/l3/NormalIpRangeFactory.java | 4 +- .../java/org/zstack/network/l3/zstack ipam.md | 139 +++ .../zstack/network/service/DhcpExtension.java | 25 +- .../service/NetworkServiceManager.java | 10 + .../service/NetworkServiceManagerImpl.java | 33 + .../zstack/appliancevm/ApplianceVmNicTO.java | 17 +- .../service/eip/EipApiInterceptor.java | 17 + .../network/service/eip/EipManagerImpl.java | 10 + .../network/service/flat/FlatDhcpBackend.java | 2 +- .../network/service/flat/FlatEipBackend.java | 11 +- .../lb/LoadBalancerApiInterceptor.java | 50 + .../network/service/lb/LoadBalancerBase.java | 16 + .../PortForwardingApiInterceptor.java | 24 + .../PortForwardingManagerImpl.java | 12 +- .../VirtualRouterManagerImpl.java | 7 +- .../zstack/sdk/ChangeVmNicNetworkAction.java | 3 + .../org/zstack/sdk/SetVmStaticIpAction.java | 3 + .../java/org/zstack/sdk/UsedIpInventory.java | 8 + test/pom.xml | 8 +- .../flat/FlatChangeVmIpOutsideCidrCase.groovy | 923 ++++++++++++++++++ ...licNetworkChangeVmIpOutsideCidrCase.groovy | 443 +++++++++ .../main/java/org/zstack/testlib/Test.groovy | 14 + .../CloudOperationsErrorCode.java | 35 + .../utils/network/NicIpAddressInfo.java | 4 + 48 files changed, 2800 insertions(+), 302 deletions(-) create mode 100644 network/src/main/java/org/zstack/network/l3/zstack ipam.md create mode 100644 test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy create mode 100644 test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy diff --git a/.gitignore b/.gitignore index 641f731fe03..e823298f7f6 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ envDSLTree test/zstack-integration-test-result/ premium/test-premium/zstack-api.log **/bin/ +CLAUDE.md +.claude/* diff --git a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java index 86ca327ae93..3ebd89f5a57 100755 --- a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java @@ -80,11 +80,8 @@ public Map getNicNetworkInfoBySystemTag(List s if(VmSystemTags.STATIC_IP.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.STATIC_IP.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.STATIC_IP_L3_UUID_TOKEN); - NicIpAddressInfo nicIpAddressInfo = ret.get(l3Uuid); - if (nicIpAddressInfo == null) { - ret.put(l3Uuid, new NicIpAddressInfo("", "", "", - "", "", "")); - } + ret.computeIfAbsent(l3Uuid, k -> new NicIpAddressInfo("", "", "", + "", "", "")); String ip = token.get(VmSystemTags.STATIC_IP_TOKEN); ip = IPv6NetworkUtils.ipv6TagValueToAddress(ip); if (NetworkUtils.isIpv4Address(ip)) { @@ -109,30 +106,42 @@ public Map getNicNetworkInfoBySystemTag(List s continue; } ret.get(l3Uuid).ipv4Gateway = token.get(VmSystemTags.IPV4_GATEWAY_TOKEN); - } - if(VmSystemTags.IPV4_NETMASK.isMatch(sysTag)) { + } else if(VmSystemTags.IPV4_NETMASK.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.IPV4_NETMASK.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN); if (ret.get(l3Uuid) == null) { continue; } ret.get(l3Uuid).ipv4Netmask = token.get(VmSystemTags.IPV4_NETMASK_TOKEN); - } - if(VmSystemTags.IPV6_GATEWAY.isMatch(sysTag)) { + } else if(VmSystemTags.IPV6_GATEWAY.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.IPV6_GATEWAY.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN); if (ret.get(l3Uuid) == null) { continue; } ret.get(l3Uuid).ipv6Gateway = IPv6NetworkUtils.ipv6TagValueToAddress(token.get(VmSystemTags.IPV6_GATEWAY_TOKEN)); - } - if(VmSystemTags.IPV6_PREFIX.isMatch(sysTag)) { + } else if(VmSystemTags.IPV6_PREFIX.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.IPV6_PREFIX.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN); if (ret.get(l3Uuid) == null) { continue; } ret.get(l3Uuid).ipv6Prefix = token.get(VmSystemTags.IPV6_PREFIX_TOKEN); + } else if(VmSystemTags.STATIC_DNS.isMatch(sysTag)) { + Map token = TagUtils.parse(VmSystemTags.STATIC_DNS.getTagFormat(), sysTag); + String l3Uuid = token.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (ret.get(l3Uuid) == null) { + continue; + } + String dnsStr = token.get(VmSystemTags.STATIC_DNS_TOKEN); + if (dnsStr != null && !dnsStr.isEmpty()) { + // Convert back from tag value: replace '--' with '::' for IPv6 addresses + List dnsList = new ArrayList<>(); + for (String dns : dnsStr.split(",")) { + dnsList.add(IPv6NetworkUtils.ipv6TagValueToAddress(dns)); + } + ret.get(l3Uuid).dnsAddresses = dnsList; + } } } @@ -222,6 +231,49 @@ public void deleteStaticIpByL3NetworkUuid(String l3Uuid) { ))); } + public void setStaticDns(String vmUuid, String l3Uuid, List dnsAddresses) { + if (dnsAddresses == null || dnsAddresses.isEmpty()) { + deleteStaticDnsByVmUuidAndL3Uuid(vmUuid, l3Uuid); + return; + } + + // Convert IPv6 addresses: replace '::' with '--' to avoid conflict with system tag delimiter + List tagSafeDns = new ArrayList<>(); + for (String dns : dnsAddresses) { + tagSafeDns.add(IPv6NetworkUtils.ipv6AddessToTagValue(dns)); + } + String dnsStr = String.join(",", tagSafeDns); + + SimpleQuery q = dbf.createQuery(SystemTagVO.class); + q.select(SystemTagVO_.uuid); + q.add(SystemTagVO_.resourceType, Op.EQ, VmInstanceVO.class.getSimpleName()); + q.add(SystemTagVO_.resourceUuid, Op.EQ, vmUuid); + q.add(SystemTagVO_.tag, Op.LIKE, TagUtils.tagPatternToSqlPattern(VmSystemTags.STATIC_DNS.instantiateTag( + map(e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid)) + ))); + String tagUuid = q.findValue(); + + if (tagUuid == null) { + SystemTagCreator creator = VmSystemTags.STATIC_DNS.newSystemTagCreator(vmUuid); + creator.setTagByTokens(map( + e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.STATIC_DNS_TOKEN, dnsStr) + )); + creator.create(); + } else { + VmSystemTags.STATIC_DNS.updateByTagUuid(tagUuid, VmSystemTags.STATIC_DNS.instantiateTag(map( + e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.STATIC_DNS_TOKEN, dnsStr) + ))); + } + } + + public void deleteStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) { + VmSystemTags.STATIC_DNS.delete(vmUuid, TagUtils.tagPatternToSqlPattern(VmSystemTags.STATIC_DNS.instantiateTag( + map(e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid)) + ))); + } + public Map getNicStaticIpMap(List nicStaticIpList) { Map nicStaticIpMap = new HashMap<>(); if (nicStaticIpList != null) { @@ -263,14 +315,14 @@ public boolean isIpChange(String vmUuid, String l3Uuid) { return false; } - public Boolean checkIpRangeConflict(VmNicVO nicVO){ + public Boolean isNicIpInL3IpRanges(VmNicVO nicVO){ if (Q.New(IpRangeVO.class).eq(IpRangeVO_.l3NetworkUuid, nicVO.getL3NetworkUuid()).list().isEmpty()) { - return Boolean.FALSE; + return Boolean.TRUE; } if (getIpRangeUuid(nicVO.getL3NetworkUuid(), nicVO.getIp()) == null) { - return Boolean.TRUE; + return Boolean.FALSE; } - return Boolean.FALSE; + return Boolean.TRUE; } public String getIpRangeUuid(String l3Uuid, String ip) { @@ -297,10 +349,19 @@ public String getIpRangeUuid(String l3Uuid, String ip) { return null; } + public NormalIpRangeVO findMatchedNormalIpRange(String l3Uuid, String ip) { + String rangeUuid = getIpRangeUuid(l3Uuid, ip); + if (rangeUuid == null) { + return null; + } + return dbf.findByUuid(rangeUuid, NormalIpRangeVO.class); + } + public void checkIpAvailability(String l3Uuid, String ip) { CheckIpAvailabilityMsg cmsg = new CheckIpAvailabilityMsg(); cmsg.setIp(ip); cmsg.setL3NetworkUuid(l3Uuid); + cmsg.setIpRangeCheck(false); bus.makeLocalServiceId(cmsg, L3NetworkConstant.SERVICE_ID); MessageReply r = bus.call(cmsg); if (!r.isSuccess()) { @@ -334,10 +395,7 @@ public List fillUpStaticIpInfoToVmNics(Map sta } if (!StringUtils.isEmpty(nicIp.ipv4Address)) { - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4) - .limit(1).find(); + NormalIpRangeVO ipRangeVO = findMatchedNormalIpRange(l3Uuid, nicIp.ipv4Address); if (ipRangeVO == null) { if (StringUtils.isEmpty(nicIp.ipv4Netmask)) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10310, "netmask must be set")); @@ -349,8 +407,10 @@ public List fillUpStaticIpInfoToVmNics(Map sta e(VmSystemTags.IPV4_NETMASK_TOKEN, ipRangeVO.getNetmask())) )); } else if (!nicIp.ipv4Netmask.equals(ipRangeVO.getNetmask())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10311, "netmask error, expect: %s, got: %s", - ipRangeVO.getNetmask(), nicIp.ipv4Netmask)); + newSystags.add(VmSystemTags.IPV4_NETMASK.instantiateTag( + map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV4_NETMASK_TOKEN, nicIp.ipv4Netmask)) + )); } if (StringUtils.isEmpty(nicIp.ipv4Gateway)) { @@ -359,17 +419,16 @@ public List fillUpStaticIpInfoToVmNics(Map sta e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipRangeVO.getGateway())) )); } else if (!nicIp.ipv4Gateway.equals(ipRangeVO.getGateway())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10312, "gateway error, expect: %s, got: %s", - ipRangeVO.getGateway(), nicIp.ipv4Gateway)); + newSystags.add(VmSystemTags.IPV4_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV4_GATEWAY_TOKEN, nicIp.ipv4Gateway)) + )); } } } if (!StringUtils.isEmpty(nicIp.ipv6Address)) { - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6) - .limit(1).find(); + NormalIpRangeVO ipRangeVO = findMatchedNormalIpRange(l3Uuid, nicIp.ipv6Address); if (ipRangeVO == null) { if (StringUtils.isEmpty(nicIp.ipv6Prefix)) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10313, "ipv6 prefix length must be set")); @@ -381,8 +440,10 @@ public List fillUpStaticIpInfoToVmNics(Map sta e(VmSystemTags.IPV6_PREFIX_TOKEN, ipRangeVO.getPrefixLen())) )); } else if (!nicIp.ipv6Prefix.equals(ipRangeVO.getPrefixLen().toString())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10314, "ipv6 prefix length error, expect: %s, got: %s", - ipRangeVO.getPrefixLen(), nicIp.ipv6Prefix)); + newSystags.add(VmSystemTags.IPV6_PREFIX.instantiateTag( + map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV6_PREFIX_TOKEN, nicIp.ipv6Prefix)) + )); } if (StringUtils.isEmpty(nicIp.ipv6Gateway)) { @@ -392,8 +453,11 @@ public List fillUpStaticIpInfoToVmNics(Map sta IPv6NetworkUtils.ipv6AddressToTagValue(ipRangeVO.getGateway()))) )); } else if (!nicIp.ipv6Gateway.equals(ipRangeVO.getGateway())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10315, "gateway error, expect: %s, got: %s", - ipRangeVO.getGateway(), nicIp.ipv6Gateway)); + newSystags.add(VmSystemTags.IPV6_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV6_GATEWAY_TOKEN, + IPv6NetworkUtils.ipv6AddressToTagValue(nicIp.ipv6Gateway))) + )); } } } 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..9bd98bd9511 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 ipv4Ranges, String l3Uuid, String defaultL3Uuid, int vmNicCount, UsedIpVO existingIp) { + boolean hasNetmask = StringUtils.isNotEmpty(userNetmask); + boolean hasGateway = StringUtils.isNotEmpty(userGateway); + + // case (a): both provided + if (hasNetmask && hasGateway) { + return new String[]{userNetmask, userGateway}; + } + + NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip, ipv4Ranges); + + // case (b): gateway provided, no netmask + if (hasGateway) { + if (matchedRange != null && matchedRange.getNetworkCidr() != null + && NetworkUtils.isIpv4InCidr(userGateway, matchedRange.getNetworkCidr())) { + return new String[]{matchedRange.getNetmask(), userGateway}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10323, + "gateway[%s] is provided but IP[%s] and gateway are not both in L3 network CIDR, netmask must be specified", + userGateway, ip)); + } + + // case (c): netmask provided, no gateway + if (hasNetmask) { + if (matchedRange != null && userNetmask.equals(matchedRange.getNetmask())) { + return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; + } + if (l3Uuid.equals(defaultL3Uuid) || vmNicCount == 1) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10324, + "netmask[%s] does not match L3 CIDR netmask and the NIC is the default or sole network, gateway must be specified", + userNetmask)); + } + return new String[]{userNetmask, ""}; + } + + // case (d): neither provided + if (existingIp != null && shouldUseExistingIpv4(ip, existingIp)) { + return new String[]{existingIp.getNetmask(), existingIp.getGateway()}; + } + if (matchedRange != null) { + return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10325, + "IP[%s] is outside all L3 network CIDRs and no existing IP parameters available, netmask and gateway must be specified", + ip)); + } + + /** + * Resolve IPv6 prefix and gateway based on 4 cases (mirrors IPv4 logic): + * (a) Both prefix+gateway provided: use user input as-is + * (b) Gateway provided, no prefix: if ip and gateway both in L3 CIDR, use CIDR prefix; else error + * (c) Prefix provided, no gateway: if prefix == CIDR prefix, use CIDR gateway; else if default/sole NIC, error; else gateway="" + * (d) Neither provided: if existingIp usable (APISetVmStaticIpMsg), use it; else if in L3 CIDR, use CIDR; else error + * + * @param existingIp pass null for APIChangeVmNicNetworkMsg (no existing IP on dest L3) + */ + private String[] resolveIpv6PrefixAndGateway(String ip6, String userPrefix, String userGateway, + List ipv6Ranges, String l3Uuid, String defaultL3Uuid, int vmNicCount, UsedIpVO existingIp) { + boolean hasPrefix = StringUtils.isNotEmpty(userPrefix); + boolean hasGateway = StringUtils.isNotEmpty(userGateway); + + // case (a): both provided + if (hasPrefix && hasGateway) { + return new String[]{userPrefix, userGateway}; + } + + NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip6, ipv6Ranges); + + // case (b): gateway provided, no prefix + if (hasGateway) { + if (matchedRange != null && matchedRange.getNetworkCidr() != null + && IPv6NetworkUtils.isIpv6InCidrRange(userGateway, matchedRange.getNetworkCidr())) { + return new String[]{matchedRange.getPrefixLen().toString(), userGateway}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10326, + "gateway[%s] is provided but IPv6[%s] and gateway are not both in L3 network CIDR, prefix must be specified", + userGateway, ip6)); + } + + // case (c): prefix provided, no gateway + if (hasPrefix) { + if (matchedRange != null && userPrefix.equals(matchedRange.getPrefixLen().toString())) { + return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; + } + if (l3Uuid.equals(defaultL3Uuid) || vmNicCount == 1) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10327, + "prefix[%s] does not match L3 CIDR prefix and the NIC is the default or sole network, gateway must be specified", + userPrefix)); + } + return new String[]{userPrefix, ""}; + } + + // case (d): neither provided + if (existingIp != null && shouldUseExistingIpv6(ip6, existingIp)) { + return new String[]{existingIp.getPrefixLen().toString(), existingIp.getGateway()}; + } + if (matchedRange != null) { + return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10328, + "IPv6[%s] is outside all L3 network CIDRs and no existing IP parameters available, prefix and gateway must be specified", + ip6)); + } + + /** + * Check whether an IP is already in use (using error code ORG_ZSTACK_COMPUTE_VM_10105). + */ + private void checkIpOccupied(String ip, String l3NetworkUuid) { + if (Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, ip).eq(UsedIpVO_.l3NetworkUuid, l3NetworkUuid).isExists()) { + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10105, + "the static IP[%s] has been occupied on the L3 network[uuid:%s]", ip, l3NetworkUuid)); + } + } + + /** + * Batch check whether multiple IPs are already in use. + */ + private void checkIpsOccupied(List ips, String l3NetworkUuid) { + if (ips == null || ips.isEmpty()) { + return; + } + + List occupiedIps = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, l3NetworkUuid) + .in(UsedIpVO_.ip, ips) + .select(UsedIpVO_.ip) + .listValues(); + + if (!occupiedIps.isEmpty()) { + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10105, + "the static IP%s has been occupied on the L3 network[uuid:%s]", occupiedIps, l3NetworkUuid)); + } + } + + private void setServiceId(APIMessage msg) { if (msg instanceof VmInstanceMessage) { VmInstanceMessage vmsg = (VmInstanceMessage) msg; @@ -287,99 +505,106 @@ private void validate(APIChangeVmNicNetworkMsg msg) { } new StaticIpOperator().validateSystemTagInApiMessage(msg); - Map> staticIps = new StaticIpOperator().getStaticIpbySystemTag(msg.getSystemTags()); - if (msg.getRequiredIpMap() != null) { - staticIps.computeIfAbsent(msg.getDestL3NetworkUuid(), k -> new ArrayList<>()).add(msg.getStaticIp()); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - List iprs = iprq.list(); - boolean found = false; - for (NormalIpRangeVO ipr : iprs) { - if (!ipr.getIpVersion().equals(NetworkUtils.getIpversion(msg.getStaticIp()))) { - continue; + // Resolve netmask/gateway for static IPs in systemTags, overriding what validateSystemTagInApiMessage may have set + { + String destL3Uuid = msg.getDestL3NetworkUuid(); + Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); + NicIpAddressInfo nicIpInfo = nicNetworkInfo.get(destL3Uuid); + if (nicIpInfo != null) { + List destIpv4Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, destL3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + List destIpv6Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, destL3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + String defaultL3Uuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findValue(); + int vmNicCount = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vmUuid).count().intValue(); + + // Remove existing netmask/gateway/prefix/ipv6Gateway tags for dest L3 from systemTags + if (msg.getSystemTags() != null) { + msg.getSystemTags().removeIf(tag -> + VmSystemTags.IPV4_NETMASK.isMatch(tag) || VmSystemTags.IPV4_GATEWAY.isMatch(tag) + || VmSystemTags.IPV6_PREFIX.isMatch(tag) || VmSystemTags.IPV6_GATEWAY.isMatch(tag)); } - if (NetworkUtils.isInRange(msg.getStaticIp(), ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; + // Resolve and add IPv4 netmask/gateway + if (StringUtils.isNotEmpty(nicIpInfo.ipv4Address)) { + String[] ipv4Result = resolveIpv4NetmaskAndGateway(nicIpInfo.ipv4Address, + nicIpInfo.ipv4Netmask, nicIpInfo.ipv4Gateway, + destIpv4Ranges, destL3Uuid, defaultL3Uuid, vmNicCount, null); + msg.getSystemTags().add(VmSystemTags.IPV4_NETMASK.instantiateTag( + map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, destL3Uuid), + e(VmSystemTags.IPV4_NETMASK_TOKEN, ipv4Result[0])))); + msg.getSystemTags().add(VmSystemTags.IPV4_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, destL3Uuid), + e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipv4Result[1])))); } - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; + // Resolve and add IPv6 prefix/gateway + if (StringUtils.isNotEmpty(nicIpInfo.ipv6Address)) { + String[] ipv6Result = resolveIpv6PrefixAndGateway(nicIpInfo.ipv6Address, + nicIpInfo.ipv6Prefix, nicIpInfo.ipv6Gateway, + destIpv6Ranges, destL3Uuid, defaultL3Uuid, vmNicCount, null); + msg.getSystemTags().add(VmSystemTags.IPV6_PREFIX.instantiateTag( + map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, destL3Uuid), + e(VmSystemTags.IPV6_PREFIX_TOKEN, ipv6Result[0])))); + msg.getSystemTags().add(VmSystemTags.IPV6_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, destL3Uuid), + e(VmSystemTags.IPV6_GATEWAY_TOKEN, + IPv6NetworkUtils.ipv6AddressToTagValue(ipv6Result[1]))))); + } } + } - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10104, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", msg.getStaticIp(), msg.getDestL3NetworkUuid())); - } + Map> staticIps = new StaticIpOperator().getStaticIpbySystemTag(msg.getSystemTags()); - SimpleQuery uq = dbf.createQuery(UsedIpVO.class); - uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - uq.add(UsedIpVO_.ip, Op.EQ, msg.getStaticIp()); - if (uq.isExists()) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10105, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", msg.getStaticIp(), msg.getDestL3NetworkUuid())); - } + // If staticIp parameter is provided, add it to the static IP list + if (msg.getStaticIp() != null) { + staticIps.computeIfAbsent(msg.getDestL3NetworkUuid(), k -> new ArrayList<>()).add(msg.getStaticIp()); } - for (Map.Entry> e : staticIps.entrySet()) { - if (!newAddedL3Uuids.contains(e.getKey())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10106, "static ip l3 uuid[%s] is not included in nic l3 [%s]", e.getKey(), newAddedL3Uuids)); - } + msg.setRequiredIpMap(new HashMap<>()); + // Unified loop: validate and set all static IPs together + for (Map.Entry> e : staticIps.entrySet()) { String l3Uuid = e.getKey(); List ips = e.getValue(); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, l3Uuid); - List iprs = iprq.list(); - - boolean found = false; - for (String staticIp : ips) { - int ipVersion = IPv6Constants.IPv4; - if (IPv6NetworkUtils.isIpv6Address(staticIp)) { - ipVersion = IPv6Constants.IPv6; - } - for (NormalIpRangeVO ipr : iprs) { - if (ipVersion != ipr.getIpVersion()) { - continue; - } - if (NetworkUtils.isInRange(staticIp, ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } + // Validate that the L3 network UUID is in the allowed list + if (!newAddedL3Uuids.contains(l3Uuid)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10106, + "static ip l3 uuid[%s] is not included in nic l3 [%s]", l3Uuid, newAddedL3Uuids)); + } - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10107, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", staticIp, l3Uuid)); - } + // Performance optimization: batch check IP occupation (one query instead of N) + checkIpsOccupied(ips, l3Uuid); - SimpleQuery uq = dbf.createQuery(UsedIpVO.class); - uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - uq.add(UsedIpVO_.ip, Op.EQ, msg.getStaticIp()); - if (uq.isExists()) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10108, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", staticIp, l3Uuid)); - } - } + // Set requiredIpMap (merged into this loop to eliminate redundant iteration) + msg.getRequiredIpMap().put(l3Uuid, ips); } - msg.setRequiredIpMap(new HashMap<>()); + validateDnsAddresses(msg.getDnsAddresses()); + } - for (Map.Entry> e : staticIps.entrySet()) { - msg.getRequiredIpMap().put(e.getKey(), e.getValue()); + private void validateDnsAddresses(List dnsAddresses) { + if (dnsAddresses == null || dnsAddresses.isEmpty()) { + return; } - final Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); - NicIpAddressInfo nicIpAddressInfo = nicNetworkInfo.get(msg.getDestL3NetworkUuid()); - if (nicIpAddressInfo != null) { - if (!nicIpAddressInfo.ipv4Address.isEmpty() && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, nicIpAddressInfo.ipv4Address).eq(UsedIpVO_.l3NetworkUuid, msg.getDestL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10109, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", nicIpAddressInfo.ipv4Address, msg.getDestL3NetworkUuid())); - } - if (!nicIpAddressInfo.ipv6Address.isEmpty() && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, IPv6NetworkUtils.getIpv6AddressCanonicalString(nicIpAddressInfo.ipv6Address)).eq(UsedIpVO_.l3NetworkUuid, msg.getDestL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10110, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", nicIpAddressInfo.ipv6Address, msg.getDestL3NetworkUuid())); + if (dnsAddresses.size() > VmInstanceConstant.MAXIMUM_NIC_DNS_NUMBER) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_COMPUTE_VM_10321, "at most %d DNS addresses are allowed, but got %d", + VmInstanceConstant.MAXIMUM_NIC_DNS_NUMBER, dnsAddresses.size())); + } + + for (String dns : dnsAddresses) { + if (!NetworkUtils.isIpv4Address(dns) && !IPv6NetworkUtils.isIpv6Address(dns)) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_COMPUTE_VM_10322, "invalid DNS address[%s], must be a valid IPv4 or IPv6 address", dns)); } } } @@ -570,58 +795,13 @@ private void validate(APIStartVmInstanceMsg msg) { } private void validateStaticIPv4(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, String ip) { - if (!NetworkUtils.isIpv4Address(ip)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10129, "%s is not a valid IPv4 address", ip)); - } - - for (UsedIpVO ipVo : vmNicVO.getUsedIps()) { - if (ipVo.getIpVersion() != IPv6Constants.IPv4) { - continue; - } - - if (ipVo.getL3NetworkUuid().equals(l3NetworkVO.getUuid())) { - if (ipVo.getIp().equals(ip)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10130, "ip address [%s] already set to vmNic [uuid:%s]", - ip, vmNicVO.getUuid())); - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - continue; - } - // check if the ip is in the ip range when ipam is enabled - NormalIpRangeVO rangeVO = dbf.findByUuid(ipVo.getIpRangeUuid(), NormalIpRangeVO.class); - if (!NetworkUtils.isIpv4InCidr(ip, rangeVO.getNetworkCidr())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10131, "ip address [%s] is not in ip range [%s]", - ip, rangeVO.getNetworkCidr())); - } - } - } + validateStaticIpCommon(vmNicVO, l3NetworkVO, ip, IPv6Constants.IPv4, + ORG_ZSTACK_COMPUTE_VM_10129, ORG_ZSTACK_COMPUTE_VM_10130); } private void validateStaticIPv6(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, String ip) { - if (!IPv6NetworkUtils.isIpv6Address(ip)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10132, "%s is not a valid IPv6 address", ip)); - } - - for (UsedIpVO ipVo : vmNicVO.getUsedIps()) { - if (ipVo.getIpVersion() != IPv6Constants.IPv6) { - continue; - } - - if (ipVo.getL3NetworkUuid().equals(l3NetworkVO.getUuid())) { - if (ip.equals(ipVo.getIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10133, "ip address [%s] already set to vmNic [uuid:%s]", - ip, vmNicVO.getUuid())); - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - continue; - } - NormalIpRangeVO rangeVO = dbf.findByUuid(ipVo.getIpRangeUuid(), NormalIpRangeVO.class); - if (!IPv6NetworkUtils.isIpv6InRange(ip, rangeVO.getStartIp(), rangeVO.getEndIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10134, "ip address [%s] is not in ip range [startIp %s, endIp %s]", - ip, rangeVO.getStartIp(), rangeVO.getEndIp())); - } - } - } + validateStaticIpCommon(vmNicVO, l3NetworkVO, ip, IPv6Constants.IPv6, + ORG_ZSTACK_COMPUTE_VM_10132, ORG_ZSTACK_COMPUTE_VM_10133); } private void validate(APISetVmStaticIpMsg msg) { @@ -639,15 +819,39 @@ private void validate(APISetVmStaticIpMsg msg) { .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); List vmNics = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, msg.getVmInstanceUuid()).list(); boolean l3Found = false; + + // Normalize IP addresses (avoid redundant calls) + String normalizedIp = null; + String normalizedIp6 = null; + UsedIpVO existingIpv4 = null; + UsedIpVO existingIpv6 = null; + for (VmNicVO nic : vmNics) { - l3Found = true; + if (msg.getL3NetworkUuid().equals(nic.getL3NetworkUuid())) { + l3Found = true; + } + + // Extract UsedIpVO records matching the same L3 and IP version from the NIC's existing IPs + for (UsedIpVO usedIp : nic.getUsedIps()) { + if (!msg.getL3NetworkUuid().equals(usedIp.getL3NetworkUuid())) { + continue; + } + + if (usedIp.getIpVersion() != null && usedIp.getIpVersion() == IPv6Constants.IPv4) { + existingIpv4 = usedIp; + } else if (usedIp.getIpVersion() != null && usedIp.getIpVersion() == IPv6Constants.IPv6) { + existingIpv6 = usedIp; + } + } if (msg.getIp() != null) { String ip = IPv6NetworkUtils.ipv6TagValueToAddress(msg.getIp()); if (NetworkUtils.isIpv4Address(ip)) { validateStaticIPv4(nic, l3NetworkVO, ip); + normalizedIp = ip; } else if (IPv6NetworkUtils.isIpv6Address(ip)) { validateStaticIPv6(nic, l3NetworkVO, ip); msg.setIp(ip); + normalizedIp6 = ip; } else { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10136, "static ip [%s] format error", msg.getIp())); } @@ -656,52 +860,38 @@ private void validate(APISetVmStaticIpMsg msg) { String ip6 = IPv6NetworkUtils.ipv6TagValueToAddress(msg.getIp6()); validateStaticIPv6(nic, l3NetworkVO, ip6); msg.setIp6(ip6); + normalizedIp6 = ip6; } } - if (msg.getIp() != null && !l3NetworkVO.enableIpAddressAllocation()) { - l3Found = true; - if (msg.getNetmask() == null) { - if (ipv4Ranges.isEmpty()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10137, "ipv4 address need a netmask")); - } else { - msg.setNetmask(ipv4Ranges.get(0).getNetmask()); - } - } - if (msg.getGateway() == null) { - if (ipv4Ranges.isEmpty()) { - msg.setGateway(""); - } else { - msg.setGateway(ipv4Ranges.get(0).getGateway()); - } - } - if (Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, msg.getIp()).eq(UsedIpVO_.l3NetworkUuid, msg.getL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10138, "ip address [%s] already set to vmNic", msg.getIp())); - } - } - if (msg.getIp6() != null && !l3NetworkVO.enableIpAddressAllocation()) { - l3Found = true; - if (msg.getIpv6Prefix() == null) { - if (ipv6Ranges.isEmpty()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10139, "ipv6 address need a prefix")); - } else { - msg.setIpv6Prefix(ipv6Ranges.get(0).getPrefixLen().toString()); - } - } - if (msg.getIpv6Gateway() == null) { - if (ipv6Ranges.isEmpty()) { - msg.setIpv6Gateway(""); - } else { - msg.setIpv6Gateway(ipv6Ranges.get(0).getGateway()); - } - } - if (Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, msg.getIp6()).eq(UsedIpVO_.l3NetworkUuid, msg.getL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10140, "ip address [%s] already set to vmNic", msg.getIp6())); - } - } + if (!l3Found) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10141, "the VM[uuid:%s] has no nic on the L3 network[uuid:%s]", msg.getVmInstanceUuid(), - msg.getL3NetworkUuid())); + msg.getL3NetworkUuid())); + } + + // Get the VM's default L3 network UUID for gateway enforcement + String defaultL3NetworkUuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) + .findValue(); + + // Fill parameters and check IP occupation + if (normalizedIp != null) { + String[] ipv4Result = resolveIpv4NetmaskAndGateway(normalizedIp, msg.getNetmask(), msg.getGateway(), + ipv4Ranges, msg.getL3NetworkUuid(), defaultL3NetworkUuid, vmNics.size(), existingIpv4); + msg.setNetmask(ipv4Result[0]); + msg.setGateway(ipv4Result[1]); + checkIpOccupied(normalizedIp, msg.getL3NetworkUuid()); + } + if (normalizedIp6 != null) { + String[] ipv6Result = resolveIpv6PrefixAndGateway(normalizedIp6, msg.getIpv6Prefix(), msg.getIpv6Gateway(), + ipv6Ranges, msg.getL3NetworkUuid(), defaultL3NetworkUuid, vmNics.size(), existingIpv6); + msg.setIpv6Prefix(ipv6Result[0]); + msg.setIpv6Gateway(ipv6Result[1]); + checkIpOccupied(normalizedIp6, msg.getL3NetworkUuid()); } + + validateDnsAddresses(msg.getDnsAddresses()); } private void validate(APIDeleteVmStaticIpMsg msg) { @@ -825,26 +1015,6 @@ private void validate(APICreateVmNicMsg msg) { } if (msg.getIp() != null) { - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); - List iprs = iprq.list(); - - boolean found = false; - for (NormalIpRangeVO ipr : iprs) { - if (NetworkUtils.isInRange(msg.getIp(), ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - - if (!l3VO.enableIpAddressAllocation()) { - found = true; - } - - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10154, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", msg.getIp(), msg.getL3NetworkUuid())); - } - SimpleQuery uq = dbf.createQuery(UsedIpVO.class); uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); uq.add(UsedIpVO_.ip, Op.EQ, msg.getIp()); @@ -936,29 +1106,6 @@ private void validate(APIAttachL3NetworkToVmMsg msg) { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); if (msg.getStaticIp() != null) { staticIps.computeIfAbsent(msg.getL3NetworkUuid(), k -> new ArrayList<>()).add(msg.getStaticIp()); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); - List iprs = iprq.list(); - - boolean found = false; - for (NormalIpRangeVO ipr : iprs) { - if (!ipr.getIpVersion().equals(NetworkUtils.getIpversion(msg.getStaticIp()))) { - continue; - } - - if (NetworkUtils.isInRange(msg.getStaticIp(), ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } - - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10164, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", msg.getStaticIp(), msg.getL3NetworkUuid())); - } SimpleQuery uq = dbf.createQuery(UsedIpVO.class); uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); @@ -975,33 +1122,8 @@ private void validate(APIAttachL3NetworkToVmMsg msg) { String l3Uuid = e.getKey(); List ips = e.getValue(); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, l3Uuid); - List iprs = iprq.list(); - boolean found = false; for (String staticIp : ips) { - int ipVersion = IPv6Constants.IPv4; - if (IPv6NetworkUtils.isIpv6Address(staticIp)) { - ipVersion = IPv6Constants.IPv6; - } - for (NormalIpRangeVO ipr : iprs) { - if (ipVersion != ipr.getIpVersion()) { - continue; - } - if (NetworkUtils.isInRange(staticIp, ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } - - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10167, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", staticIp, l3Uuid)); - } SimpleQuery uq = dbf.createQuery(UsedIpVO.class); uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); @@ -1424,7 +1546,7 @@ private void validate(NewVmInstanceMessage2 msg) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10206, "l3Network[uuid:%s] is Disabled, can not create vm on it", l3Uuid)); } if (system && (msg.getType() == null || VmInstanceConstant.USER_VM_TYPE.equals(msg.getType()))) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10207, "l3Network[uuid:%s] is system network, can not create user vm on it", l3Uuid)); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10207, "l3Network[uuid:%s] is system network, can not create user vm on it", l3Uuid)); } } } 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..58ff4e7b8d9 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -1046,7 +1046,46 @@ public String getName() { }); } - private void changeVmIp(final String l3Uuid, final Map staticIpMap, final Completion completion) { + static class IpOverrideInfo { + private String netmask; + private String gateway; + private String ipv6Gateway; + private String ipv6Prefix; + + public String getNetmask() { + return netmask; + } + + public void setNetmask(String netmask) { + this.netmask = netmask; + } + + public String getGateway() { + return gateway; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public String getIpv6Gateway() { + return ipv6Gateway; + } + + public void setIpv6Gateway(String ipv6Gateway) { + this.ipv6Gateway = ipv6Gateway; + } + + public String getIpv6Prefix() { + return ipv6Prefix; + } + + public void setIpv6Prefix(String ipv6Prefix) { + this.ipv6Prefix = ipv6Prefix; + } + } + + private void changeVmIp(final String l3Uuid, final Map staticIpMap, final IpOverrideInfo overrideInfo, final Completion completion) { final VmNicVO targetNic = CollectionUtils.find(self.getVmNics(), new Function() { @Override public VmNicVO call(VmNicVO arg) { @@ -1105,6 +1144,15 @@ public void run(final FlowTrigger trigger, Map data) { amsg.setL3NetworkUuid(l3Uuid); amsg.setRequiredIp(entry.getValue()); amsg.setIpVersion(entry.getKey()); + if (overrideInfo != null) { + if (entry.getKey() == IPv6Constants.IPv4) { + amsg.setNetmask(overrideInfo.getNetmask()); + amsg.setGateway(overrideInfo.getGateway()); + } else if (entry.getKey() == IPv6Constants.IPv6) { + amsg.setIpv6Gateway(overrideInfo.getIpv6Gateway()); + amsg.setIpv6Prefix(overrideInfo.getIpv6Prefix()); + } + } bus.makeTargetServiceIdByResourceUuid(amsg, L3NetworkConstant.SERVICE_ID, l3Uuid); bus.send(amsg, new CloudBusCallBack(trigger) { @Override @@ -3481,6 +3529,7 @@ private void handle(final APISetVmStaticIpMsg msg) { cmsg.setNetmask(msg.getNetmask()); cmsg.setIpv6Gateway(msg.getIpv6Gateway()); cmsg.setIpv6Prefix(msg.getIpv6Prefix()); + cmsg.setDnsAddresses(msg.getDnsAddresses()); bus.makeTargetServiceIdByResourceUuid(cmsg, VmInstanceConstant.SERVICE_ID, cmsg.getVmInstanceUuid()); bus.send(cmsg, new CloudBusCallBack(msg) { @Override @@ -3507,7 +3556,17 @@ public String getSyncSignature() { public void run(final SyncTaskChain chain) { L3NetworkVO l3NetworkVO = Q.New(L3NetworkVO.class).eq(L3NetworkVO_.uuid, msg.getL3NetworkUuid()).find(); - if (!l3NetworkVO.enableIpAddressAllocation()) { + + List staticIpList = new ArrayList<>(); + if (msg.getIp() != null) { + staticIpList.add(msg.getIp()); + } + if (msg.getIp6() != null) { + staticIpList.add(msg.getIp6()); + } + + if (!l3NetworkVO.enableIpAddressAllocation() + || allStaticIpsOutsideRange(msg.getL3NetworkUuid(), staticIpList)) { setNoIpamStaticIp(msg, new Completion(reply) { @Override public void success() { @@ -3652,6 +3711,11 @@ public void run(FlowTrigger trigger, Map data) { done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { + // Set DNS addresses if provided + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getL3NetworkUuid(), msg.getDnsAddresses()); + } + completion.success(); } }); @@ -3680,7 +3744,13 @@ private void setIpamStaticIp(final SetVmStaticIpMsg msg, final Completion comple staticIpMap.put(IPv6Constants.IPv6, msg.getIp6()); } - changeVmIp(msg.getL3NetworkUuid(), staticIpMap, new Completion(msg, completion) { + IpOverrideInfo overrideInfo = new IpOverrideInfo(); + overrideInfo.setNetmask(msg.getNetmask()); + overrideInfo.setGateway(msg.getGateway()); + overrideInfo.setIpv6Gateway(msg.getIpv6Gateway()); + overrideInfo.setIpv6Prefix(msg.getIpv6Prefix()); + + changeVmIp(msg.getL3NetworkUuid(), staticIpMap, overrideInfo, new Completion(msg, completion) { @Override public void success() { if (msg.getIp() != null) { @@ -3690,6 +3760,10 @@ public void success() { new StaticIpOperator().setStaticIp(self.getUuid(), msg.getL3NetworkUuid(), msg.getIp6()); } new StaticIpOperator().setIpChange(self.getUuid(), msg.getL3NetworkUuid()); + // Set DNS addresses if provided + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getL3NetworkUuid(), msg.getDnsAddresses()); + } completion.success(); } @@ -5437,6 +5511,7 @@ public void handle(Map data) { private void removeStaticIp() { for (UsedIpInventory ip : nic.getUsedIps()) { new StaticIpOperator().deleteStaticIpByVmUuidAndL3Uuid(self.getUuid(), ip.getL3NetworkUuid()); + new StaticIpOperator().deleteStaticDnsByVmUuidAndL3Uuid(self.getUuid(), ip.getL3NetworkUuid()); } } @@ -6195,6 +6270,7 @@ private void handle(ChangeVmNicNetworkMsg msg) { public void success(VmNicInventory returnValue) { String originalL3Uuid = nic.getL3NetworkUuid(); new StaticIpOperator().deleteStaticIpByVmUuidAndL3Uuid(self.getUuid(), originalL3Uuid); + new StaticIpOperator().deleteStaticDnsByVmUuidAndL3Uuid(self.getUuid(), originalL3Uuid); reply.setInventory(returnValue); bus.reply(msg, reply); } @@ -6225,6 +6301,7 @@ private void handle(APIChangeVmNicNetworkMsg msg) { cmsg.setVmInstanceUuid(msg.getVmInstanceUuid()); cmsg.setRequiredIpMap(msg.getRequiredIpMap()); cmsg.setSystemTags(msg.getSystemTags()); + cmsg.setDnsAddresses(msg.getDnsAddresses()); bus.makeTargetServiceIdByResourceUuid(cmsg, VmInstanceConstant.SERVICE_ID, cmsg.getVmInstanceUuid()); bus.send(cmsg, new CloudBusCallBack(msg) { @Override @@ -6240,6 +6317,20 @@ public void run(MessageReply reply) { }); } + private boolean allStaticIpsOutsideRange(String l3Uuid, List ips) { + if (ips == null || ips.isEmpty()) { + return false; + } + + for (String ip : ips) { + if (new StaticIpOperator().getIpRangeUuid(l3Uuid, ip) != null) { + return false; + } + } + + return true; + } + private void changeVmNicNetwork(ChangeVmNicNetworkMsg msg, VmNicInventory nic, L3NetworkInventory destL3, final ReturnValueCompletion completion) { thdf.chainSubmit(new ChainTask(completion) { @Override @@ -6252,6 +6343,7 @@ public String getSyncSignature() { public void run(final SyncTaskChain chain) { class SetStaticIp { private boolean isSet = false; + private boolean isDnsSet = false; Map> staticIpMap = null; void set() { @@ -6272,17 +6364,28 @@ void set() { isSet = true; } + void setDns() { + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getDestL3NetworkUuid(), msg.getDnsAddresses()); + isDnsSet = true; + } + } + void rollback() { if (isSet) { for (Map.Entry> e : staticIpMap.entrySet()) { new StaticIpOperator().deleteStaticIpByVmUuidAndL3Uuid(self.getUuid(), e.getKey()); } } + if (isDnsSet) { + new StaticIpOperator().deleteStaticDnsByVmUuidAndL3Uuid(self.getUuid(), msg.getDestL3NetworkUuid()); + } } } final SetStaticIp setStaticIp = new SetStaticIp(); setStaticIp.set(); + setStaticIp.setDns(); Defer.guard(new Runnable() { @Override public void run() { @@ -6316,11 +6419,23 @@ public void run(FlowTrigger trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (!destL3.enableIpAddressAllocation()) { + if (!destL3.enableIpAddressAllocation() + || allStaticIpsOutsideRange(destL3.getUuid(), + msg.getRequiredIpMap() != null ? msg.getRequiredIpMap().get(destL3.getUuid()) : null)) { trigger.next(); return; } - allocateIp(destL3, nic, new ReturnValueCompletion>(chain) { + IpOverrideInfo nicOverrideInfo = null; + Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); + NicIpAddressInfo nicIpInfo = nicNetworkInfo.get(msg.getDestL3NetworkUuid()); + if (nicIpInfo != null) { + nicOverrideInfo = new IpOverrideInfo(); + nicOverrideInfo.setNetmask(nicIpInfo.ipv4Netmask); + nicOverrideInfo.setGateway(nicIpInfo.ipv4Gateway); + nicOverrideInfo.setIpv6Gateway(nicIpInfo.ipv6Gateway); + nicOverrideInfo.setIpv6Prefix(nicIpInfo.ipv6Prefix); + } + allocateIp(destL3, nic, nicOverrideInfo, new ReturnValueCompletion>(chain) { @Override public void success(List returnValue) { data.put(VmInstanceConstant.Params.VmAllocateNicFlow_ips.toString(), returnValue); @@ -6370,7 +6485,9 @@ public void rollback(FlowRollback trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (destL3.enableIpAddressAllocation()) { + if (destL3.enableIpAddressAllocation() + && !allStaticIpsOutsideRange(destL3.getUuid(), + msg.getRequiredIpMap() != null ? msg.getRequiredIpMap().get(destL3.getUuid()) : null)) { trigger.next(); return; } @@ -6445,7 +6562,9 @@ public void run(FlowTrigger trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (!destL3.enableIpAddressAllocation()) { + if (!destL3.enableIpAddressAllocation() + || allStaticIpsOutsideRange(destL3.getUuid(), + msg.getRequiredIpMap() != null ? msg.getRequiredIpMap().get(destL3.getUuid()) : null)) { trigger.next(); return; } @@ -6617,7 +6736,7 @@ public String getName() { }); } - private void allocateIp(L3NetworkInventory l3, VmNicInventory nic,final ReturnValueCompletion> completion) { + private void allocateIp(L3NetworkInventory l3, VmNicInventory nic, final IpOverrideInfo overrideInfo, final ReturnValueCompletion> completion) { L3NetworkInventory nw = l3; Map> vmStaticIps = new StaticIpOperator().getStaticIpbyVmUuid(getSelf().getUuid()); List ipVersions = nw.getIpVersions(); @@ -6641,6 +6760,15 @@ private void allocateIp(L3NetworkInventory l3, VmNicInventory nic,final ReturnVa } } msg.setIpVersion(ipversion); + if (overrideInfo != null) { + if (ipversion == IPv6Constants.IPv4) { + msg.setNetmask(overrideInfo.getNetmask()); + msg.setGateway(overrideInfo.getGateway()); + } else if (ipversion == IPv6Constants.IPv6) { + msg.setIpv6Gateway(overrideInfo.getIpv6Gateway()); + msg.setIpv6Prefix(overrideInfo.getIpv6Prefix()); + } + } bus.makeTargetServiceIdByResourceUuid(msg, L3NetworkConstant.SERVICE_ID, nw.getUuid()); msgs.add(msg); } 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..df0c9fdd9e8 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java @@ -311,6 +311,11 @@ public String desensitizeTag(SystemTag systemTag, String tag) { } } + // DNS servers for VM NIC, format: staticDns::{l3NetworkUuid}::{dns1,dns2,dns3} + public static String STATIC_DNS_L3_UUID_TOKEN = "l3NetworkUuid"; + public static String STATIC_DNS_TOKEN = "staticDns"; + public static PatternedSystemTag STATIC_DNS = new PatternedSystemTag(String.format("staticDns::{%s}::{%s}", STATIC_DNS_L3_UUID_TOKEN, STATIC_DNS_TOKEN), VmInstanceVO.class); + 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); diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 8d29c9f95ec..6c6168445f8 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -125,3 +125,56 @@ ALTER TABLE `zstack`.`BareMetal2InstanceVO` -- ZSTAC-68709: Add targetQueueKey for evaluation task queuing per service endpoint CALL ADD_COLUMN('ModelEvaluationTaskVO', 'targetQueueKey', 'TEXT', 1, NULL); + +-- Add prefixLen column to UsedIpVO for IPv6 addresses outside IP range +CALL ADD_COLUMN('UsedIpVO', 'prefixLen', 'INT', 1, NULL); + +-- Backfill prefixLen from IpRangeVO for existing IPv6 UsedIpVO records +UPDATE `zstack`.`UsedIpVO` `u` +INNER JOIN `zstack`.`IpRangeVO` `r` ON `u`.`ipRangeUuid` = `r`.`uuid` +SET `u`.`prefixLen` = `r`.`prefixLen` +WHERE `u`.`ipVersion` = 6 + AND `u`.`ipRangeUuid` IS NOT NULL + AND `u`.`prefixLen` IS NULL; + +-- Modify ipRangeUuid foreign key constraint to SET NULL on delete (instead of CASCADE) +DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; +DELIMITER $$ + +CREATE PROCEDURE ModifyUsedIpVOForeignKey() +BEGIN + DECLARE constraint_exists INT; + + -- Check if the constraint exists before dropping it + SELECT COUNT(*) + INTO constraint_exists + FROM `INFORMATION_SCHEMA`.`TABLE_CONSTRAINTS` + WHERE `TABLE_SCHEMA` = 'zstack' + AND `TABLE_NAME` = 'UsedIpVO' + AND `CONSTRAINT_NAME` = 'fkUsedIpVOIpRangeEO' + AND `CONSTRAINT_TYPE` = 'FOREIGN KEY'; + + IF constraint_exists > 0 THEN + ALTER TABLE `zstack`.`UsedIpVO` DROP FOREIGN KEY `fkUsedIpVOIpRangeEO`; + END IF; + + -- Re-check before adding so the migration stays idempotent + SELECT COUNT(*) + INTO constraint_exists + FROM `INFORMATION_SCHEMA`.`TABLE_CONSTRAINTS` + WHERE `TABLE_SCHEMA` = 'zstack' + AND `TABLE_NAME` = 'UsedIpVO' + AND `CONSTRAINT_NAME` = 'fkUsedIpVOIpRangeEO' + AND `CONSTRAINT_TYPE` = 'FOREIGN KEY'; + + IF constraint_exists = 0 THEN + ALTER TABLE `zstack`.`UsedIpVO` + ADD CONSTRAINT `fkUsedIpVOIpRangeEO` + FOREIGN KEY (`ipRangeUuid`) REFERENCES `zstack`.`IpRangeEO`(`uuid`) ON DELETE SET NULL; + END IF; +END $$ + +DELIMITER ; + +CALL ModifyUsedIpVOForeignKey(); +DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java b/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java index 849ebe4c96c..78af601bea6 100755 --- a/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java @@ -12,6 +12,10 @@ public class AllocateIpMsg extends NeedReplyMessage implements L3NetworkMessage, private String ipRangeUuid; private String ipRangeType; private int ipVersion = IPv6Constants.IPv4; + private String netmask; + private String gateway; + private String ipv6Gateway; + private String ipv6Prefix; public String getRequiredIp() { return requiredIp; @@ -74,4 +78,40 @@ public int getIpVersion() { public void setIpVersion(int ipVersion) { this.ipVersion = ipVersion; } + + @Override + public String getNetmask() { + return netmask; + } + + public void setNetmask(String netmask) { + this.netmask = netmask; + } + + @Override + public String getGateway() { + return gateway; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + @Override + public String getIpv6Gateway() { + return ipv6Gateway; + } + + public void setIpv6Gateway(String ipv6Gateway) { + this.ipv6Gateway = ipv6Gateway; + } + + @Override + public String getIpv6Prefix() { + return ipv6Prefix; + } + + public void setIpv6Prefix(String ipv6Prefix) { + this.ipv6Prefix = ipv6Prefix; + } } diff --git a/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java b/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java index 4bb49d9fc8f..0d886bab7b5 100755 --- a/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java +++ b/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java @@ -15,6 +15,22 @@ default String getExcludedIp() { default boolean isDuplicatedIpAllowed() {return false;} + default String getNetmask() { + return null; + } + + default String getGateway() { + return null; + } + + default String getIpv6Gateway() { + return null; + } + + default String getIpv6Prefix() { + return null; + } + void setIpRangeUuid(String ipRangeUuid); void setRequiredIp(String requiredIp); diff --git a/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java b/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java index 775dc66d9c5..c9f406ae985 100755 --- a/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java +++ b/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java @@ -30,6 +30,7 @@ public class UsedIpInventory implements Serializable { private Integer ipVersion; private String ip; private String netmask; + private Integer prefixLen; private String gateway; private String usedFor; @APINoSee @@ -52,6 +53,7 @@ public static UsedIpInventory valueOf(UsedIpVO vo) { inv.setL3NetworkUuid(vo.getL3NetworkUuid()); inv.setGateway(vo.getGateway()); inv.setNetmask(vo.getNetmask()); + inv.setPrefixLen(vo.getPrefixLen()); inv.setUsedFor(vo.getUsedFor()); inv.setVmNicUuid(vo.getVmNicUuid()); inv.setMetaData(vo.getMetaData()); @@ -139,6 +141,14 @@ public void setNetmask(String netmask) { this.netmask = netmask; } + public Integer getPrefixLen() { + return prefixLen; + } + + public void setPrefixLen(Integer prefixLen) { + this.prefixLen = prefixLen; + } + public String getGateway() { return gateway; } diff --git a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java index c35346301ff..c4f4376fa3b 100755 --- a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java +++ b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java @@ -24,7 +24,7 @@ public class UsedIpVO { private String uuid; @Column - @ForeignKey(parentEntityClass = IpRangeEO.class, onDeleteAction = ReferenceOption.CASCADE) + @ForeignKey(parentEntityClass = IpRangeEO.class, onDeleteAction = ReferenceOption.SET_NULL) private String ipRangeUuid; @Column @@ -48,6 +48,9 @@ public class UsedIpVO { @Column private String netmask; + @Column + private Integer prefixLen; + @Column @Index private long ipInLong; @@ -147,6 +150,14 @@ public void setNetmask(String netmask) { this.netmask = netmask; } + public Integer getPrefixLen() { + return prefixLen; + } + + public void setPrefixLen(Integer prefixLen) { + this.prefixLen = prefixLen; + } + public String getUsedFor() { return usedFor; } diff --git a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java index 4186a4a6d54..6625e62b12e 100755 --- a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java +++ b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java @@ -16,6 +16,8 @@ public class UsedIpVO_ { public static volatile SingularAttribute ipInLong; public static volatile SingularAttribute vmNicUuid; public static volatile SingularAttribute gateway; + public static volatile SingularAttribute netmask; + public static volatile SingularAttribute prefixLen; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java index c00ab47e904..6762f85f793 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java @@ -2,12 +2,16 @@ import org.springframework.http.HttpMethod; import org.zstack.header.identity.Action; +import org.zstack.header.message.APIEvent; import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.network.l3.L3NetworkVO; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.other.APIMultiAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -18,7 +22,7 @@ method = HttpMethod.POST, responseClass = APIChangeVmNicNetworkEvent.class ) -public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMessage{ +public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMessage, APIMultiAuditor { @APIParam(resourceType = VmNicVO.class, checkAccount = true, operationTarget = true) private String vmNicUuid; @@ -33,6 +37,10 @@ public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMe private String staticIp; + + @APIParam(required = false) + private List dnsAddresses; + public String getVmNicUuid() { return vmNicUuid; } @@ -57,6 +65,7 @@ public void setRequiredIpMap(Map> requiredIpMap) { this.requiredIpMap = requiredIpMap; } + public static APIChangeVmNicNetworkMsg __example__() { APIChangeVmNicNetworkMsg msg = new APIChangeVmNicNetworkMsg(); msg.vmNicUuid = uuid(); @@ -80,4 +89,20 @@ public String getStaticIp() { public void setStaticIp(String staticIp) { this.staticIp = staticIp; } + + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } + + @Override + public List multiAudit(APIMessage msg, APIEvent rsp) { + APIChangeVmNicNetworkMsg amsg = (APIChangeVmNicNetworkMsg) msg; + List res = new ArrayList<>(); + res.add(new APIAuditor.Result(amsg.getVmInstanceUuid(), VmInstanceVO.class)); + return res; + } } diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy index 893596734c7..c02221bd20e 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy @@ -66,6 +66,15 @@ doc { optional true since "0.6" } + column { + name "dnsAddresses" + enclosedIn "params" + desc "DNS服务器地址列表" + location "body" + type "List" + optional true + since "5.5.6" + } } } diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java index a4e0d71b209..80a022b5cfb 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java @@ -7,6 +7,8 @@ import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; +import java.util.List; + /** * Created by frank on 2/26/2016. */ @@ -34,6 +36,8 @@ public class APISetVmStaticIpMsg extends APIMessage implements VmInstanceMessage private String ipv6Gateway; @APIParam(required = false) private String ipv6Prefix; + @APIParam(required = false) + private List dnsAddresses; public String getIp() { return ip; @@ -100,6 +104,14 @@ public void setIpv6Prefix(String ipv6Prefix) { this.ipv6Prefix = ipv6Prefix; } + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } + public static APISetVmStaticIpMsg __example__() { APISetVmStaticIpMsg msg = new APISetVmStaticIpMsg(); msg.vmInstanceUuid = uuid(); diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy index 9415e501788..20513324725 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy @@ -114,6 +114,15 @@ doc { optional true since "0.6" } + column { + name "dnsAddresses" + enclosedIn "setVmStaticIp" + desc "DNS服务器地址列表" + location "body" + type "List" + optional true + since "5.5.6" + } } } diff --git a/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java index 1e7f7b1772f..54816bdfcde 100644 --- a/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java @@ -14,6 +14,7 @@ public class ChangeVmNicNetworkMsg extends NeedReplyMessage implements VmInstanc private String vmInstanceUuid; private Map> requiredIpMap; private String staticIp; + private List dnsAddresses; public String getVmNicUuid() { return vmNicUuid; @@ -55,4 +56,13 @@ public String getStaticIp() { public void setStaticIp(String staticIp) { this.staticIp = staticIp; } + + + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } } diff --git a/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java index dd27c3c0c48..a1a117da8a9 100644 --- a/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java +++ b/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java @@ -2,6 +2,8 @@ import org.zstack.header.message.NeedReplyMessage; +import java.util.List; + /** * Created by LiangHanYu on 2022/6/22 17:12 */ @@ -14,6 +16,7 @@ public class SetVmStaticIpMsg extends NeedReplyMessage implements VmInstanceMess private String gateway; private String ipv6Gateway; private String ipv6Prefix; + private List dnsAddresses; @Override public String getVmInstanceUuid() { @@ -79,4 +82,12 @@ public String getIpv6Prefix() { public void setIpv6Prefix(String ipv6Prefix) { this.ipv6Prefix = ipv6Prefix; } + + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java index 896e60e414e..fec2e4f1b51 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java @@ -13,6 +13,7 @@ public interface VmInstanceConstant { // System limit int MAXIMUM_CDROM_NUMBER = 3; + int MAXIMUM_NIC_DNS_NUMBER = 3; String KVM_HYPERVISOR_TYPE = "KVM"; diff --git a/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java b/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java index fffbff32665..64cd7854716 100644 --- a/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java +++ b/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java @@ -264,6 +264,95 @@ public static boolean isIpAddressAllocationEnableOnL3(String l3Uuid) { return l3NetworkVO.enableIpAddressAllocation(); } + /** + * Check if an IP address is within any L3 network's CIDR (from NormalIpRange). + */ + public static boolean isIpInL3NetworkCidr(String ip, String l3Uuid) { + if (ip == null || l3Uuid == null) { + return false; + } + + if (IPv6NetworkUtils.isIpv6Address(ip)) { + List ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + for (NormalIpRangeVO ipr : ranges) { + String cidr = ipr.getNetworkCidr(); + if (cidr != null && IPv6NetworkUtils.isIpv6InCidrRange(ip, cidr)) { + return true; + } + } + } else if (NetworkUtils.isIpv4Address(ip)) { + List ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + for (NormalIpRangeVO ipr : ranges) { + String cidr = ipr.getNetworkCidr(); + if (cidr != null && NetworkUtils.isIpv4InCidr(ip, cidr)) { + return true; + } + } + } + + return false; + } + + /** + * Check if an IP address is outside all L3 network CIDRs. + */ + public static boolean isIpOutsideL3NetworkCidr(String ip, String l3Uuid) { + return !isIpInL3NetworkCidr(ip, l3Uuid); + } + + /** + * Find a NormalIpRangeVO whose CIDR contains the given IP. + * First tries exact range match (startIp-endIp), then falls back to CIDR match. + */ + public static NormalIpRangeVO findIpRangeByCidr(String ip, List ranges) { + if (ip == null || ranges == null || ranges.isEmpty()) { + return null; + } + + boolean isIpv4 = NetworkUtils.isIpv4Address(ip); + boolean isIpv6 = IPv6NetworkUtils.isIpv6Address(ip); + int targetVersion = isIpv4 ? IPv6Constants.IPv4 : (isIpv6 ? IPv6Constants.IPv6 : -1); + if (targetVersion == -1) { + return null; + } + + // First try exact range match + for (NormalIpRangeVO ipr : ranges) { + if (ipr.getIpVersion() != targetVersion) { + continue; + } + if (isIpv4 && NetworkUtils.isInRange(ip, ipr.getStartIp(), ipr.getEndIp())) { + return ipr; + } + if (isIpv6 && IPv6NetworkUtils.isIpv6InRange(ip, ipr.getStartIp(), ipr.getEndIp())) { + return ipr; + } + } + + // Fallback to CIDR match + for (NormalIpRangeVO ipr : ranges) { + if (ipr.getIpVersion() != targetVersion) { + continue; + } + String cidr = ipr.getNetworkCidr(); + if (cidr == null) { + continue; + } + if (isIpv4 && NetworkUtils.isIpv4InCidr(ip, cidr)) { + return ipr; + } + if (isIpv6 && IPv6NetworkUtils.isIpv6InCidrRange(ip, cidr)) { + return ipr; + } + } + + return null; + } + public static IpRangeVO fromIpRangeInventory(IpRangeInventory ipr, String accountUuid) { NormalIpRangeVO vo = new NormalIpRangeVO(); vo.setUuid(ipr.getUuid() == null ? Platform.getUuid() : ipr.getUuid()); diff --git a/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java b/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java index b1b0b92d497..d7c6c6798d9 100755 --- a/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java +++ b/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java @@ -319,6 +319,7 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger trigger, Map data) { + SQL.New(UsedIpVO.class).eq(UsedIpVO_.ipRangeUuid, iprvo.getUuid()).delete(); dbf.remove(iprvo); IpRangeHelper.updateL3NetworkIpversion(iprvo); @@ -555,6 +556,8 @@ public void run(SyncTaskChain chain) { return; } + ip = overrideUsedIpIfNeeded(msg, ip); + logger.debug(String.format("Ip allocator strategy[%s] successfully allocates an ip[%s]", strategyType, ip.getIp())); reply.setIpInventory(ip); bus.reply(msg, reply); @@ -568,6 +571,53 @@ public String getName() { }); } + private UsedIpInventory overrideUsedIpIfNeeded(AllocateIpMsg msg, UsedIpInventory ip) { + String overrideNetmask = null; + String overrideGateway = null; + Integer prefixLength = null; + + if (ip.getIpVersion() != null && ip.getIpVersion() == IPv6Constants.IPv4) { + if (msg.getNetmask() != null) { + overrideNetmask = msg.getNetmask(); + } + if (msg.getGateway() != null) { + overrideGateway = msg.getGateway(); + } + } else if (ip.getIpVersion() != null && ip.getIpVersion() == IPv6Constants.IPv6) { + if (msg.getIpv6Prefix() != null) { + try { + prefixLength = Integer.parseInt(msg.getIpv6Prefix()); + overrideNetmask = IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(ip.getIp() + "/" + msg.getIpv6Prefix()); + } catch (NumberFormatException e) { + logger.warn(String.format("failed to parse prefix length[%s], ignore it and use the default prefix length of the ip range", + msg.getIpv6Prefix())); + } + } + if (msg.getIpv6Gateway() != null) { + overrideGateway = msg.getIpv6Gateway().isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getIpv6Gateway()); + } + } + + if (overrideNetmask != null || overrideGateway != null) { + UsedIpVO vo = dbf.findByUuid(ip.getUuid(), UsedIpVO.class); + if (vo != null) { + if (overrideNetmask != null) { + vo.setNetmask(overrideNetmask); + } + if (overrideGateway != null) { + vo.setGateway(overrideGateway); + } + if (prefixLength != null) { + vo.setPrefixLen(prefixLength); + } + vo = dbf.updateAndRefresh(vo); + ip = UsedIpInventory.valueOf(vo); + } + } + + return ip; + } + private void handleApiMessage(APIMessage msg) { if (msg instanceof APIDeleteL3NetworkMsg) { handle((APIDeleteL3NetworkMsg) msg); @@ -767,10 +817,7 @@ private void handle(APIDeleteIpAddressMsg msg) { @Override public CheckIpAvailabilityReply checkIpAvailability(CheckIpAvailabilityMsg msg) { CheckIpAvailabilityReply reply = new CheckIpAvailabilityReply(); - int ipversion = IPv6Constants.IPv4; - if (IPv6NetworkUtils.isIpv6Address(msg.getIp())) { - ipversion = IPv6Constants.IPv6; - } + final int ipversion = IPv6NetworkUtils.isIpv6Address(msg.getIp()) ? IPv6Constants.IPv6 : IPv6Constants.IPv4; SimpleQuery rq = dbf.createQuery(IpRangeVO.class); rq.select(IpRangeVO_.startIp, IpRangeVO_.endIp, IpRangeVO_.gateway); rq.add(IpRangeVO_.l3NetworkUuid, Op.EQ, self.getUuid()); diff --git a/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java b/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java index d4dfa4dc8d6..ac88e09dbf8 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java @@ -726,6 +726,48 @@ private void validate(IpRangeInventory ipr) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L3_10064, "new add ip range gateway %s is different from old gateway %s", ipr.getGateway(), r.getGateway())); } } + + // When adding the first IpRange, check if network address or gateway is already used + if (l3IpRanges.isEmpty()) { + String networkAddress = info.getNetworkAddress(); + String broadcastAddress = info.getBroadcastAddress(); + + // Check if gateway address is already used by VmNic with ipRangeUuid=null + boolean gatewayUsed = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, ipr.getL3NetworkUuid()) + .eq(UsedIpVO_.ip, ipr.getGateway()) + .isNull(UsedIpVO_.ipRangeUuid) + .isExists(); + if (gatewayUsed) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_NETWORK_L3_10079, "gateway address[%s] is already used by a VM NIC, cannot add IP range with this gateway", + ipr.getGateway())); + } + + // Check if network address is already used + boolean networkAddressUsed = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, ipr.getL3NetworkUuid()) + .eq(UsedIpVO_.ip, networkAddress) + .isNull(UsedIpVO_.ipRangeUuid) + .isExists(); + if (networkAddressUsed) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_NETWORK_L3_10080, "network address[%s] is already used by a VM NIC, cannot add IP range containing this address", + networkAddress)); + } + + // Check if broadcast address is already used + boolean broadcastAddressUsed = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, ipr.getL3NetworkUuid()) + .eq(UsedIpVO_.ip, broadcastAddress) + .isNull(UsedIpVO_.ipRangeUuid) + .isExists(); + if (broadcastAddressUsed) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_NETWORK_L3_10081, "broadcast address[%s] is already used by a VM NIC, cannot add IP range containing this address", + broadcastAddress)); + } + } } else if (ipr.getIpRangeType() == IpRangeType.AddressPool) { validateAddressPool(ipr); } 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..81fb7e94b3f 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java @@ -383,7 +383,7 @@ public IpCapacity call() { ts = IpRangeHelper.stripNetworkAndBroadcastAddress(ts); calcElementTotalIp(ts, ret); - sql = "select count(distinct uip.ip), uip.l3NetworkUuid, uip.ipVersion from UsedIpVO uip where uip.l3NetworkUuid in (:uuids) and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by uip.l3NetworkUuid, uip.ipVersion"; + sql = "select count(distinct uip.ip), uip.l3NetworkUuid, uip.ipVersion from UsedIpVO uip where uip.l3NetworkUuid in (:uuids) and uip.ipRangeUuid is not null and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by uip.l3NetworkUuid, uip.ipVersion"; TypedQuery cq = dbf.getEntityManager().createQuery(sql, Tuple.class); cq.setParameter("uuids", msg.getL3NetworkUuids()); cq.setParameter("notAccountMetaData", notAccountMetaDatas); @@ -399,7 +399,7 @@ public IpCapacity call() { ts = IpRangeHelper.stripNetworkAndBroadcastAddress(ts); calcElementTotalIp(ts, ret); - sql = "select count(distinct uip.ip), zone.uuid, uip.ipVersion from UsedIpVO uip, L3NetworkVO l3, ZoneVO zone where uip.l3NetworkUuid = l3.uuid and l3.zoneUuid = zone.uuid and zone.uuid in (:uuids) and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by zone.uuid, uip.ipVersion"; + sql = "select count(distinct uip.ip), zone.uuid, uip.ipVersion from UsedIpVO uip, L3NetworkVO l3, ZoneVO zone where uip.l3NetworkUuid = l3.uuid and l3.zoneUuid = zone.uuid and zone.uuid in (:uuids) and uip.ipRangeUuid is not null and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by zone.uuid, uip.ipVersion"; TypedQuery cq = dbf.getEntityManager().createQuery(sql, Tuple.class); cq.setParameter("uuids", msg.getZoneUuids()); cq.setParameter("notAccountMetaData", notAccountMetaDatas); @@ -723,6 +723,7 @@ private UsedIpInventory reserveIpv6(IpRangeVO ipRange, String ip, boolean allowD vo.setL3NetworkUuid(ipRange.getL3NetworkUuid()); vo.setNetmask(ipRange.getNetmask()); vo.setGateway(ipRange.getGateway()); + vo.setPrefixLen(ipRange.getPrefixLen()); vo.setIpVersion(IPv6Constants.IPv6); vo = dbf.persistAndRefresh(vo); return UsedIpInventory.valueOf(vo); diff --git a/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java b/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java index f6712c54053..c0403be58ed 100644 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java @@ -1,6 +1,5 @@ package org.zstack.network.l3; -import org.zstack.header.network.l2.L2NetworkVO; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.tag.TagDefinition; import org.zstack.tag.PatternedSystemTag; diff --git a/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java b/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java index 6b7d70d45eb..c052d0a1d69 100644 --- a/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java +++ b/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java @@ -70,9 +70,11 @@ protected NormalIpRangeVO scripts() { IpRangeHelper.updateL3NetworkIpversion(vo); + // Update UsedIpVO records that have ipRangeUuid=null and IP is within the new range List usedIpVos = Q.New(UsedIpVO.class) .eq(UsedIpVO_.l3NetworkUuid, vo.getL3NetworkUuid()) - .eq(UsedIpVO_.ipVersion, vo.getIpVersion()).list(); + .eq(UsedIpVO_.ipVersion, vo.getIpVersion()) + .isNull(UsedIpVO_.ipRangeUuid).list(); List updateVos = new ArrayList<>(); for (UsedIpVO ipvo : usedIpVos) { if (ipvo.getIpVersion() == IPv6Constants.IPv4) { diff --git a/network/src/main/java/org/zstack/network/l3/zstack ipam.md b/network/src/main/java/org/zstack/network/l3/zstack ipam.md new file mode 100644 index 00000000000..63951964dbd --- /dev/null +++ b/network/src/main/java/org/zstack/network/l3/zstack ipam.md @@ -0,0 +1,139 @@ +# ZStack IPAM + +ZStack IPAM 负责管理 L3 网络的 IP 地址分配和回收。它提供三种方式: +1. **自动分配**: ZStack云平台根据L3配置的ip range自动分配。 +2. **手动分配**: 用户可以在创建虚拟机时指定IP地址。 +3. **qga获取**: 通过DHCP服务器动态分配IP地址。 + +## 自动分配 + +自动分配需要满足两个条件: +1. L3网络必须配置ip range。 +2. L3网络必须enable dhcp服务。这是个历史遗留问题: 扁平网络使用dhcp服务标识是否启用自动分配功能, 其它网络类型不受影响 +它根据用户输入的l3网络uuid和可选的ip地址, 按照地址分配算法分配一个可用地址,分配的IP地址包含: ip地址,掩码(或者前缀长度),网关 + +### 自动分配算法 +- 随机分配: 从可用ip地址池中随机选择一个ip地址分配给虚拟机 +- 顺序分配: 从可用ip地址池中按照顺序选择一个ip +- 循环分配: 从可用ip地址池中按照顺序选择一个ip, 分配完最后一个ip后, 从第一个ip重新开始分配 + +### 当前状况 +cloud 5.5版本情况: +1. 扁平网络可以有三种情况: no ip range, ip range without dhcp, ip range with dhcp. +2. 公有网络和VPC网络有两种情况: ip range without dhcp, ip range with dhcp. +3. 管理网和流量网络只有一种情况: ip range without dhcp. + +### 工作时机 +以下操作会触发自动分配: +1. 创建虚拟机(APICreateVmInstanceMsg) +2. 虚拟机添加网卡(APIAttachL3NetworkToVmMsg, APICreateVmNicMsg) +3. 修改虚拟机IP(APISetVmStaticIpMsg, APIChangeVmNicNetworkMsg) +4. 创建applianceVm(APICreateVpcVRouterMsg, APICreateSlbInstanceMsg, APICreateNfvInstMsg) +5. 创建Vip(APICreateVipMsg) + +## 手动分配 +手动指定仅仅对虚拟机生效,对于applianceVm不生效。 +在前述场景1,2,3的情况下,用户可以指定ip地址. 这又分两种情况: +1. 指定的ip在ip range之内,后端仍然执行的自动分配流量 +2. 指定的ip不在ip range之内, 按照手动指定流程分配 + 1. 如果指定的ip地址不在l3 cidr之内,必须指定掩码, 网关可选 + 2. 如果指定的ip地址在l3 cidr之内,可以不指定掩码, 网关, 如果指定必须和l3 cidr一致 + +### 工作时机 +1. 在5.5.12之前, 扁平网络在两种情况下: no ip range, ip range without dhcp, 允许指定地址不在ip range之内 +2. 在5.5.12版本及其以后, 任意网络,都可以通过修改虚拟机IP(APISetVmStaticIpMsg, APIChangeVmNicNetworkMsg) 设置不在ip range之内的地址, + +如果指定的ip地址在ip ranges之外, 但是在l3 cidr之内, 则掩码和网关可以不指定, 系统会自动从l3 cidr中获取掩码和网关 +如果指定的ip地址在ip cidr之外, 用户输入必须同时输入IP, 掩码或者前缀长度, 如果是默认网卡,必须指定网关, 网关必须在l3 cidr之内 + + +## qga获取 +这种方式需要打开全局配置: VmGlobalConfig.ENABLE_VM_INTERNAL_IP_OVERWRITE(默认值是false) +ZStack kvmagent会定期通过qga从云主机内部读取ip地址, 仅在扁平网络在no ip range的情况下会把读出来的ip地址分配给云主机, 其它网络类型不受影响。 +其它情况下,qga获取的ip地址如果和虚拟机的ip地址冲突, 则发送报警。 + +## 配置虚拟机guest OS的IP地址 +配置虚拟机guest OS的IP地址有3种方式: +1. **DHCP**: 通过DHCP服务器动态分配IP地址。 +2. **Cloud-init**: 在虚拟机创建时,使用Cloud-init工具预配置IP地址。 +3. **QGA**: 通过QEMU Guest Agent从虚拟 + +### DHCP +ZStack会在每个物理机启动分布式dhcp server, 虚拟机启动时候, 通过dhclient获取地址和dns等参数。 + +### Cloud-init +ZStack会在每个物理机启动分布式userdata server, 虚拟机启动时候, 通过cloud-init获取地址和dns等参数。 + +### QGA +当虚拟机安装ZStack Guest Agent后,在zstack检测guest agent第一次启动时候,通过qga配置虚拟机的ip地址,dns等参数 +当用户在UI手动修改IP(APISetVmStaticIpMsg, APIChangeVmNicNetworkMsg), UI调用后端api, 触发一次配置虚拟机ip地址的过程 +qga配置虚拟机的参数包含: +- IP地址 +- 掩码或者前缀长度 +- 网关 +- DNS服务器地址 +- mtu +- hostname +用户可以通过全局配置来限制配置的字段: GuestToolsGlobalProperty.GUESTTOOLS_VM_PORT_CONFIGFIELDS来限制 + +## 网络服务 +当网卡地址不在 l3 ip range之内的时候, 又可以分为在l3 cidr之内和在l3 cidr之外两种情况: + +### 在l3 cidr之内 +这种情况和在l3 ip range之内的情况一样, 网络服务没有影响 + +### 在l3 cidr之外 +#### 安全组 +1. 安全组的规则不关心网卡ip, 当这种网卡配置了安全组以后, 需要用户小心规则的配置,否则可能满足不了需求 + +#### DHCP +如果网卡没有ip range, 则没有dhcp服务 +如果网卡有ip range, zstack会启动dhcp服务, dnsmasq的配置文件要求指定一个ip cidr +如果网卡的ip地址不在dhcp服务的ip cidr之内, 因此dhcp模块下发配置的时候调多cidr之外的地址 + +#### Eip +对于扁平网络, eip功能不受影响,可以继续创建。 +对于vpc网络, eip的私网地址不在l3 cidr之内, vpc路由器无法路由,网络不通 +为了一致性,eip不能绑定ip地址不在l3 cidr之内的网卡, APIGetEipAttachableVmNicsMsg 也不返回ip地址不在l3 cidr之内的网卡 + +#### Port forwarding +只有vpc网络才有port forwarding功能, 和eip一样, vpc路由器无法路由,网络不通 +Port forwarding不能绑定ip地址不在l3 cidr之内的网卡, APIGetPortForwardingAttachableVmNicsMsg 也不返回ip地址不在l3 cidr之内的网卡 + +#### LoadBalancer +和eip一样,网络不通 +APIAddVmNicToLoadBalancerMsg, APIAddBackendServerToServerGroupMsg 不能绑定ip地址不在l3 cidr之内的网卡, +APIGetCandidateVmNicsForLoadBalancerServerGroupMsg, APIGetCandidateVmNicsForLoadBalancerMsg也不返回ip地址不在l3 cidr之内的网卡 + + +## 代码细节 + +### APISetVmStaticIpMsg +通过成员字段配置虚拟机的IP地址,掩码,网关等参数。需要完整性校验 +- 如果用户输入IP地址,不输入掩码和网关,优先使用网卡上使用的掩码和网关; +- 继续,如果网卡上没有使用的掩码和网关, 则从l3 cidr中获取掩码和网关 +- 继续,如果l3 cidr中也没有掩码和网关,报错 +- ipv6和ipv4的逻辑一样 +- +### APIChangeVmNicNetworkMsg +通过system tags来配置虚拟机的IP地址,掩码,网关等参数。需要完整性校验 +- 如果配置ip地址,且在l3 cidr之内,掩码和网关从l3 cidr中获取 +- 如果配置ip地址,且在l3 cidr之外,必须指定掩码, 网关可选; 如果是默认网卡,必须指定网关 +- 如果配置ipv6地址,且在l3 cidr之内,前缀长度和网关从l3 cidr中获取 +- 如果配置ipv6地址,且在l3 cidr之外,必须指定前缀长度, 网关可选; 如果是默认网卡,必须指定网关 + +### APICreateVmInstanceMsg +逻辑和APIChangeVmNicNetworkMsg相同 + +### APIGetL3NetworkIpStatisticMsg +不统计在ip range之外的ip地址 + +### APIAddIpRangeMsg +允许给云主机设置不在ip range之内的ip地址, 这样在添加ip range的时候, 可能包含已经分配的ip地址, 此时, 让已分配的地址属于新加入的ip range + +### APIAddReservedIpRangeMsg +这个api不仅添加了ReservedIpRangeVO, 还把ReservedIpRangeVO和ip range重叠的ip添加到UsedIpVO, +vo.setUsedFor(IpAllocatedReason.Reserved.toString()); + +### APICheckIpAvailabilityMsg +这个api在5.5.12版本之前, 在扁平网络no dhcp的情况下跳过ip range的检查, 这个功能不变 diff --git a/network/src/main/java/org/zstack/network/service/DhcpExtension.java b/network/src/main/java/org/zstack/network/service/DhcpExtension.java index 625c872dc8a..8e37da8b1e9 100755 --- a/network/src/main/java/org/zstack/network/service/DhcpExtension.java +++ b/network/src/main/java/org/zstack/network/service/DhcpExtension.java @@ -136,11 +136,16 @@ private void populateExtensions() { } public boolean isDualStackNicInSingleL3Network(VmNicInventory nic) { - if (nic.getUsedIps().size() < 2) { + // Filter out IPs outside L3 CIDR range + List validIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null || IpRangeHelper.isIpInL3NetworkCidr(ip.getIp(), ip.getL3NetworkUuid())) + .collect(Collectors.toList()); + + if (validIps.size() < 2) { return false; } - return nic.getUsedIps().stream().map(UsedIpInventory::getL3NetworkUuid).distinct().count() == 1; + return validIps.stream().map(UsedIpInventory::getL3NetworkUuid).distinct().count() == 1; } private DhcpStruct getDhcpStruct(VmInstanceInventory vm, List hostNames, VmNicVO nic, UsedIpVO ip, boolean isDefaultNic) { @@ -194,7 +199,11 @@ private boolean isEnableRa(String l3Uuid) { private void setDualStackNicOfSingleL3Network(DhcpStruct struct, VmNicVO nic) { struct.setIpVersion(IPv6Constants.DUAL_STACK); - List sortedIps = nic.getUsedIps().stream().sorted(Comparator.comparingLong(UsedIpVO::getIpVersionl)).collect(Collectors.toList()); + // Filter out IPs outside L3 CIDR range + List sortedIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null || IpRangeHelper.isIpInL3NetworkCidr(ip.getIp(), ip.getL3NetworkUuid())) + .sorted(Comparator.comparingLong(UsedIpVO::getIpVersionl)) + .collect(Collectors.toList()); for (UsedIpVO ip : sortedIps) { if (ip.getIpVersion() == IPv6Constants.IPv4) { struct.setGateway(ip.getGateway()); @@ -275,6 +284,11 @@ public List makeDhcpStruct(VmInstanceInventory vm, List> workoutDhcp(VmInstanceS for (VmNicInventory inv : spec.getDestNics()) { VmNicVO vmNicVO = dbf.findByUuid(inv.getUuid(), VmNicVO.class); for (UsedIpVO ip : vmNicVO.getUsedIps()) { + // Skip IPs outside L3 IP range (not managed by DHCP) + if (ip.getIpRangeUuid() == null) { + continue; + } + L3NetworkInventory l3 = l3Map.get(ip.getL3NetworkUuid()); if (l3 == null) { continue; diff --git a/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java b/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java index cd80c9b184f..7d421393afe 100755 --- a/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java +++ b/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java @@ -18,6 +18,16 @@ public interface NetworkServiceManager { void applyNetworkServiceOnChangeIP(VmInstanceSpec spec, NetworkServiceExtensionPoint.NetworkServiceExtensionPosition position, Completion completion); List getL3NetworkDns(String l3NetworkUuid); + /** + * Get DNS servers for a VM NIC. + * Priority: VM NIC system tag > L3 Network DNS + * + * @param vmUuid VM instance UUID + * @param l3NetworkUuid L3 network UUID + * @return List of DNS server addresses + */ + List getVmNicDns(String vmUuid, String l3NetworkUuid); + void enableNetworkService(L3NetworkVO l3VO, NetworkServiceProviderType providerType, NetworkServiceType nsType, List systemTags, Completion completion); diff --git a/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java b/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java index 5e353fa1ff2..24ab48a51de 100755 --- a/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java +++ b/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java @@ -25,9 +25,12 @@ import org.zstack.header.network.service.*; import org.zstack.header.network.service.NetworkServiceExtensionPoint.NetworkServiceExtensionPosition; import org.zstack.header.vm.*; +import org.zstack.header.tag.SystemTagVO; +import org.zstack.header.tag.SystemTagVO_; import org.zstack.query.QueryFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import java.util.*; @@ -483,6 +486,36 @@ public List getL3NetworkDns(String l3NetworkUuid){ return dns; } + @Override + public List getVmNicDns(String vmUuid, String l3NetworkUuid) { + // First try to get DNS from system tag (VM NIC-level custom DNS) + // Tag format: staticDns::{l3NetworkUuid}::{dns1,dns2,dns3} + String tagLike = String.format("staticDns::%s::%%", l3NetworkUuid); + List tags = Q.New(SystemTagVO.class) + .select(SystemTagVO_.tag) + .eq(SystemTagVO_.resourceUuid, vmUuid) + .eq(SystemTagVO_.resourceType, VmInstanceVO.class.getSimpleName()) + .like(SystemTagVO_.tag, tagLike) + .listValues(); + if (tags != null && !tags.isEmpty()) { + String tag = tags.get(0); + // Parse DNS part: staticDns::{l3Uuid}::{dnsStr} + String prefix = String.format("staticDns::%s::", l3NetworkUuid); + if (tag.startsWith(prefix)) { + String dnsStr = tag.substring(prefix.length()); + if (!dnsStr.isEmpty()) { + List dnsList = new ArrayList<>(); + for (String dns : dnsStr.split(",")) { + dnsList.add(IPv6NetworkUtils.ipv6TagValueToAddress(dns)); + } + return dnsList; + } + } + } + // Fall back to L3 network DNS + return getL3NetworkDns(l3NetworkUuid); + } + @Override public void instantiateResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, Completion completion) { preInstantiateVmResource(spec, completion); diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java index c9905a78667..2c31637659d 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java @@ -35,9 +35,20 @@ public ApplianceVmNicTO(VmNicInventory inv) { } else { ip6 = uip.getIp(); gateway6 = uip.getGateway(); - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, uip.getIpRangeUuid()).find(); - prefixLength = ipRangeVO.getPrefixLen(); - addressMode = ipRangeVO.getAddressMode(); + // First try to use prefixLen from UsedIpInventory (for IP outside range) + if (uip.getPrefixLen() != null) { + prefixLength = uip.getPrefixLen(); + addressMode = IPv6Constants.SLAAC; + } + if (uip.getIpRangeUuid() != null) { + NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, uip.getIpRangeUuid()).find(); + if (ipRangeVO != null) { + if (prefixLength == null) { + prefixLength = ipRangeVO.getPrefixLen(); + } + addressMode = ipRangeVO.getAddressMode(); + } + } } } /* for virtual router, gateway ip is in the usedIpVO */ diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java index 85e9a357a06..c1ce073cdd8 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java @@ -23,6 +23,7 @@ import org.zstack.header.vm.VmNicHelper; import org.zstack.header.vm.VmNicVO; import org.zstack.header.vm.VmNicVO_; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.service.vip.VipNetworkServicesRefVO; import org.zstack.network.service.vip.VipNetworkServicesRefVO_; import org.zstack.network.service.vip.VipState; @@ -202,6 +203,14 @@ public VipVO call() { } else { msg.setUsedIpUuid(nic.getUsedIpUuid()); } + + // Check if the IP is outside L3 CIDR range + UsedIpVO usedIpVO = dbf.findByUuid(msg.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_EIP_10024, + "cannot bind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } } private void validate(APIDetachEipMsg msg) { @@ -304,6 +313,14 @@ private void validate(APICreateEipMsg msg) { if (msg.getUsedIpUuid() != null) { isVipInVmNicSubnet(msg.getVipUuid(), msg.getUsedIpUuid()); + + // Check if the IP is outside L3 CIDR range + UsedIpVO usedIpVO = dbf.findByUuid(msg.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_EIP_10024, + "cannot bind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } } checkNicRule(msg.getVmNicUuid()); diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java index 8e23e2a9175..dae4a3e10bf 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java @@ -34,6 +34,7 @@ import org.zstack.header.query.ExpandedQueryStruct; import org.zstack.header.vm.*; import org.zstack.identity.AccountManager; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.NetworkServiceManager; import org.zstack.network.service.vip.*; @@ -416,6 +417,15 @@ private List getAttachableVmNicForEip(VipInventory vip, APIGetEi } else { ret = l3Mgr.filterVmNicByIpVersion(VmNicInventory.valueOf(nics), IPv6Constants.IPv4); } + + // Filter out NICs whose primary IP is outside L3 CIDR + final int targetIpVersion = NetworkUtils.isIpv4Address(vip.getIp()) ? IPv6Constants.IPv4 : IPv6Constants.IPv6; + ret = ret.stream().filter(nic -> { + return nic.getUsedIps().stream() + .filter(ip -> ip.getIpVersion() == targetIpVersion) + .anyMatch(ip -> IpRangeHelper.isIpInL3NetworkCidr(ip.getIp(), ip.getL3NetworkUuid())); + }).collect(Collectors.toList()); + return ret; } diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java index 962be8fe4ef..ecc0c14e2e3 100755 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java @@ -1991,7 +1991,7 @@ public DhcpInfo call(DhcpStruct arg) { List dns = new ArrayList<>(); List dns6 = new ArrayList<>(); - for (String dnsIp : nwServiceMgr.getL3NetworkDns(arg.getL3Network().getUuid())) { + for (String dnsIp : nwServiceMgr.getVmNicDns(arg.getVmUuid(), arg.getL3Network().getUuid())) { if (NetworkUtils.isIpv4Address(dnsIp)) { dns.add(dnsIp); } else { diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java index 5f0b1fb23e7..38a88a84950 100755 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java @@ -439,8 +439,15 @@ public EipTO call(EipVO eip) { to.nicIp = ip.getIp(); to.nicGateway = ip.getGateway(); to.nicNetmask = ip.getNetmask(); - NormalIpRangeVO ipr = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, ip.getIpRangeUuid()).find(); - to.nicPrefixLen = ipr.getPrefixLen(); + // First try to use prefixLen from UsedIpVO (for IP outside range) + if (ip.getPrefixLen() != null) { + to.nicPrefixLen = ip.getPrefixLen(); + } else if (ip.getIpRangeUuid() != null) { + NormalIpRangeVO ipr = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, ip.getIpRangeUuid()).find(); + if (ipr != null) { + to.nicPrefixLen = ipr.getPrefixLen(); + } + } to.vmBridgeName = bridgeNames.get(ip.getL3NetworkUuid()); } } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java index c8a1ba8c08c..f2f8271674b 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java @@ -33,6 +33,7 @@ import org.zstack.header.tag.SystemTagVO_; import org.zstack.header.vm.VmNicVO; import org.zstack.header.vm.VmNicVO_; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.service.vip.VipNetworkServicesRefVO; import org.zstack.network.service.vip.VipNetworkServicesRefVO_; import org.zstack.network.service.vip.VipVO; @@ -655,6 +656,30 @@ public void run(String arg) { q = dbf.getEntityManager().createQuery(sql, String.class); q.setParameter("uuid", msg.getListenerUuid()); msg.setLoadBalancerUuid(q.getSingleResult()); + + // When the load balancer has a VIP configured, the NIC's corresponding IP must be within L3 CIDR + LoadBalancerVO lbVO = dbf.findByUuid(msg.getLoadBalancerUuid(), LoadBalancerVO.class); + for (String nicUuid : msg.getVmNicUuids()) { + VmNicVO nicVO = dbf.findByUuid(nicUuid, VmNicVO.class); + if (nicVO == null) { + continue; + } + for (UsedIpVO usedIpVO : nicVO.getUsedIps()) { + if (!IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + continue; + } + if (lbVO.getVipUuid() != null && usedIpVO.getIpVersion() == IPv6Constants.IPv4) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10176, + "cannot add VM NIC[uuid:%s] with IPv4 address[%s] which is outside L3 network CIDR range to load balancer", + nicUuid, usedIpVO.getIp())); + } + if (lbVO.getIpv6VipUuid() != null && usedIpVO.getIpVersion() == IPv6Constants.IPv6) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10173, + "cannot add VM NIC[uuid:%s] with IPv6 address[%s] which is outside L3 network CIDR range to load balancer", + nicUuid, usedIpVO.getIp())); + } + } + } } private boolean hasTag(APIMessage msg, PatternedSystemTag tag) { @@ -1586,6 +1611,31 @@ private void validate(APIAddBackendServerToServerGroupMsg msg){ } } + // When server group has an IP version, the vmnic's corresponding IP must be within L3 CIDR + if (groupVO.getIpVersion() != null) { + for (String nicUuid : vmNicUuids) { + VmNicVO nicVO = dbf.findByUuid(nicUuid, VmNicVO.class); + if (nicVO == null) { + continue; + } + for (UsedIpVO usedIpVO : nicVO.getUsedIps()) { + if (!IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + continue; + } + if (groupVO.getIpVersion() == IPv6Constants.IPv4 && usedIpVO.getIpVersion() == IPv6Constants.IPv4) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10174, + "cannot add VM NIC[uuid:%s] with IPv4 address[%s] which is outside L3 network CIDR range to server group[uuid:%s]", + nicUuid, usedIpVO.getIp(), msg.getServerGroupUuid())); + } + if (groupVO.getIpVersion() == IPv6Constants.IPv6 && usedIpVO.getIpVersion() == IPv6Constants.IPv6) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10175, + "cannot add VM NIC[uuid:%s] with IPv6 address[%s] which is outside L3 network CIDR range to server group[uuid:%s]", + nicUuid, usedIpVO.getIp(), msg.getServerGroupUuid())); + } + } + } + } + canAddVmNic = true; } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java index f51ed2efb25..dadec74b6ec 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java @@ -43,6 +43,7 @@ import org.zstack.header.vm.*; import org.zstack.header.vo.ResourceVO; import org.zstack.identity.Account; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.vip.*; import org.zstack.tag.PatternedSystemTag; @@ -635,6 +636,14 @@ private void handle(APIGetCandidateVmNicsForLoadBalancerServerGroupMsg msg) { ipVersion = groupVO.getIpVersion(); } List nicVOS = f.getAttachableVmNicsForServerGroup(self, groupVO, ipVersion); + + // Filter out NICs whose primary IP is outside L3 CIDR + nicVOS = nicVOS.stream().filter(nic -> { + String nicIp = nic.getIp(); + String nicL3 = nic.getL3NetworkUuid(); + return nicIp == null || IpRangeHelper.isIpInL3NetworkCidr(nicIp, nicL3); + }).collect(Collectors.toList()); + reply.setInventories(VmNicInventory.valueOf(nicVOS)); bus.reply(msg, reply); } @@ -1046,6 +1055,13 @@ protected void scripts() { .filter(nic -> !listenerVO.getAttachedVmNics().contains(nic.getUuid())) .collect(Collectors.toList()); + // Filter out NICs whose primary IP is outside L3 CIDR + nics = nics.stream().filter(nic -> { + String nicIp = nic.getIp(); + String nicL3 = nic.getL3NetworkUuid(); + return nicIp == null || IpRangeHelper.isIpInL3NetworkCidr(nicIp, nicL3); + }).collect(Collectors.toList()); + reply.setInventories(callGetCandidateVmNicsForLoadBalancerExtensionPoint(msg, VmNicInventory.valueOf(nics))); } }.execute(); diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java index 43a2a4d10e7..5316be0629b 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java @@ -19,6 +19,8 @@ import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.vm.VmNicVO; import org.zstack.header.vm.VmNicVO_; +import org.zstack.header.network.l3.UsedIpVO; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.service.vip.*; import org.zstack.utils.VipUseForList; import org.zstack.utils.network.IPv6Constants; @@ -147,6 +149,17 @@ public VipVO call() { } catch (CloudRuntimeException e) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10011, e.getMessage())); } + + // Check if the NIC's IP is outside L3 CIDR range + VmNicVO nicVO = dbf.findByUuid(msg.getVmNicUuid(), VmNicVO.class); + if (nicVO != null && nicVO.getUsedIpUuid() != null) { + UsedIpVO usedIpVO = dbf.findByUuid(nicVO.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025, + "cannot bind port forwarding rule to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } + } } private boolean rangeOverlap(int s1, int e1, int s2, int e2) { @@ -243,6 +256,17 @@ private void validate(APICreatePortForwardingRuleMsg msg) { checkIfAnotherVip(msg.getVipUuid(), msg.getVmNicUuid()); checkForConflictsWithOtherRules(msg.getVmNicUuid(), msg.getPrivatePortStart(), msg.getPrivatePortEnd(), msg.getAllowedCidr(), PortForwardingProtocolType.valueOf(msg.getProtocolType())); + + // Check if the NIC's IP is outside L3 CIDR range + VmNicVO nicVO = dbf.findByUuid(msg.getVmNicUuid(), VmNicVO.class); + if (nicVO != null && nicVO.getUsedIpUuid() != null) { + UsedIpVO usedIpVO = dbf.findByUuid(nicVO.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025, + "cannot bind port forwarding rule to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } + } } if(msg.getAllowedCidr() != null){ diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java index ce34e098021..80aa7597ff1 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java @@ -37,6 +37,7 @@ import org.zstack.header.query.ExpandedQueryStruct; import org.zstack.header.vm.*; import org.zstack.identity.AccountManager; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.NetworkServiceManager; import org.zstack.network.service.vip.*; @@ -365,7 +366,16 @@ protected List scripts() { /* TODO: only ipv4 portforwarding is supported */ List nicInvs = VmNicInventory.valueOf(nics.stream().filter(nic -> !usedVm.contains(nic.getVmInstanceUuid())).collect(Collectors.toList())); - return l3Mgr.filterVmNicByIpVersion(nicInvs, IPv6Constants.IPv4); + List filtered = l3Mgr.filterVmNicByIpVersion(nicInvs, IPv6Constants.IPv4); + + // Filter out NICs whose primary IP is outside L3 CIDR + filtered = filtered.stream().filter(nic -> { + String nicIp = nic.getIp(); + String nicL3 = nic.getL3NetworkUuid(); + return nicIp == null || IpRangeHelper.isIpInL3NetworkCidr(nicIp, nicL3); + }).collect(Collectors.toList()); + + return filtered; } }.execute(); } diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java index 610331e5f6c..a876ee24a34 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java @@ -2092,6 +2092,9 @@ void applianceVmsDeleteIpByIpRanges(List applianceVmVOS, vo = dbf.findByUuid(vo.getUuid(), ApplianceVmVO.class); for (VmNicVO nic : vo.getVmNics()) { for (UsedIpVO ip : nic.getUsedIps()) { + if (ip.getIpRangeUuid() == null) { + continue; + } if (ip.getIpVersion() == IPv6Constants.IPv4 && ipv4RangeUuids.contains(ip.getIpRangeUuid())) { ReturnIpMsg rmsg = new ReturnIpMsg(); rmsg.setL3NetworkUuid(ip.getL3NetworkUuid()); @@ -2139,7 +2142,7 @@ public List applianceVmsToDeleteNicByIpRanges(List appli for (ApplianceVmVO vo : applianceVmVOS) { for (VmNicVO nic : vo.getVmNics()) { for (UsedIpVO ip : nic.getUsedIps()) { - if (!iprUuids.contains(ip.getIpRangeUuid())) { + if (ip.getIpRangeUuid() == null || !iprUuids.contains(ip.getIpRangeUuid())) { continue; } @@ -2172,7 +2175,7 @@ public List applianceVmsToBeDeletedByIpRanges(List } /* if any ip of the nic is deleted, delete the appliance vm */ - if (nic.getUsedIps().stream().anyMatch(ip -> iprUuids.contains(ip.getIpRangeUuid()))) { + if (nic.getUsedIps().stream().anyMatch(ip -> ip.getIpRangeUuid() != null && iprUuids.contains(ip.getIpRangeUuid()))) { toDeleted.add(vos); break; } diff --git a/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java b/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java index ffec933f2cc..59beff6880a 100644 --- a/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java +++ b/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java @@ -34,6 +34,9 @@ public Result throwExceptionIfError() { @Param(required = false) public java.lang.String staticIp; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List dnsAddresses; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java b/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java index 8240899d354..3deb766e3ce 100644 --- a/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java +++ b/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java @@ -49,6 +49,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String ipv6Prefix; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List dnsAddresses; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java b/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java index 8befbe48ff7..5c47e90bb46 100644 --- a/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java @@ -52,6 +52,14 @@ public java.lang.String getNetmask() { return this.netmask; } + public java.lang.Integer prefixLen; + public void setPrefixLen(java.lang.Integer prefixLen) { + this.prefixLen = prefixLen; + } + public java.lang.Integer getPrefixLen() { + return this.prefixLen; + } + public java.lang.String gateway; public void setGateway(java.lang.String gateway) { this.gateway = gateway; diff --git a/test/pom.xml b/test/pom.xml index 7dfbb0bbcac..6505ec9324c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -9,7 +9,7 @@ zstack org.zstack - 5.5.0 + 5.5.0 .. test @@ -292,6 +292,12 @@ org.jasig.cas.client cas-client-core + + org.zstack + eip + 5.5.0 + test + diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy new file mode 100644 index 00000000000..62b71e75e04 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy @@ -0,0 +1,923 @@ +package org.zstack.test.integration.networkservice.provider.flat + +import org.springframework.http.HttpEntity +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.Q +import org.zstack.header.network.l3.UsedIpVO +import org.zstack.header.network.l3.UsedIpVO_ +import org.zstack.header.network.service.NetworkServiceType +import org.zstack.header.vm.VmNicVO +import org.zstack.network.securitygroup.SecurityGroupConstant +import org.zstack.network.service.eip.EipConstant +import org.zstack.network.service.flat.FlatDhcpBackend +import org.zstack.network.service.flat.FlatNetworkServiceConstant +import org.zstack.network.service.userdata.UserdataConstant +import org.zstack.sdk.* +import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil +import org.zstack.utils.network.IPv6Constants + +/** + * Test IP outside CIDR behavior for flat networks. + * Outside-range IPs are always allowed (no global config needed). + * + * Flat network combinations (3 combos): + * 1. flatL3_noRange_noDhcp — no IP range, no DHCP (enableIPAM=false, enableIpAddressAllocation()=false) + * 2. flatL3_range_noDhcp — has IP range, no DHCP (enableIPAM=true, enableIpAddressAllocation()=false) + * 3. flatL3_range_dhcp — has IP range, has DHCP (enableIPAM=true, enableIpAddressAllocation()=true) + * + * pubL3_range_dhcp is included only as VIP network for EIP tests. + * + * Each scenario tests: setVmStaticIp, changeVmNicNetwork, DHCP skip, EIP rejection. + * Additional: orphan IP backfill when adding IP range. + */ +class FlatChangeVmIpOutsideCidrCase extends SubCase { + + EnvSpec env + DatabaseFacade dbf + + @Override + void setup() { + useSpring(NetworkServiceProviderTest.springSpec) + } + + @Override + void clean() { + env.delete() + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "127.0.0.1" + username = "root" + password = "password" + } + + attachPrimaryStorage("local") + attachL2Network("l2-flat-noRange-noDhcp") + attachL2Network("l2-flat-range-noDhcp") + attachL2Network("l2-flat-range-dhcp") + attachL2Network("l2-pub-range-dhcp") + attachL2Network("l2-backfill") + attachL2Network("l2-dest") + } + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + // ========== Flat: no IP range, no DHCP ========== + l2NoVlanNetwork { + name = "l2-flat-noRange-noDhcp" + physicalInterface = "eth0" + + l3Network { + name = "flatL3_noRange_noDhcp" + enableIPAM = false + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range, no DHCP + } + } + + // ========== Flat: has IP range, no DHCP ========== + l2NoVlanNetwork { + name = "l2-flat-range-noDhcp" + physicalInterface = "eth1" + + l3Network { + name = "flatL3_range_noDhcp" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.200" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + + // ========== Flat: has IP range, has DHCP ========== + l2NoVlanNetwork { + name = "l2-flat-range-dhcp" + physicalInterface = "eth2" + + l3Network { + name = "flatL3_range_dhcp" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [NetworkServiceType.DHCP.toString(), + UserdataConstant.USERDATA_TYPE_STRING, + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "192.168.200.10" + endIp = "192.168.200.200" + netmask = "255.255.255.0" + gateway = "192.168.200.1" + } + } + } + + // ========== Public: has IP range, has DHCP (VIP network for EIP tests only) ========== + l2NoVlanNetwork { + name = "l2-pub-range-dhcp" + physicalInterface = "eth4" + + l3Network { + name = "pubL3_range_dhcp" + category = "Public" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [NetworkServiceType.DHCP.toString(), + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "12.100.20.10" + endIp = "12.100.20.200" + netmask = "255.255.255.0" + gateway = "12.100.20.1" + } + } + } + + // ========== Dedicated L2/L3 for orphan IP backfill test ========== + l2NoVlanNetwork { + name = "l2-backfill" + physicalInterface = "eth5" + + l3Network { + name = "flatL3_backfill" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range initially, no DHCP + } + } + + // ========== Destination L3 for changeVmNicNetwork tests ========== + l2NoVlanNetwork { + name = "l2-dest" + physicalInterface = "eth6" + + l3Network { + name = "flatL3_dest" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range, no DHCP — used as changeVmNicNetwork source + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + dbf = bean(DatabaseFacade.class) + env.create { + // ========================================== + // Outside-range IPs are always allowed (no global config needed) + // ========================================== + + // --- Flat: no IP range, no DHCP --- + testSetStaticIp_flatNoRangeNoDhcp() + testChangeNicNetwork_flatNoRangeNoDhcp() + testDhcpSkip_flatNoRangeNoDhcp() + testEipReject_flatNoRangeNoDhcp() + + // --- Flat: has IP range, no DHCP --- + testSetStaticIp_flatRangeNoDhcp() + testChangeNicNetwork_flatRangeNoDhcp() + testDhcpSkip_flatRangeNoDhcp() + testEipReject_flatRangeNoDhcp() + + // --- Flat: has IP range, has DHCP --- + testSetStaticIp_flatRangeDhcp() + testChangeNicNetwork_flatRangeDhcp() + testDhcpSkip_flatRangeDhcp() + testEipReject_flatRangeDhcp() + + // --- NIC DNS priority --- + testNicDnsPreservedWhenApiOmitsDns_flatRangeDhcp() + testNicDnsRemovedWhenEmptyDnsList_flatRangeDhcp() + + + // ========================================== + // Orphan IP backfill + // ========================================== + testOrphanIpBackfillOnAddIpRange() + } + } + + + // ================================================================ + // Helper: create a VM on a given L3 + // ================================================================ + + VmInstanceInventory createVmOnL3(String vmName, String l3Uuid) { + return createVmInstance { + name = vmName + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [l3Uuid] + } + } + + // ================================================================ + // Flat: no IP range, no DHCP + // ================================================================ + + /** + * Flat/no-range/no-DHCP: setVmStaticIp with outside-range IP should succeed. + * Must provide netmask/gateway explicitly (no IP range to default from). + */ + void testSetStaticIp_flatNoRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_noRange_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-flat-noRange-noDhcp-set", l3.uuid) + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "172.16.0.50" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + } + + // Verify UsedIpVO + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "172.16.0.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.0.0" + assert usedIp.gateway == "172.16.0.1" + + // Verify VmNicVO + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "172.16.0.50" + assert nicVO.netmask == "255.255.0.0" + assert nicVO.gateway == "172.16.0.1" + } + + /** + * Flat/no-range/no-DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_flatNoRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_noRange_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-flat-noRange-noDhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::172.16.0.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.0.0", l3.uuid), + String.format("ipv4Gateway::%s::172.16.0.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "172.16.0.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "172.16.0.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Flat/no-range/no-DHCP: outside-range IP should NOT appear in DHCP messages on reboot. + */ + void testDhcpSkip_flatNoRangeNoDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-noRange-noDhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpApplied = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "172.16.0.50") { dhcpApplied = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "172.16.0.50") { dhcpApplied = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpApplied : "DHCP should NOT include outside-range IP 172.16.0.50 on no-DHCP L3" + } + + + /** + * Flat/no-range/no-DHCP: EIP should reject binding to NIC with outside-range IP. + */ + void testEipReject_flatNoRangeNoDhcp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3_range_dhcp") + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-noRange-noDhcp-set"] }[0] + L3NetworkInventory l3 = env.inventoryByName("flatL3_noRange_noDhcp") + VmNicInventory nic = vm.vmNics.find { it.l3NetworkUuid == l3.uuid } + assert nic != null + + VipInventory vip = createVip { + name = "vip-flat-noRange-noDhcp" + l3NetworkUuid = pubL3.uuid + } + EipInventory eip = createEip { + name = "eip-flat-noRange-noDhcp" + vipUuid = vip.uuid + } + + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nic.uuid + } + } + } + + // ================================================================ + // Part 2: Global config ON — Flat: has IP range, no DHCP + // ================================================================ + + /** + * Flat/range/no-DHCP: outside-range IP should succeed; in-range IP also works. + */ + void testSetStaticIp_flatRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-noDhcp-set", l3.uuid) + + // Outside-range IP should succeed + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.0.50" + netmask = "255.255.255.0" + gateway = "10.0.0.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.0.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.0.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.0.50" + + // Also verify in-range IP works + VmInstanceInventory vm2 = createVmOnL3("vm-flat-range-noDhcp-inrange", l3.uuid) + List freeIps1 = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String inRangeIp1 = freeIps1.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm2.uuid + l3NetworkUuid = l3.uuid + ip = inRangeIp1 + } + + UsedIpVO inRangeIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) + .eq(UsedIpVO_.ip, inRangeIp1) + .find() + assert inRangeIp != null + assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" + } + + /** + * Flat/range/no-DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_flatRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-noDhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.0.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.0.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.0.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Flat/range/no-DHCP: outside-range IP should NOT appear in DHCP messages on reboot. + */ + void testDhcpSkip_flatRangeNoDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-noDhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpApplied = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.0.50") { dhcpApplied = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.0.50") { dhcpApplied = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpApplied : "DHCP should NOT include outside-range IP 10.0.0.50 on no-DHCP L3" + } + + + /** + * Flat/range/no-DHCP: EIP should reject binding to NIC with outside-range IP. + */ + void testEipReject_flatRangeNoDhcp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3_range_dhcp") + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-noDhcp-set"] }[0] + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + VmNicInventory nic = vm.vmNics.find { it.l3NetworkUuid == l3.uuid } + assert nic != null + + VipInventory vip = createVip { + name = "vip-flat-range-noDhcp" + l3NetworkUuid = pubL3.uuid + } + EipInventory eip = createEip { + name = "eip-flat-range-noDhcp" + vipUuid = vip.uuid + } + + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nic.uuid + } + } + } + + // ================================================================ + // Part 2: Global config ON — Flat: has IP range, has DHCP + // ================================================================ + + /** + * Flat/range/DHCP: outside-range IP should succeed with global config ON; + * in-range IP also works normally. + */ + void testSetStaticIp_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-dhcp-set", l3.uuid) + + // Outside-range IP should succeed with global config ON + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.1.50" + netmask = "255.255.255.0" + gateway = "10.0.1.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.1.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.1.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.1.50" + + // Also verify in-range IP works + VmInstanceInventory vm2 = createVmOnL3("vm-flat-range-dhcp-inrange", l3.uuid) + List freeIps2 = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String inRangeIp2 = freeIps2.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm2.uuid + l3NetworkUuid = l3.uuid + ip = inRangeIp2 + } + + UsedIpVO inRangeIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) + .eq(UsedIpVO_.ip, inRangeIp2) + .find() + assert inRangeIp != null + assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" + } + + /** + * Flat/range/DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-dhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.1.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.1.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.1.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.1.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Flat/range/DHCP: outside-range IP should NOT appear in DHCP messages on reboot, + * even though DHCP service is enabled on this L3. + */ + void testDhcpSkip_flatRangeDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-dhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpAppliedForOutsideIp = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.1.50") { dhcpAppliedForOutsideIp = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.1.50") { dhcpAppliedForOutsideIp = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpAppliedForOutsideIp : "DHCP should NOT include outside-range IP 10.0.1.50 even on DHCP-enabled L3" + } + + + /** + * Flat/range/DHCP: EIP should reject binding to NIC with outside-range IP. + */ + void testEipReject_flatRangeDhcp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3_range_dhcp") + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-dhcp-set"] }[0] + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + VmNicInventory nic = vm.vmNics.find { it.l3NetworkUuid == l3.uuid } + assert nic != null + + VipInventory vip = createVip { + name = "vip-flat-range-dhcp" + l3NetworkUuid = pubL3.uuid + } + EipInventory eip = createEip { + name = "eip-flat-range-dhcp" + vipUuid = vip.uuid + } + + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nic.uuid + } + } + } + + // ================================================================ + // NIC DNS priority: NIC DNS > L3 DNS + // ================================================================ + + /** + * When setVmStaticIp is called with dnsAddresses, NIC-level DNS is used in DHCP. + * When setVmStaticIp is called again WITHOUT dnsAddresses (null), old NIC DNS is preserved. + */ + void testNicDnsPreservedWhenApiOmitsDns_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + + // Add L3-level DNS + addDnsToL3Network { + l3NetworkUuid = l3.uuid + dns = "8.8.8.8" + } + + // Create a VM on the DHCP-enabled L3 + VmInstanceInventory vm = createVmOnL3("vm-nic-dns-preserve", l3.uuid) + + // Set NIC-level DNS via setVmStaticIp + List freeIps = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String ip1 = freeIps.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = ip1 + dnsAddresses = ["1.1.1.1", "1.0.0.1"] + } + + // Intercept DHCP apply and reboot to trigger DHCP re-apply + FlatDhcpBackend.BatchApplyDhcpCmd cmd1 = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + cmd1 = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert cmd1 != null + def dhcp1 = cmd1.dhcpInfos.collectMany { it.dhcp }.find { it.ip == ip1 } + assert dhcp1 != null : "DHCP entry for IP ${ip1} should exist" + assert dhcp1.dns == ["1.1.1.1", "1.0.0.1"] : "NIC DNS should override L3 DNS, got: ${dhcp1.dns}" + + // Now call setVmStaticIp again WITHOUT dnsAddresses (only change IP) + // dnsAddresses is null => old NIC DNS should be preserved + List freeIps2 = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String ip2 = freeIps2.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = ip2 + // dnsAddresses is NOT set => null in the message + } + + FlatDhcpBackend.BatchApplyDhcpCmd cmd2 = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + cmd2 = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert cmd2 != null + def dhcp2 = cmd2.dhcpInfos.collectMany { it.dhcp }.find { it.ip == ip2 } + assert dhcp2 != null : "DHCP entry for IP ${ip2} should exist" + assert dhcp2.dns == ["1.1.1.1", "1.0.0.1"] : "NIC DNS should be preserved when API omits dnsAddresses, got: ${dhcp2.dns}" + } + + /** + * When setVmStaticIp is called with an empty dnsAddresses list ([]), + * NIC DNS is removed and DHCP falls back to L3 network DNS. + */ + void testNicDnsRemovedWhenEmptyDnsList_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + + // The previous test already added "8.8.8.8" as L3 DNS and created "vm-nic-dns-preserve" + // with NIC DNS ["1.1.1.1", "1.0.0.1"]. Reuse that VM. + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-nic-dns-preserve"] }[0] + + // Must use a different IP because setVmStaticIp rejects the same IP + List freeIps = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String newIp = freeIps.get(0).getIp() + + // Call setVmStaticIp with a new IP and explicit empty dnsAddresses => removes NIC DNS + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = newIp + dnsAddresses = [] + } + + // Intercept DHCP apply and reboot + FlatDhcpBackend.BatchApplyDhcpCmd cmd = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert cmd != null + def dhcp = cmd.dhcpInfos.collectMany { it.dhcp }.find { it.ip == newIp } + assert dhcp != null : "DHCP entry for IP ${newIp} should exist" + assert dhcp.dns.contains("8.8.8.8") : "After removing NIC DNS, DHCP should fall back to L3 DNS (8.8.8.8), got: ${dhcp.dns}" + assert !dhcp.dns.contains("1.1.1.1") : "NIC DNS 1.1.1.1 should no longer be present after clearing, got: ${dhcp.dns}" + assert !dhcp.dns.contains("1.0.0.1") : "NIC DNS 1.0.0.1 should no longer be present after clearing, got: ${dhcp.dns}" + } + + + // ================================================================ + // Part 3: Orphan IP backfill (global config ON) + // ================================================================ + + /** + * Create orphan IPs on flatL3_backfill (no IP range), + * then add an IP range covering the orphan IPs. + * Verify ipRangeUuid is backfilled and capacity increases. + */ + void testOrphanIpBackfillOnAddIpRange() { + L3NetworkInventory backfillL3 = env.inventoryByName("flatL3_backfill") + + // Step 1: create VMs and assign outside-range IPs + VmInstanceInventory orphanVm1 = createVmOnL3("vm-backfill-orphan-1", backfillL3.uuid) + setVmStaticIp { + vmInstanceUuid = orphanVm1.uuid + l3NetworkUuid = backfillL3.uuid + ip = "172.16.0.80" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + } + + VmInstanceInventory orphanVm2 = createVmOnL3("vm-backfill-orphan-2", backfillL3.uuid) + setVmStaticIp { + vmInstanceUuid = orphanVm2.uuid + l3NetworkUuid = backfillL3.uuid + ip = "172.16.0.90" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + } + + // Confirm orphan IPs exist + long outsideCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, backfillL3.uuid) + .isNull(UsedIpVO_.ipRangeUuid) + .count() + assert outsideCount == 2 : "There should be 2 orphan IPs on flatL3_backfill" + + // Step 2: record capacity before backfill + GetIpAddressCapacityResult beforeBackfill = getIpAddressCapacity { + l3NetworkUuids = [backfillL3.uuid] + } + + // Step 3: add IP range covering the orphan IPs (172.16.0.80, 172.16.0.90) + IpRangeInventory ipRange = addIpRange { + delegate.name = "backfill-ip-range" + delegate.l3NetworkUuid = backfillL3.uuid + delegate.startIp = "172.16.0.2" + delegate.endIp = "172.16.0.253" + delegate.gateway = "172.16.0.1" + delegate.netmask = "255.255.0.0" + } + + // Step 4: verify orphan IPs now have ipRangeUuid backfilled + long backfilledCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, backfillL3.uuid) + .eq(UsedIpVO_.ipRangeUuid, ipRange.uuid) + .count() + assert backfilledCount == outsideCount : + "all ${outsideCount} orphan IPs should now be associated with the new IP range" + + // No more orphan IPs + long remainingOrphanCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, backfillL3.uuid) + .isNull(UsedIpVO_.ipRangeUuid) + .count() + assert remainingOrphanCount == 0 : "all orphan IPs should now have ipRangeUuid" + + // Step 5: capacity should now include the backfilled IPs + GetIpAddressCapacityResult afterBackfill = getIpAddressCapacity { + l3NetworkUuids = [backfillL3.uuid] + } + assert afterBackfill.totalCapacity > beforeBackfill.totalCapacity : + "totalCapacity should increase after adding new IP range" + assert afterBackfill.usedIpAddressNumber == beforeBackfill.usedIpAddressNumber + outsideCount : + "usedIpAddressNumber should increase by ${outsideCount} after backfill" + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy new file mode 100644 index 00000000000..cd226cac406 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy @@ -0,0 +1,443 @@ +package org.zstack.test.integration.networkservice.provider.flat + +import org.springframework.http.HttpEntity +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.Q +import org.zstack.header.network.l3.UsedIpVO +import org.zstack.header.network.l3.UsedIpVO_ +import org.zstack.header.network.service.NetworkServiceType +import org.zstack.header.vm.VmNicVO +import org.zstack.network.securitygroup.SecurityGroupConstant +import org.zstack.network.service.eip.EipConstant +import org.zstack.network.service.flat.FlatDhcpBackend +import org.zstack.network.service.flat.FlatNetworkServiceConstant +import org.zstack.sdk.* +import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil +import org.zstack.utils.network.IPv6Constants + +/** + * Test IP outside CIDR behavior for public networks. + * Outside-range IPs are always allowed (no global config needed). + * + * Public network combinations (2 combos): + * 1. pubL3_range_noDhcp — has IP range, no DHCP (enableIPAM=true, enableIpAddressAllocation()=false) + * 2. pubL3_range_dhcp — has IP range, has DHCP (enableIPAM=true, enableIpAddressAllocation()=true) + * + * Each scenario tests: setVmStaticIp, changeVmNicNetwork, DHCP skip. + * No EIP tests — public networks do not need EIP testing. + */ +class PublicNetworkChangeVmIpOutsideCidrCase extends SubCase { + + EnvSpec env + DatabaseFacade dbf + + @Override + void setup() { + useSpring(NetworkServiceProviderTest.springSpec) + } + + @Override + void clean() { + env.delete() + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "127.0.0.1" + username = "root" + password = "password" + } + + attachPrimaryStorage("local") + attachL2Network("l2-pub-range-noDhcp") + attachL2Network("l2-pub-range-dhcp") + attachL2Network("l2-dest") + } + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + // ========== Public: has IP range, no DHCP ========== + l2NoVlanNetwork { + name = "l2-pub-range-noDhcp" + physicalInterface = "eth0" + + l3Network { + name = "pubL3_range_noDhcp" + category = "Public" + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "12.100.10.10" + endIp = "12.100.10.200" + netmask = "255.255.255.0" + gateway = "12.100.10.1" + } + } + } + + // ========== Public: has IP range, has DHCP ========== + l2NoVlanNetwork { + name = "l2-pub-range-dhcp" + physicalInterface = "eth1" + + l3Network { + name = "pubL3_range_dhcp" + category = "Public" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [NetworkServiceType.DHCP.toString(), + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "12.100.20.10" + endIp = "12.100.20.200" + netmask = "255.255.255.0" + gateway = "12.100.20.1" + } + } + } + + // ========== Destination L3 for changeVmNicNetwork tests ========== + l2NoVlanNetwork { + name = "l2-dest" + physicalInterface = "eth2" + + l3Network { + name = "flatL3_dest" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range, no DHCP — used as changeVmNicNetwork source + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + dbf = bean(DatabaseFacade.class) + env.create { + // ========================================== + // Outside-range IPs are always allowed (no global config needed) + // ========================================== + + // --- Public: has IP range, no DHCP --- + testSetStaticIp_pubRangeNoDhcp() + testChangeNicNetwork_pubRangeNoDhcp() + testDhcpSkip_pubRangeNoDhcp() + + // --- Public: has IP range, has DHCP --- + testSetStaticIp_pubRangeDhcp() + testChangeNicNetwork_pubRangeDhcp() + testDhcpSkip_pubRangeDhcp() + } + } + + // ================================================================ + // Helper: create a VM on a given L3 + // ================================================================ + + VmInstanceInventory createVmOnL3(String vmName, String l3Uuid) { + return createVmInstance { + name = vmName + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [l3Uuid] + } + } + + // ================================================================ + // Public: has IP range, no DHCP + // ================================================================ + + /** + * Public/range/no-DHCP: outside-range IP should succeed; in-range IP also works. + */ + void testSetStaticIp_pubRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-noDhcp-set", l3.uuid) + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.2.50" + netmask = "255.255.255.0" + gateway = "10.0.2.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.2.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.2.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.2.50" + + // Also verify in-range IP works + VmInstanceInventory vm2 = createVmOnL3("vm-pub-range-noDhcp-inrange", l3.uuid) + List freeIps1 = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String inRangeIp1 = freeIps1.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm2.uuid + l3NetworkUuid = l3.uuid + ip = inRangeIp1 + } + + UsedIpVO inRangeIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) + .eq(UsedIpVO_.ip, inRangeIp1) + .find() + assert inRangeIp != null + assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" + } + + /** + * Public/range/no-DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_pubRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-noDhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.2.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.2.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.2.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.2.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Public/range/no-DHCP: outside-range IP should NOT appear in DHCP messages on reboot. + */ + void testDhcpSkip_pubRangeNoDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-pub-range-noDhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpApplied = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.2.50") { dhcpApplied = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.2.50") { dhcpApplied = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpApplied : "DHCP should NOT include outside-range IP 10.0.2.50" + } + + // ================================================================ + // Part 2: Global config ON — Public: has IP range, has DHCP + // ================================================================ + + /** + * Public/range/DHCP: outside-range IP should succeed; in-range IP also works. + */ + void testSetStaticIp_pubRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_dhcp") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-dhcp-set", l3.uuid) + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.3.50" + netmask = "255.255.255.0" + gateway = "10.0.3.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.3.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.3.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.3.50" + + // Also verify in-range IP works + VmInstanceInventory vm2 = createVmOnL3("vm-pub-range-dhcp-inrange", l3.uuid) + List freeIps2 = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String inRangeIp2 = freeIps2.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm2.uuid + l3NetworkUuid = l3.uuid + ip = inRangeIp2 + } + + UsedIpVO inRangeIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) + .eq(UsedIpVO_.ip, inRangeIp2) + .find() + assert inRangeIp != null + assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" + } + + /** + * Public/range/DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_pubRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_dhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-dhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.3.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.3.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.3.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.3.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Public/range/DHCP: outside-range IP should NOT appear in DHCP messages on reboot, + * even though DHCP service is enabled on this L3. + */ + void testDhcpSkip_pubRangeDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-pub-range-dhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpAppliedForOutsideIp = false + boolean dhcpTriggered = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + dhcpTriggered = true + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.3.50") { dhcpAppliedForOutsideIp = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + dhcpTriggered = true + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.3.50") { dhcpAppliedForOutsideIp = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpTriggered : "expected DHCP backend to run on a DHCP-enabled public L3" + assert !dhcpAppliedForOutsideIp : "DHCP should NOT include outside-range IP 10.0.3.50 even on DHCP-enabled public L3" + } +} + diff --git a/testlib/src/main/java/org/zstack/testlib/Test.groovy b/testlib/src/main/java/org/zstack/testlib/Test.groovy index dc3b369f8b2..40f46defad1 100755 --- a/testlib/src/main/java/org/zstack/testlib/Test.groovy +++ b/testlib/src/main/java/org/zstack/testlib/Test.groovy @@ -1001,6 +1001,20 @@ mysqldump -u root zstack > ${failureLogDir.absolutePath}/dbdump.sql } } + /** + * Expect an API call to fail and verify the error details. + * The second closure's delegate is the parsed {@link ErrorCodeList}, so you can + * directly access {@code code}, {@code details}, {@code globalErrorCode}, etc. + * + * Example: + *

+     *   expectApiFailure {
+     *       someApiCall { ... }
+     *   } {
+     *       assert details.contains("expected error keyword")
+     *   }
+     * 
+ */ static void expectApiFailure(Closure c, @DelegatesTo(strategy = Closure.OWNER_FIRST, value = ErrorCodeList.class) Closure errorCodeChecker) { AssertionError error = null 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..e84e33e7412 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -970,6 +970,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_L3_10078 = "ORG_ZSTACK_NETWORK_L3_10078"; + public static final String ORG_ZSTACK_NETWORK_L3_10079 = "ORG_ZSTACK_NETWORK_L3_10079"; + + public static final String ORG_ZSTACK_NETWORK_L3_10080 = "ORG_ZSTACK_NETWORK_L3_10080"; + + public static final String ORG_ZSTACK_NETWORK_L3_10081 = "ORG_ZSTACK_NETWORK_L3_10081"; + public static final String ORG_ZSTACK_SNS_PLATFORM_UNIVERSALSMS_SUPPLIER_EMAY_10000 = "ORG_ZSTACK_SNS_PLATFORM_UNIVERSALSMS_SUPPLIER_EMAY_10000"; public static final String ORG_ZSTACK_CORE_VALIDATION_10000 = "ORG_ZSTACK_CORE_VALIDATION_10000"; @@ -3234,6 +3240,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10024 = "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10024"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025 = "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025"; + public static final String ORG_ZSTACK_STORAGE_DEVICE_10000 = "ORG_ZSTACK_STORAGE_DEVICE_10000"; public static final String ORG_ZSTACK_STORAGE_DEVICE_10001 = "ORG_ZSTACK_STORAGE_DEVICE_10001"; @@ -9850,6 +9858,22 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_COMPUTE_VM_10320 = "ORG_ZSTACK_COMPUTE_VM_10320"; + public static final String ORG_ZSTACK_COMPUTE_VM_10321 = "ORG_ZSTACK_COMPUTE_VM_10321"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10322 = "ORG_ZSTACK_COMPUTE_VM_10322"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10323 = "ORG_ZSTACK_COMPUTE_VM_10323"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10324 = "ORG_ZSTACK_COMPUTE_VM_10324"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10325 = "ORG_ZSTACK_COMPUTE_VM_10325"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10326 = "ORG_ZSTACK_COMPUTE_VM_10326"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10327 = "ORG_ZSTACK_COMPUTE_VM_10327"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10328 = "ORG_ZSTACK_COMPUTE_VM_10328"; + public static final String ORG_ZSTACK_IDENTITY_LOGIN_10000 = "ORG_ZSTACK_IDENTITY_LOGIN_10000"; public static final String ORG_ZSTACK_STORAGE_VOLUME_BLOCK_EXPON_10000 = "ORG_ZSTACK_STORAGE_VOLUME_BLOCK_EXPON_10000"; @@ -10402,6 +10426,7 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SECURITYGROUP_10129 = "ORG_ZSTACK_NETWORK_SECURITYGROUP_10129"; + public static final String ORG_ZSTACK_TEMPLATECONFIG_10000 = "ORG_ZSTACK_TEMPLATECONFIG_10000"; public static final String ORG_ZSTACK_TEMPLATECONFIG_10001 = "ORG_ZSTACK_TEMPLATECONFIG_10001"; @@ -13628,6 +13653,14 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10172 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10172"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10173 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10173"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10174 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10174"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10175 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10175"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10176 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10176"; + public static final String ORG_ZSTACK_IPSEC_10000 = "ORG_ZSTACK_IPSEC_10000"; public static final String ORG_ZSTACK_IPSEC_10001 = "ORG_ZSTACK_IPSEC_10001"; @@ -15150,6 +15183,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_EIP_10023 = "ORG_ZSTACK_NETWORK_SERVICE_EIP_10023"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_EIP_10024 = "ORG_ZSTACK_NETWORK_SERVICE_EIP_10024"; + public static final String ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10000 = "ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10000"; public static final String ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10001 = "ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10001"; diff --git a/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java b/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java index 9803946ee25..f190a2f278b 100644 --- a/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java +++ b/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java @@ -1,5 +1,7 @@ package org.zstack.utils.network; +import java.util.List; + public class NicIpAddressInfo { public String ipv4Address; public String ipv4Gateway; @@ -7,6 +9,7 @@ public class NicIpAddressInfo { public String ipv6Address; public String ipv6Gateway; public String ipv6Prefix; + public List dnsAddresses; public NicIpAddressInfo(String ipv4Address, String ipv4Gateway, String ipv4Netmask, String ipv6Address, String ipv6Gateway, String ipv6Prefix) { this.ipv4Address = ipv4Address; @@ -15,5 +18,6 @@ public NicIpAddressInfo(String ipv4Address, String ipv4Gateway, String ipv4Netma this.ipv6Address = ipv6Address; this.ipv6Gateway = ipv6Gateway; this.ipv6Prefix = ipv6Prefix; + this.dnsAddresses = null; } } From 44aec1912887e0046e82f7a6d06ef76e25856abd Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Tue, 17 Mar 2026 12:45:34 +0800 Subject: [PATCH 36/77] [l2network]: validate physicalInterface for LinuxBridge When vSwitchType is LinuxBridge, physicalInterface must not be null or empty. Add interceptor check in L2NetworkApiInterceptor and unit test in AttachL2NetworkCase. ZSTAC-83300 Change-Id: I333a5bac866354c366051fece84e447f089e2306 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../network/l2/L2NetworkApiInterceptor.java | 6 ++++ .../l2network/AttachL2NetworkCase.groovy | 31 +++++++++++++++++++ .../CloudOperationsErrorCode.java | 2 ++ 3 files changed, 39 insertions(+) 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..40786eb7b2e 100755 --- a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java @@ -123,6 +123,12 @@ private void validate(APICreateL2NetworkMsg msg) { } catch (Exception e) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10012, "unsupported vSwitch type[%s]", msg.getvSwitchType())); } + + if (L2NetworkConstant.VSWITCH_TYPE_LINUX_BRIDGE.equals(msg.getvSwitchType()) + && (msg.getPhysicalInterface() == null || msg.getPhysicalInterface().trim().isEmpty())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10021, + "physicalInterface is required when vSwitchType is [%s]", msg.getvSwitchType())); + } } private void validate(APIChangeL2NetworkVlanIdMsg msg) { diff --git a/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy b/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy index f31d19bae92..7d5e8bbfa1d 100644 --- a/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy @@ -94,6 +94,7 @@ public class AttachL2NetworkCase extends SubCase{ @Override public void test() { env.create { + testCreateL2NetworkWithoutPhysicalInterface() testAttachL2NoVlanNetwork() testAttachL2ValnNetwork() testAttachL2NoVlanNetworkSynchronously() @@ -103,6 +104,36 @@ public class AttachL2NetworkCase extends SubCase{ } + void testCreateL2NetworkWithoutPhysicalInterface() { + ZoneInventory zone = env.inventoryByName("zone") + + // creating L2 NoVlan network without physicalInterface should fail when vSwitchType is LinuxBridge + expect(AssertionError.class) { + createL2NoVlanNetwork { + name = "test-no-physical-interface" + zoneUuid = zone.uuid + } + } + + // creating L2 NoVlan network with empty physicalInterface should also fail + expect(AssertionError.class) { + createL2NoVlanNetwork { + name = "test-empty-physical-interface" + zoneUuid = zone.uuid + physicalInterface = "" + } + } + + // creating L2 Vlan network without physicalInterface should fail when vSwitchType is LinuxBridge + expect(AssertionError.class) { + createL2VlanNetwork { + name = "test-vlan-no-physical-interface" + zoneUuid = zone.uuid + vlan = 100 + } + } + } + void testAttachL2NoVlanNetwork(){ L2NetworkInventory l21 = env.inventoryByName("l2-1") L2NetworkInventory l22 = env.inventoryByName("l2-2") 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..f88b2100e08 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -1016,6 +1016,8 @@ public class CloudOperationsErrorCode { 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"; public static final String ORG_ZSTACK_CONSOLE_10001 = "ORG_ZSTACK_CONSOLE_10001"; From 76044ca40c742272129d8f8970aa23133006030d Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Tue, 17 Mar 2026 17:07:06 +0800 Subject: [PATCH 37/77] [docs]: move to zstack/docs Resolves: ZSTAC-81969 Change-Id: I7176617669636d7362616973787a68766c6d6579 --- docs/modules/network/nav.adoc | 1 + .../network/pages/networkResource/l3Ipam.adoc | 202 ++++++++++++++++++ .../networkResource/networkResource.adoc | 1 + .../java/org/zstack/network/l3/zstack ipam.md | 139 ------------ 4 files changed, 204 insertions(+), 139 deletions(-) create mode 100644 docs/modules/network/pages/networkResource/l3Ipam.adoc delete mode 100644 network/src/main/java/org/zstack/network/l3/zstack ipam.md diff --git a/docs/modules/network/nav.adoc b/docs/modules/network/nav.adoc index 53e42d44f22..19dbb6c06b1 100644 --- a/docs/modules/network/nav.adoc +++ b/docs/modules/network/nav.adoc @@ -2,5 +2,6 @@ ** xref:networkResource/networkResource.adoc[] *** xref:networkResource/L2Network.adoc[] *** xref:networkResource/L3Network.adoc[] + *** xref:networkResource/l3Ipam.adoc[] *** xref:networkResource/VpcRouter.adoc[] ** xref:networkService/networkService.adoc[] \ No newline at end of file diff --git a/docs/modules/network/pages/networkResource/l3Ipam.adoc b/docs/modules/network/pages/networkResource/l3Ipam.adoc new file mode 100644 index 00000000000..ce3fcda8335 --- /dev/null +++ b/docs/modules/network/pages/networkResource/l3Ipam.adoc @@ -0,0 +1,202 @@ += 三层网络IPAM + +ZStack IPAM 负责管理三层网络(L3)的 IP 地址分配与回收,支持以下三种方式: + +. 自动分配:ZStack 云平台根据 L3 网络配置的 IP range 自动分配。 +. 手动分配:用户在创建云主机时手动指定 IP 地址。 +. QGA 获取:通过 QEMU Guest Agent(QGA)从云主机内部读取并上报 IP 地址。 + +== 自动分配 + +自动分配需要满足以下两个条件: + +. L3 网络已配置 IP range。 +. L3 网络已启用 DHCP 服务。 ++ +这是历史遗留行为:扁平网络使用 DHCP 服务作为自动分配功能的开关,其他网络类型不受影响。 + +系统根据用户输入的 L3 网络 UUID 以及可选的 IP 地址,按照分配算法分配一个可用地址。返回的地址信息包括:IP 地址、掩码(或前缀长度)、网关。 + +=== 自动分配算法 + +* 随机分配:从可用地址池中随机选择一个 IP 地址。 +* 顺序分配:从可用地址池中按顺序选择一个 IP 地址。 +* 循环分配:从可用地址池中按顺序分配,分配到最后一个 IP 后回到第一个 IP 继续分配。 + +=== 当前状况 + +Cloud 5.5 版本中: + +. 扁平网络有三种情况:无 IP range、有 IP range 且未启用 DHCP、有 IP range 且已启用 DHCP。 +. 公有网络与 VPC 网络有两种情况:有 IP range 且未启用 DHCP、有 IP range 且已启用 DHCP。 +. 管理网络与流量网络仅有一种情况:有 IP range 且未启用 DHCP。 + +=== 工作时机 + +以下操作会触发自动分配: + +. 创建云主机(`APICreateVmInstanceMsg`) +. 云主机添加网卡(`APIAttachL3NetworkToVmMsg`, `APICreateVmNicMsg`) +. 修改云主机 IP(`APISetVmStaticIpMsg`, `APIChangeVmNicNetworkMsg`) +. 创建 appliance VM(`APICreateVpcVRouterMsg`, `APICreateSlbInstanceMsg`, `APICreateNfvInstMsg`) +. 创建 VIP(`APICreateVipMsg`) + +== 手动分配 + +手动指定仅对云主机生效,对 appliance VM 不生效。 + +在前述场景 1、2、3 中,用户可以指定 IP 地址,分为两种情况: + +. 指定 IP 在 IP range 之内:后端仍执行自动分配流程。 +. 指定 IP 不在 IP range 之内:按手动分配流程处理。 +.. 如果指定 IP 不在 L3 CIDR 内,必须指定掩码,网关可选。 +.. 如果指定 IP 在 L3 CIDR 内,可不指定掩码和网关;如指定,必须与 L3 CIDR 一致。 + +=== 工作时机 + +. 5.5.12 之前:扁平网络在“无 IP range”或“有 IP range 但未启用 DHCP”两种情况下,允许指定不在 IP range 内的地址。 +. 5.5.12 及以后:任意网络都可通过修改云主机 IP(`APISetVmStaticIpMsg`, `APIChangeVmNicNetworkMsg`)设置不在 IP range 内的地址。 + +如果指定 IP 在 IP range 之外、但在 L3 CIDR 之内,则掩码和网关可不指定,系统会自动从 L3 CIDR 推导。 + +如果指定 IP 在 L3 CIDR 之外,用户必须同时提供 IP 与掩码(或前缀长度);若为默认网卡,还必须指定网关,且网关必须位于 L3 CIDR 内。 + +== QGA 获取 + +该方式需要开启全局配置 `VmGlobalConfig.ENABLE_VM_INTERNAL_IP_OVERWRITE`(默认值为 `false`)。 + +ZStack KVM Agent 会定期通过 QGA 从云主机内部读取 IP 地址。仅在扁平网络且无 IP range 时,会将读取到的 IP 分配给云主机;其他网络类型不受影响。 + +在其他情况下,如果 QGA 读取到的 IP 与云主机当前 IP 冲突,系统会产生告警。 + +== 配置虚拟机 Guest OS 的 IP 地址 + +配置虚拟机 Guest OS 的 IP 地址有三种方式: + +. DHCP:通过 DHCP 服务器动态分配 IP 地址。 +. Cloud-init:在云主机创建时通过 Cloud-init 预配置 IP 地址。 +. QGA:通过 QEMU Guest Agent 配置云主机内部网络参数。 + +=== DHCP + +ZStack 会在每个物理机启动分布式 DHCP Server。云主机启动后通过 `dhclient` 获取 IP 地址、DNS 等参数。 + +=== Cloud-init + +ZStack 会在每个物理机启动分布式 Userdata Server。云主机启动后通过 Cloud-init 获取 IP 地址、DNS 等参数。 + +=== QGA + +云主机安装 ZStack Guest Agent 后,系统在检测到 Guest Agent 首次启动时,会通过 QGA 配置云主机的 IP、DNS 等参数。 + +当用户在 UI 手动修改 IP(`APISetVmStaticIpMsg`, `APIChangeVmNicNetworkMsg`)后,后端 API 会触发一次 Guest OS 网络参数下发流程。 + +QGA 下发的参数包括: + +* IP 地址 +* 掩码或前缀长度 +* 网关 +* DNS 服务器地址 +* MTU +* Hostname + +用户可通过全局配置 `GuestToolsGlobalProperty.GUESTTOOLS_VM_PORT_CONFIGFIELDS` 限制下发字段。 + +== 网络服务 + +当网卡地址不在 L3 IP range 内时,可分为“在 L3 CIDR 内”和“在 L3 CIDR 外”两种情况。 + +=== 在 L3 CIDR 内 + +该情况与“IP 在 L3 IP range 内”一致,网络服务不受影响。 + +=== 在 L3 CIDR 外 + +==== 安全组 + +安全组规则本身不依赖网卡 IP 是否位于 L3 CIDR 内,但需要用户谨慎设计规则,避免不符合预期。 + +==== DHCP + +如果网卡所属网络无 IP range,则不提供 DHCP 服务。 + +如果网卡所属网络有 IP range,ZStack 会启动 DHCP 服务,且 `dnsmasq` 配置要求指定一个 IP CIDR。 + +当网卡 IP 不在 DHCP 服务对应的 IP CIDR 内时,DHCP 模块下发配置时会过滤掉 CIDR 外地址。 + +==== EIP + +对于扁平网络,EIP 功能不受影响,可继续创建。 + +对于 VPC 网络,若 EIP 绑定的私网地址不在 L3 CIDR 内,VPC 路由器无法路由,网络不通。 + +为保证一致性,EIP 不能绑定 IP 不在 L3 CIDR 内的网卡;`APIGetEipAttachableVmNicsMsg` 也不会返回该类网卡。 + +==== Port Forwarding + +Port Forwarding 仅用于 VPC 网络。与 EIP 类似,若网卡 IP 不在 L3 CIDR 内,VPC 路由器无法路由,网络不通。 + +Port Forwarding 不能绑定 IP 不在 L3 CIDR 内的网卡;`APIGetPortForwardingAttachableVmNicsMsg` 也不会返回该类网卡。 + +==== Load Balancer + +与 EIP 类似,若网卡 IP 不在 L3 CIDR 内,网络不通。 + +`APIAddVmNicToLoadBalancerMsg`、`APIAddBackendServerToServerGroupMsg` 不能绑定 IP 不在 L3 CIDR 内的网卡。 + +`APIGetCandidateVmNicsForLoadBalancerServerGroupMsg`、`APIGetCandidateVmNicsForLoadBalancerMsg` 也不会返回该类网卡。 + +== 代码细节 + +=== APISetVmStaticIpMsg + +该 API 通过成员字段配置云主机 IP、掩码、网关等参数,并进行完整性校验: + +* 用户输入掩码与网关:以用户输入为准。 +* 用户仅输入网关:若 IP 与网关均在 L3 CIDR 内,使用 L3 CIDR 掩码;否则报错。 +* 用户仅输入掩码:若与 L3 CIDR 掩码一致,使用 L3 CIDR 网关;否则若网卡为默认网卡或唯一网卡,报错;否则使用输入掩码,网关置空。 +* 用户未输入掩码和网关,且 IP 在 L3 CIDR 内:使用 L3 CIDR 的掩码与网关。 +* 用户未输入掩码和网关,且 IP 在 L3 CIDR 外:报错。 +* IPv6 与 IPv4 的逻辑一致。 +* 对 DNS 参数:若 UI 传递 `NULL`,后端保持旧 DNS 参数不变。 +* 对 DNS 参数:若 UI 传递 `""` 或 `[]`,后端删除旧 DNS。 +* 其他情况仅允许传递合法 DNS 列表,后端会先删除旧 DNS,再配置新 DNS。 + +=== APIChangeVmNicNetworkMsg + +该 API 通过 System Tags 配置云主机 IP、掩码、网关等参数,并进行完整性校验: + +* 用户输入掩码与网关:以用户输入为准。 +* 用户仅输入网关:若 IP 与网关均在 L3 CIDR 内,使用 L3 CIDR 掩码;否则报错。 +* 用户仅输入掩码:若与 L3 CIDR 掩码一致,使用 L3 CIDR 网关;否则若网卡为默认网卡或唯一网卡,报错;否则使用输入掩码,网关置空。 +* 用户未输入掩码和网关:若网卡存在相同 IP 版本地址,且输入 IP 在旧 IP 的掩码与网关组成 CIDR 内,则复用旧掩码与网关;否则若 IP 在 L3 CIDR 内,使用 L3 CIDR 掩码与网关;否则报错。 +* IPv6 与 IPv4 的逻辑一致。 +* 对 DNS 参数:若 UI 传递 `NULL`,后端保持旧 DNS 参数不变。 +* 对 DNS 参数:若 UI 传递 `""` 或 `[]`,后端删除旧 DNS。 +* 其他情况仅允许传递合法 DNS 列表,后端会先删除旧 DNS,再配置新 DNS。 + +=== APICreateVmInstanceMsg + +逻辑与 `APIChangeVmNicNetworkMsg` 相同。 + +=== APIGetL3NetworkIpStatisticMsg + +不统计位于 IP range 之外的 IP 地址。 + +=== APIAddIpRangeMsg + +系统允许给云主机设置不在 IP range 内的 IP,因此添加 IP range 时可能覆盖已分配 IP。此时会将这些已分配地址归属到新加入的 IP range。 + +=== APIAddReservedIpRangeMsg + +该 API 不仅会添加 `ReservedIpRangeVO`,还会将 `ReservedIpRangeVO` 与 IP range 重叠的地址写入 `UsedIpVO`。 + +[source,java] +---- +vo.setUsedFor(IpAllocatedReason.Reserved.toString()); +---- + +=== APICheckIpAvailabilityMsg + +在 5.5.12 之前,该 API 在扁平网络未启用 DHCP 的情况下会跳过 IP range 检查;该行为保持不变。 + diff --git a/docs/modules/network/pages/networkResource/networkResource.adoc b/docs/modules/network/pages/networkResource/networkResource.adoc index 9aa66ce7341..3b567e4eb66 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/l3Ipam.adoc[] * xref:networkResource/VpcRouter.adoc[] \ No newline at end of file diff --git a/network/src/main/java/org/zstack/network/l3/zstack ipam.md b/network/src/main/java/org/zstack/network/l3/zstack ipam.md deleted file mode 100644 index 63951964dbd..00000000000 --- a/network/src/main/java/org/zstack/network/l3/zstack ipam.md +++ /dev/null @@ -1,139 +0,0 @@ -# ZStack IPAM - -ZStack IPAM 负责管理 L3 网络的 IP 地址分配和回收。它提供三种方式: -1. **自动分配**: ZStack云平台根据L3配置的ip range自动分配。 -2. **手动分配**: 用户可以在创建虚拟机时指定IP地址。 -3. **qga获取**: 通过DHCP服务器动态分配IP地址。 - -## 自动分配 - -自动分配需要满足两个条件: -1. L3网络必须配置ip range。 -2. L3网络必须enable dhcp服务。这是个历史遗留问题: 扁平网络使用dhcp服务标识是否启用自动分配功能, 其它网络类型不受影响 -它根据用户输入的l3网络uuid和可选的ip地址, 按照地址分配算法分配一个可用地址,分配的IP地址包含: ip地址,掩码(或者前缀长度),网关 - -### 自动分配算法 -- 随机分配: 从可用ip地址池中随机选择一个ip地址分配给虚拟机 -- 顺序分配: 从可用ip地址池中按照顺序选择一个ip -- 循环分配: 从可用ip地址池中按照顺序选择一个ip, 分配完最后一个ip后, 从第一个ip重新开始分配 - -### 当前状况 -cloud 5.5版本情况: -1. 扁平网络可以有三种情况: no ip range, ip range without dhcp, ip range with dhcp. -2. 公有网络和VPC网络有两种情况: ip range without dhcp, ip range with dhcp. -3. 管理网和流量网络只有一种情况: ip range without dhcp. - -### 工作时机 -以下操作会触发自动分配: -1. 创建虚拟机(APICreateVmInstanceMsg) -2. 虚拟机添加网卡(APIAttachL3NetworkToVmMsg, APICreateVmNicMsg) -3. 修改虚拟机IP(APISetVmStaticIpMsg, APIChangeVmNicNetworkMsg) -4. 创建applianceVm(APICreateVpcVRouterMsg, APICreateSlbInstanceMsg, APICreateNfvInstMsg) -5. 创建Vip(APICreateVipMsg) - -## 手动分配 -手动指定仅仅对虚拟机生效,对于applianceVm不生效。 -在前述场景1,2,3的情况下,用户可以指定ip地址. 这又分两种情况: -1. 指定的ip在ip range之内,后端仍然执行的自动分配流量 -2. 指定的ip不在ip range之内, 按照手动指定流程分配 - 1. 如果指定的ip地址不在l3 cidr之内,必须指定掩码, 网关可选 - 2. 如果指定的ip地址在l3 cidr之内,可以不指定掩码, 网关, 如果指定必须和l3 cidr一致 - -### 工作时机 -1. 在5.5.12之前, 扁平网络在两种情况下: no ip range, ip range without dhcp, 允许指定地址不在ip range之内 -2. 在5.5.12版本及其以后, 任意网络,都可以通过修改虚拟机IP(APISetVmStaticIpMsg, APIChangeVmNicNetworkMsg) 设置不在ip range之内的地址, - -如果指定的ip地址在ip ranges之外, 但是在l3 cidr之内, 则掩码和网关可以不指定, 系统会自动从l3 cidr中获取掩码和网关 -如果指定的ip地址在ip cidr之外, 用户输入必须同时输入IP, 掩码或者前缀长度, 如果是默认网卡,必须指定网关, 网关必须在l3 cidr之内 - - -## qga获取 -这种方式需要打开全局配置: VmGlobalConfig.ENABLE_VM_INTERNAL_IP_OVERWRITE(默认值是false) -ZStack kvmagent会定期通过qga从云主机内部读取ip地址, 仅在扁平网络在no ip range的情况下会把读出来的ip地址分配给云主机, 其它网络类型不受影响。 -其它情况下,qga获取的ip地址如果和虚拟机的ip地址冲突, 则发送报警。 - -## 配置虚拟机guest OS的IP地址 -配置虚拟机guest OS的IP地址有3种方式: -1. **DHCP**: 通过DHCP服务器动态分配IP地址。 -2. **Cloud-init**: 在虚拟机创建时,使用Cloud-init工具预配置IP地址。 -3. **QGA**: 通过QEMU Guest Agent从虚拟 - -### DHCP -ZStack会在每个物理机启动分布式dhcp server, 虚拟机启动时候, 通过dhclient获取地址和dns等参数。 - -### Cloud-init -ZStack会在每个物理机启动分布式userdata server, 虚拟机启动时候, 通过cloud-init获取地址和dns等参数。 - -### QGA -当虚拟机安装ZStack Guest Agent后,在zstack检测guest agent第一次启动时候,通过qga配置虚拟机的ip地址,dns等参数 -当用户在UI手动修改IP(APISetVmStaticIpMsg, APIChangeVmNicNetworkMsg), UI调用后端api, 触发一次配置虚拟机ip地址的过程 -qga配置虚拟机的参数包含: -- IP地址 -- 掩码或者前缀长度 -- 网关 -- DNS服务器地址 -- mtu -- hostname -用户可以通过全局配置来限制配置的字段: GuestToolsGlobalProperty.GUESTTOOLS_VM_PORT_CONFIGFIELDS来限制 - -## 网络服务 -当网卡地址不在 l3 ip range之内的时候, 又可以分为在l3 cidr之内和在l3 cidr之外两种情况: - -### 在l3 cidr之内 -这种情况和在l3 ip range之内的情况一样, 网络服务没有影响 - -### 在l3 cidr之外 -#### 安全组 -1. 安全组的规则不关心网卡ip, 当这种网卡配置了安全组以后, 需要用户小心规则的配置,否则可能满足不了需求 - -#### DHCP -如果网卡没有ip range, 则没有dhcp服务 -如果网卡有ip range, zstack会启动dhcp服务, dnsmasq的配置文件要求指定一个ip cidr -如果网卡的ip地址不在dhcp服务的ip cidr之内, 因此dhcp模块下发配置的时候调多cidr之外的地址 - -#### Eip -对于扁平网络, eip功能不受影响,可以继续创建。 -对于vpc网络, eip的私网地址不在l3 cidr之内, vpc路由器无法路由,网络不通 -为了一致性,eip不能绑定ip地址不在l3 cidr之内的网卡, APIGetEipAttachableVmNicsMsg 也不返回ip地址不在l3 cidr之内的网卡 - -#### Port forwarding -只有vpc网络才有port forwarding功能, 和eip一样, vpc路由器无法路由,网络不通 -Port forwarding不能绑定ip地址不在l3 cidr之内的网卡, APIGetPortForwardingAttachableVmNicsMsg 也不返回ip地址不在l3 cidr之内的网卡 - -#### LoadBalancer -和eip一样,网络不通 -APIAddVmNicToLoadBalancerMsg, APIAddBackendServerToServerGroupMsg 不能绑定ip地址不在l3 cidr之内的网卡, -APIGetCandidateVmNicsForLoadBalancerServerGroupMsg, APIGetCandidateVmNicsForLoadBalancerMsg也不返回ip地址不在l3 cidr之内的网卡 - - -## 代码细节 - -### APISetVmStaticIpMsg -通过成员字段配置虚拟机的IP地址,掩码,网关等参数。需要完整性校验 -- 如果用户输入IP地址,不输入掩码和网关,优先使用网卡上使用的掩码和网关; -- 继续,如果网卡上没有使用的掩码和网关, 则从l3 cidr中获取掩码和网关 -- 继续,如果l3 cidr中也没有掩码和网关,报错 -- ipv6和ipv4的逻辑一样 -- -### APIChangeVmNicNetworkMsg -通过system tags来配置虚拟机的IP地址,掩码,网关等参数。需要完整性校验 -- 如果配置ip地址,且在l3 cidr之内,掩码和网关从l3 cidr中获取 -- 如果配置ip地址,且在l3 cidr之外,必须指定掩码, 网关可选; 如果是默认网卡,必须指定网关 -- 如果配置ipv6地址,且在l3 cidr之内,前缀长度和网关从l3 cidr中获取 -- 如果配置ipv6地址,且在l3 cidr之外,必须指定前缀长度, 网关可选; 如果是默认网卡,必须指定网关 - -### APICreateVmInstanceMsg -逻辑和APIChangeVmNicNetworkMsg相同 - -### APIGetL3NetworkIpStatisticMsg -不统计在ip range之外的ip地址 - -### APIAddIpRangeMsg -允许给云主机设置不在ip range之内的ip地址, 这样在添加ip range的时候, 可能包含已经分配的ip地址, 此时, 让已分配的地址属于新加入的ip range - -### APIAddReservedIpRangeMsg -这个api不仅添加了ReservedIpRangeVO, 还把ReservedIpRangeVO和ip range重叠的ip添加到UsedIpVO, -vo.setUsedFor(IpAllocatedReason.Reserved.toString()); - -### APICheckIpAvailabilityMsg -这个api在5.5.12版本之前, 在扁平网络no dhcp的情况下跳过ip range的检查, 这个功能不变 From f32fb964eab9a349a52f76e1b0ae15e62f59d9f9 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 17 Mar 2026 18:09:14 +0800 Subject: [PATCH 38/77] =?UTF-8?q?[errorcode]:=20address=20review=20?= =?UTF-8?q?=E2=80=94=20null-safe=20message=20fallback=20and=20avoid=20load?= =?UTF-8?q?er=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GlobalErrorCodeI18nServiceImpl: add final fallback to error.code/empty string when both details and description are null, guaranteeing message is never null - Platform: use existing loader field instead of getComponentLoader() to avoid triggering ComponentLoader creation during early startup; move message fallback outside catch block so it always runs Co-Authored-By: ye.zou Change-Id: I8571f05657dc2173cc232b511f505eedc68d714e --- .../src/main/java/org/zstack/core/Platform.java | 17 ++++++++++------- .../GlobalErrorCodeI18nServiceImpl.java | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index dc7f6697150..78b184d12e7 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -992,15 +992,18 @@ public static ErrorCode err(String globalErrorCode, Enum errCode, ErrorCode caus // populate message at creation time with default locale; // RestServer will override with client's Accept-Language if different try { - GlobalErrorCodeI18nService i18nService = getComponentLoader().getComponent(GlobalErrorCodeI18nService.class); - if (i18nService != null) { - i18nService.localizeErrorCode(result, org.zstack.core.errorcode.LocaleUtils.DEFAULT_LOCALE); + ComponentLoader currentLoader = loader; + if (currentLoader != null) { + GlobalErrorCodeI18nService i18nService = currentLoader.getComponent(GlobalErrorCodeI18nService.class); + if (i18nService != null) { + i18nService.localizeErrorCode(result, org.zstack.core.errorcode.LocaleUtils.DEFAULT_LOCALE); + } } } catch (Exception e) { - // i18n service not initialized during early startup, use details as fallback - if (result.getMessage() == null) { - result.setMessage(details); - } + // i18n service not initialized during early startup + } + if (result.getMessage() == null) { + result.setMessage(details != null ? details : result.getDescription()); } return result; diff --git a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java index 00b05e4330c..b6820fc2d3b 100644 --- a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java +++ b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java @@ -140,7 +140,8 @@ public void localizeErrorCode(ErrorCode error, String locale) { // guarantee: message is never null if (error.getMessage() == null) { - error.setMessage(error.getDetails() != null ? error.getDetails() : error.getDescription()); + String fallback = error.getDetails() != null ? error.getDetails() : error.getDescription(); + error.setMessage(fallback != null ? fallback : (error.getCode() != null ? error.getCode() : "")); } if (error.getCause() != null) { From 3feb9e903e7a617aab0f30e3bbb2a9840a795646 Mon Sep 17 00:00:00 2001 From: "yingzhe.hu" Date: Wed, 11 Mar 2026 10:55:36 +0800 Subject: [PATCH 39/77] [kvm]: add libvirt TLS config Add libvirt.tls.enabled GlobalConfig and useTls field in MigrateVmCmd to support TLS-encrypted libvirt connections for migration and V2V. Resolves: ZSTAC-81343 Change-Id: I391fa36c0dd63c25c5d85d102bc3579c8eb3d685 --- .../java/org/zstack/kvm/KVMAgentCommands.java | 20 ++ .../java/org/zstack/kvm/KVMGlobalConfig.java | 4 + .../src/main/java/org/zstack/kvm/KVMHost.java | 13 + .../zstack/kvm/KVMHostDeployArguments.java | 10 + .../java/org/zstack/kvm/KVMHostFactory.java | 57 ++++ .../vm/migrate/LibvirtTlsMigrateCase.groovy | 267 ++++++++++++++++++ 6 files changed, 371 insertions(+) create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy 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..2a36bb5aba3 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -3805,6 +3805,26 @@ public static class MigrateVmCmd extends AgentCommand implements HasThreadContex private boolean reload; @GrayVersion(value = "5.0.0") private long bandwidth; + @GrayVersion(value = "5.5.12") + private boolean useTls; + @GrayVersion(value = "5.5.12") + private String srcHostManagementIp; + + public String getSrcHostManagementIp() { + return srcHostManagementIp; + } + + public void setSrcHostManagementIp(String srcHostManagementIp) { + this.srcHostManagementIp = srcHostManagementIp; + } + + public boolean isUseTls() { + return useTls; + } + + public void setUseTls(boolean useTls) { + this.useTls = useTls; + } public Integer getDownTime() { return downTime; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java index 8cdd2f54167..ee765aad40f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -139,6 +139,10 @@ public class KVMGlobalConfig { @BindResourceConfig({HostVO.class, ClusterVO.class}) public static GlobalConfig RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE = new GlobalConfig(CATEGORY, "reconnect.host.restart.libvirtd.service"); + @GlobalConfigValidation(validValues = {"true", "false"}) + @GlobalConfigDef(defaultValue = "true", type = Boolean.class, description = "enable TLS encryption for libvirt remote connections (migration)") + public static GlobalConfig LIBVIRT_TLS_ENABLED = new GlobalConfig(CATEGORY, "libvirt.tls.enabled"); + @GlobalConfigValidation public static GlobalConfig KVMAGENT_PHYSICAL_MEMORY_USAGE_ALARM_THRESHOLD = new GlobalConfig(CATEGORY, "kvmagent.physicalmemory.usage.alarm.threshold"); 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..420f00aa2fd 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -3163,6 +3163,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setDestHostIp(dstHostMigrateIp); cmd.setSrcHostIp(srcHostMigrateIp); cmd.setDestHostManagementIp(dstHostMnIp); + cmd.setSrcHostManagementIp(srcHostMnIp); cmd.setMigrateFromDestination(migrateFromDestination); cmd.setStorageMigrationPolicy(storageMigrationPolicy == null ? null : storageMigrationPolicy.toString()); cmd.setVmUuid(vmUuid); @@ -3174,6 +3175,8 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setDownTime(s.downTime); cmd.setBandwidth(s.bandwidth); cmd.setNics(nicTos); + cmd.setUseTls(KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) + && rcf.getResourceConfigValue(KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE, self.getUuid(), Boolean.class)); if (s.diskMigrationMap != null) { Map diskMigrationMap = new HashMap<>(); @@ -5815,6 +5818,16 @@ public void run(final FlowTrigger trigger, Map data) { deployArguments.setSkipPackages(info.getSkipPackages()); deployArguments.setUpdatePackages(String.valueOf(CoreGlobalProperty.UPDATE_PKG_WHEN_CONNECT)); + // Build TLS cert IP list: management IP + extra IPs (migration network etc.) + String managementIp = getSelf().getManagementIp(); + String extraIps = HostSystemTags.EXTRA_IPS.getTokenByResourceUuid( + self.getUuid(), HostSystemTags.EXTRA_IPS_TOKEN); + if (extraIps != null && !extraIps.isEmpty()) { + deployArguments.setTlsCertIps(managementIp + "," + extraIps); + } else { + deployArguments.setTlsCertIps(managementIp); + } + if (deployArguments.isForceRun()) { runner.setForceRun(true); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java index 71fb8a9769e..f2bb79c110b 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java @@ -39,6 +39,8 @@ public class KVMHostDeployArguments extends SyncTimeRequestedDeployArguments { private String restartLibvirtd; @SerializedName("extra_packages") private String extraPackages; + @SerializedName("tls_cert_ips") + private String tlsCertIps; private transient boolean forceRun = false; @@ -135,6 +137,14 @@ public void setExtraPackages(String extraPackages) { this.extraPackages = extraPackages; } + public String getTlsCertIps() { + return tlsCertIps; + } + + public void setTlsCertIps(String tlsCertIps) { + this.tlsCertIps = tlsCertIps; + } + public String getEnableSpiceTls() { return enableSpiceTls; } 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..0188c920a83 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java @@ -1,5 +1,6 @@ package org.zstack.kvm; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.util.UriComponentsBuilder; @@ -9,6 +10,8 @@ import org.zstack.compute.vm.VmNicManager; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.ansible.AnsibleFacade; +import org.zstack.core.jsonlabel.JsonLabel; +import org.zstack.core.jsonlabel.JsonLabelInventory; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusListCallBack; import org.zstack.core.cloudbus.CloudBusSteppingCallback; @@ -75,6 +78,7 @@ import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.CollectionUtils; import org.zstack.utils.IpRangeSet; +import org.zstack.utils.ShellUtils; import org.zstack.utils.SizeUtils; import org.zstack.utils.Utils; import org.zstack.utils.data.SizeUnit; @@ -85,6 +89,7 @@ import org.zstack.utils.logging.CLogger; import javax.persistence.Tuple; +import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -122,6 +127,10 @@ public class KVMHostFactory extends AbstractService implements HypervisorFactory HypervisorMessageFactory { private static final CLogger logger = Utils.getLogger(KVMHostFactory.class); + private static final String LIBVIRT_TLS_CA_KEY = "libvirtTLSCA"; + private static final String LIBVIRT_TLS_PRIVATE_KEY = "libvirtTLSPrivateKey"; + private static final String CA_DIR = "/var/lib/zstack/pki/CA"; + public static final HypervisorType hypervisorType = new HypervisorType(KVMConstant.KVM_HYPERVISOR_TYPE); public static final VolumeFormat QCOW2_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_QCOW2, hypervisorType); public static final VolumeFormat RAW_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_RAW, hypervisorType); @@ -458,8 +467,56 @@ private void processKvmagentPhysicalMemUsageAbnormal(HostProcessPhysicalMemoryUs bus.send(restartKvmAgentMsg); } + private void initLibvirtTlsCA() { + if (CoreGlobalProperty.UNIT_TEST_ON) { + return; + } + + try { + ShellUtils.run(String.format("mkdir -p %s", CA_DIR)); + ShellUtils.run("chown -R zstack:zstack /var/lib/zstack/pki"); + + File caFile = new File(CA_DIR + "/cacert.pem"); + File keyFile = new File(CA_DIR + "/cakey.pem"); + + // Local CA missing — generate with openssl + // NOTE: ShellUtils.run() prepends sudo only to the first command in &&-chains, + // so each command must be a separate call. + if (!caFile.exists() || !keyFile.exists()) { + ShellUtils.run(String.format( + "openssl genrsa -out %s/cakey.pem 4096", CA_DIR)); + ShellUtils.run(String.format( + "openssl req -new -x509 -days 3650 -key %s/cakey.pem " + + "-out %s/cacert.pem -subj '/O=ZStack/CN=ZStack Libvirt CA'", + CA_DIR, CA_DIR)); + ShellUtils.run(String.format("chown zstack:zstack %s/cakey.pem %s/cacert.pem", + CA_DIR, CA_DIR)); + ShellUtils.run(String.format("chmod 600 %s/cakey.pem", CA_DIR)); + ShellUtils.run(String.format("chmod 644 %s/cacert.pem", CA_DIR)); + } + + String ca = FileUtils.readFileToString(caFile).trim(); + String key = FileUtils.readFileToString(keyFile).trim(); + + // createIfAbsent: DB has no record → write; DB has record → return DB value + JsonLabelInventory caInv = new JsonLabel().createIfAbsent(LIBVIRT_TLS_CA_KEY, ca); + JsonLabelInventory keyInv = new JsonLabel().createIfAbsent(LIBVIRT_TLS_PRIVATE_KEY, key); + + // Use DB as source of truth — overwrite local files (HA: MN2 uses MN1's CA from DB) + FileUtils.writeStringToFile(caFile, caInv.getLabelValue()); + FileUtils.writeStringToFile(keyFile, keyInv.getLabelValue()); + ShellUtils.run(String.format("chmod 600 %s/cakey.pem", CA_DIR)); + ShellUtils.run(String.format("chmod 644 %s/cacert.pem", CA_DIR)); + + logger.info("Libvirt TLS CA initialized and persisted to database"); + } catch (Exception e) { + logger.warn("Failed to initialize libvirt TLS CA", e); + } + } + @Override public boolean start() { + initLibvirtTlsCA(); deployAnsibleModule(); populateExtensions(); configKVMDeviceType(); diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy new file mode 100644 index 00000000000..d4a30cef78e --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy @@ -0,0 +1,267 @@ +package org.zstack.test.integration.kvm.vm.migrate + +import org.springframework.http.HttpEntity +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.kvm.KVMGlobalConfig +import org.zstack.sdk.HostInventory +import org.zstack.sdk.UpdateGlobalConfigAction +import org.zstack.sdk.VmInstanceInventory +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.testlib.Test +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +/** + * Verify that the libvirt TLS configuration (ZSTAC-81343) is correctly + * propagated in the MigrateVmCmd sent to kvmagent. + * + * Key logic under test (KVMHost.java): + * cmd.setUseTls(LIBVIRT_TLS_ENABLED && RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE) + * cmd.setSrcHostManagementIp(srcHostMnIp) + */ +class LibvirtTlsMigrateCase extends SubCase { + EnvSpec env + + @Override + void clean() { + env.delete() + } + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(8) + cpu = 4 + } + + zone { + name = "zone" + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm1" + managementIp = "127.0.0.1" + username = "root" + password = "password" + usedMem = 1000 + totalCpu = 10 + } + kvm { + name = "kvm2" + managementIp = "127.0.0.2" + username = "root" + password = "password" + usedMem = 1000 + totalCpu = 10 + } + + attachPrimaryStorage("ps") + attachL2Network("l2") + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.100" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + + cephPrimaryStorage { + name = "ps" + totalCapacity = SizeUnit.GIGABYTE.toByte(100) + availableCapacity = SizeUnit.GIGABYTE.toByte(100) + url = "ceph://pri" + fsid = "7ff218d9-f525-435f-8a40-3618d1772a64" + monUrls = ["root:password@localhost/?monPort=7777"] + } + + attachBackupStorage("bs") + } + + cephBackupStorage { + name = "bs" + totalCapacity = SizeUnit.GIGABYTE.toByte(100) + availableCapacity = SizeUnit.GIGABYTE.toByte(100) + url = "/bk" + fsid = "7ff218d9-f525-435f-8a40-3618d1772a64" + monUrls = ["root:password@localhost/?monPort=7777"] + + image { + name = "image" + url = "http://zstack.org/download/image.qcow2" + } + } + + vm { + name = "vm" + useCluster("cluster") + useHost("kvm1") + useL3Networks("l3") + useInstanceOffering("instanceOffering") + useImage("image") + } + } + } + + @Override + void test() { + env.create { + testMigrateWithTlsEnabled() + testMigrateWithTlsDisabled() + testMigrateWithRestartLibvirtdDisabled() + testGlobalConfigValidation() + } + } + + /** + * Case 1: Both LIBVIRT_TLS_ENABLED=true and RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE=true + * => useTls should be true, srcHostManagementIp should be set + */ + void testMigrateWithTlsEnabled() { + def vm = env.inventoryByName("vm") as VmInstanceInventory + def host1 = env.inventoryByName("kvm1") as HostInventory + def host2 = env.inventoryByName("kvm2") as HostInventory + + // Ensure TLS is enabled (default is true) + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("true") + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("true") + + KVMAgentCommands.MigrateVmCmd cmd = null + env.afterSimulator(KVMConstant.KVM_MIGRATE_VM_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MigrateVmCmd.class) + return rsp + } + + // Migrate vm from kvm1 to kvm2 + migrateVm { + vmInstanceUuid = vm.uuid + hostUuid = host2.uuid + } + + assert cmd != null : "MigrateVmCmd should have been captured" + assert cmd.useTls : "useTls should be true when both TLS and restartLibvirtd are enabled" + assert cmd.srcHostManagementIp == host1.managementIp : + "srcHostManagementIp should be source host management IP" + assert cmd.destHostManagementIp == host2.managementIp : + "destHostManagementIp should be dest host management IP" + } + + /** + * Case 2: LIBVIRT_TLS_ENABLED=false => useTls should be false regardless of restartLibvirtd + */ + void testMigrateWithTlsDisabled() { + def vm = env.inventoryByName("vm") as VmInstanceInventory + def host1 = env.inventoryByName("kvm1") as HostInventory + + // Disable TLS + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("false") + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("true") + + KVMAgentCommands.MigrateVmCmd cmd = null + env.afterSimulator(KVMConstant.KVM_MIGRATE_VM_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MigrateVmCmd.class) + return rsp + } + + // Migrate back to kvm1 + migrateVm { + vmInstanceUuid = vm.uuid + hostUuid = host1.uuid + } + + assert cmd != null : "MigrateVmCmd should have been captured" + assert !cmd.useTls : "useTls should be false when TLS config is disabled" + + // Restore default + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("true") + } + + /** + * Case 3: LIBVIRT_TLS_ENABLED=true but RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE=false + * => useTls should be false (AND logic: both must be true) + * + * This is a critical boundary: TLS config is on, but libvirtd was not restarted + * with TLS certs deployed, so we must NOT tell kvmagent to use TLS. + */ + void testMigrateWithRestartLibvirtdDisabled() { + def vm = env.inventoryByName("vm") as VmInstanceInventory + def host2 = env.inventoryByName("kvm2") as HostInventory + + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("true") + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("false") + + KVMAgentCommands.MigrateVmCmd cmd = null + env.afterSimulator(KVMConstant.KVM_MIGRATE_VM_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MigrateVmCmd.class) + return rsp + } + + migrateVm { + vmInstanceUuid = vm.uuid + hostUuid = host2.uuid + } + + assert cmd != null : "MigrateVmCmd should have been captured" + assert !cmd.useTls : + "useTls should be false when restartLibvirtd is disabled (TLS certs not deployed)" + + // Restore default + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("true") + } + + /** + * Case 4: Validate that libvirt.tls.enabled GlobalConfig only accepts true/false + */ + void testGlobalConfigValidation() { + // Valid values via SDK action + updateGlobalConfig { + category = "kvm" + name = "libvirt.tls.enabled" + value = "true" + } + assert KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) == true + + updateGlobalConfig { + category = "kvm" + name = "libvirt.tls.enabled" + value = "false" + } + assert KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) == false + + // Invalid value should be rejected + def action = new UpdateGlobalConfigAction() + action.category = "kvm" + action.name = "libvirt.tls.enabled" + action.value = "invalid" + action.sessionId = Test.currentEnvSpec.session.uuid + UpdateGlobalConfigAction.Result res = action.call() + assert res.error != null : "Setting an invalid value for libvirt.tls.enabled should fail" + + // Restore default + updateGlobalConfig { + category = "kvm" + name = "libvirt.tls.enabled" + value = "true" + } + } +} From 7b6ceead7ac67eaba829989d20fe796d857d5f4a Mon Sep 17 00:00:00 2001 From: J M Date: Thu, 19 Mar 2026 13:23:52 +0800 Subject: [PATCH 40/77] [core,kvm]: fix SSH session leak in CallBackNetworkChecker and KVMHost 1. CallBackNetworkChecker.stopAnsible() creates Ssh object but never closes it, leaking JSch Session thread on every call. 2. KVMHost "check-host-is-taken-over" flow has the same pattern. When a host continuously fails reconnect (e.g. sharedblock VG error), each reconnect cycle leaks 1-2 SSH sessions. Over days this exhausts heap memory (18000+ threads observed) causing MN OOM and Unknown status. Fix: add finally { ssh.close() } to both code paths. Resolves: ZSTAC-83305 Resolves: ZSTAC-82731 Change-Id: I786e766e79776d6b7a75786d776278696c76666f --- .../org/zstack/core/ansible/CallBackNetworkChecker.java | 2 ++ plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java b/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java index d2661d5cab8..031f624218c 100644 --- a/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java +++ b/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java @@ -72,6 +72,8 @@ public ErrorCode stopAnsible() { return useNcatAndNmapToTestConnection(ssh); } catch (SshException e) { return operr(ORG_ZSTACK_CORE_ANSIBLE_10004, e.getMessage()); + } finally { + ssh.close(); } } 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 420f00aa2fd..b8b38d1b802 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5573,10 +5573,10 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { + Ssh ssh = new Ssh().setUsername(getSelf().getUsername()) + .setPassword(getSelf().getPassword()).setPort(getSelf().getPort()) + .setHostname(getSelf().getManagementIp()); try { - Ssh ssh = new Ssh().setUsername(getSelf().getUsername()) - .setPassword(getSelf().getPassword()).setPort(getSelf().getPort()) - .setHostname(getSelf().getManagementIp()); ssh.command(String.format("grep -i ^uuid %s | sed 's/uuid://g'", hostTakeOverFlagPath)); SshResult hostRet = ssh.run(); if (hostRet.isSshFailure() || hostRet.getReturnCode() != 0) { @@ -5625,6 +5625,8 @@ public void run(FlowTrigger trigger, Map data) { logger.warn(e.getMessage(), e); trigger.next(); return; + } finally { + ssh.close(); } } }); From 9349cabf6dd4563a8285a984aaeb3b5c3d2f87a0 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 19 Mar 2026 15:26:11 +0800 Subject: [PATCH 41/77] [compute]: cancel backup longjobs before migration When UI sends backupTaskLongJobUuids in MigrateVmLongJob jobData, cancel those backup longjobs and wait for volume chain tasks to exit before starting migration. Includes element-level validation for deserialized UUID list. Resolves: ZSTAC-82195 Change-Id: If4203d967b23568e7cd09fb1ecc95ae653e137d9 --- .../zstack/compute/vm/MigrateVmLongJob.java | 157 +++++++++++++++++- 1 file changed, 150 insertions(+), 7 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java b/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java index bd90e4c597a..8a2073333f4 100644 --- a/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java +++ b/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java @@ -1,27 +1,34 @@ package org.zstack.compute.vm; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; 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.Platform; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.thread.ThreadFacade; import org.zstack.header.Constants; import org.zstack.header.core.Completion; import org.zstack.header.core.ReturnValueCompletion; -import org.zstack.header.longjob.LongJobErrors; -import org.zstack.header.longjob.LongJobFor; -import org.zstack.header.longjob.LongJobVO; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.longjob.*; import org.zstack.header.message.APIEvent; import org.zstack.header.message.MessageReply; import org.zstack.header.vm.*; -import org.zstack.header.longjob.LongJob; -import org.zstack.longjob.LongJobUtils; +import org.zstack.header.volume.GetVolumeTaskMsg; +import org.zstack.header.volume.GetVolumeTaskReply; +import org.zstack.header.volume.VolumeConstant; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; import org.zstack.utils.gson.JSONObjectUtil; -import static org.zstack.core.Platform.err; +import java.util.*; +import java.util.concurrent.TimeUnit; + import static org.zstack.core.Platform.operr; @@ -31,16 +38,152 @@ @LongJobFor(APIMigrateVmMsg.class) @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class MigrateVmLongJob implements LongJob { + private static final Logger logger = LogManager.getLogger(MigrateVmLongJob.class); + private static final int WAIT_CHAIN_TASK_EXIT_MAX_RETRIES = 30; + private static final long WAIT_CHAIN_TASK_EXIT_INTERVAL_SECS = 1; + @Autowired protected CloudBus bus; @Autowired protected DatabaseFacade dbf; + @Autowired + private ThreadFacade thdf; protected String auditResourceUuid; @Override public void start(LongJobVO job, ReturnValueCompletion completion) { MigrateVmInnerMsg msg = JSONObjectUtil.toObject(job.getJobData(), MigrateVmInnerMsg.class); + + List backupTaskLongJobUuids = getBackupTaskLongJobUuids(job.getJobData()); + if (backupTaskLongJobUuids != null && !backupTaskLongJobUuids.isEmpty()) { + logger.info(String.format("migrate vm[uuid:%s] longjob has %d backup longjobs to cancel first", + msg.getVmInstanceUuid(), backupTaskLongJobUuids.size())); + cancelBackupLongJobsThenMigrate(backupTaskLongJobUuids, msg, completion); + } else { + doMigrate(msg, completion); + } + } + + private List getBackupTaskLongJobUuids(String jobData) { + Map raw = JSONObjectUtil.toObject(jobData, LinkedHashMap.class); + Object uuids = raw == null ? null : raw.get("backupTaskLongJobUuids"); + if (!(uuids instanceof List)) { + return null; + } + + List result = new ArrayList<>(); + for (Object item : (List) uuids) { + if (item == null) { + continue; + } + String uuid = String.valueOf(item).trim(); + if (!uuid.isEmpty()) { + result.add(uuid); + } + } + return result.isEmpty() ? null : result; + } + + private void cancelBackupLongJobsThenMigrate(List backupTaskLongJobUuids, + MigrateVmInnerMsg msg, + ReturnValueCompletion completion) { + cancelBackupLongJobs(backupTaskLongJobUuids.iterator(), new Completion(completion) { + @Override + public void success() { + waitForVolumeChainTasksExit(msg.getVmInstanceUuid(), WAIT_CHAIN_TASK_EXIT_MAX_RETRIES, + new Completion(completion) { + @Override + public void success() { + doMigrate(msg, completion); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to cancel backup longjobs for vm[uuid:%s], " + + "attempting migration anyway: %s", msg.getVmInstanceUuid(), errorCode)); + doMigrate(msg, completion); + } + }); + } + + private void cancelBackupLongJobs(Iterator it, Completion completion) { + if (!it.hasNext()) { + completion.success(); + return; + } + + String longJobUuid = it.next(); + CancelLongJobMsg cmsg = new CancelLongJobMsg(); + cmsg.setUuid(longJobUuid); + bus.makeLocalServiceId(cmsg, LongJobConstants.SERVICE_ID); + bus.send(cmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to cancel backup longjob[uuid:%s]: %s", + longJobUuid, reply.getError())); + } + cancelBackupLongJobs(it, completion); + } + }); + } + + private void waitForVolumeChainTasksExit(String vmUuid, int retriesLeft, Completion completion) { + List volUuids = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .select(VolumeVO_.uuid) + .listValues(); + + if (volUuids.isEmpty()) { + completion.success(); + return; + } + + GetVolumeTaskMsg gmsg = new GetVolumeTaskMsg(); + gmsg.setVolumeUuids(volUuids); + bus.makeLocalServiceId(gmsg, VolumeConstant.SERVICE_ID); + bus.send(gmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + GetVolumeTaskReply gr = reply.castReply(); + boolean hasRunningTasks = gr.getResults().values().stream() + .anyMatch(info -> !info.getRunningTask().isEmpty()); + + if (!hasRunningTasks) { + completion.success(); + return; + } + + if (retriesLeft <= 0) { + completion.fail(operr( + "timeout waiting for volume backup chain tasks to exit for vm[uuid:%s]", vmUuid)); + return; + } + + logger.debug(String.format( + "volumes of vm[uuid:%s] still have running tasks, retry in %ds (retries left: %d)", + vmUuid, WAIT_CHAIN_TASK_EXIT_INTERVAL_SECS, retriesLeft)); + thdf.submitTimeoutTask( + () -> waitForVolumeChainTasksExit(vmUuid, retriesLeft - 1, completion), + TimeUnit.SECONDS, WAIT_CHAIN_TASK_EXIT_INTERVAL_SECS); + } + }); + } + + private void doMigrate(MigrateVmInnerMsg msg, ReturnValueCompletion completion) { bus.makeTargetServiceIdByResourceUuid(msg, VmInstanceConstant.SERVICE_ID, msg.getVmInstanceUuid()); bus.send(msg, new CloudBusCallBack(completion) { @Override From ec18635daa4090a840ca0a3bcb72915bfe71032e Mon Sep 17 00:00:00 2001 From: zhangjianjun Date: Mon, 16 Mar 2026 14:01:26 +0800 Subject: [PATCH 42/77] [zwatch]: add OVN instance default alarms DBImpact Resolves: ZSTAC-73154 Change-Id: I7a6b69706374636e6d65716c6e7571657766636c Signed-off-by: zhangjianjun --- conf/db/upgrade/V5.5.12__schema.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index 6c6168445f8..b3443c65dfc 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -177,4 +177,9 @@ END $$ DELIMITER ; CALL ModifyUsedIpVOForeignKey(); -DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; \ No newline at end of file +DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; + +-- Add ActiveAlarmTemplate rows for OVN (SDN) VM instance metric alarms +INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('f1a6c2d85e7b8c9d3e4f5a6b7c8d9e0f','OvnVmInstance-DiskAllUsedCapacityInPercent','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','DiskAllUsedCapacityInPercent',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); +INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('b3c8e4f07a9d0e1f5a6b7c8d9e0f1a2b','OvnVmInstance-MemoryUsedInPercent','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','MemoryUsedInPercent',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); +INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('d5e0a6b29c1f2a3b7c8d9e0f1a2b3c4d','OvnVmInstance-CPUAverageUsedUtilization','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','CPUAverageUsedUtilization',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); From 046d023e5fa376488ff7401b4f2ea93e01fe4f64 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 18 Mar 2026 16:21:35 +0800 Subject: [PATCH 43/77] [compute]: fix user define param error when user define ip/netmask/gateway, then gateway must be in the cidr of ip/netamsl Resolves: ZSTAC-83321 Change-Id: I78616376707476666a7a72786a7062766272686b --- .../zstack/compute/vm/StaticIpOperator.java | 349 +++++++++++++--- .../compute/vm/VmInstanceApiInterceptor.java | 267 +++--------- .../network/pages/networkResource/l3Ipam.adoc | 5 +- .../flat/FlatChangeVmIpOutsideCidrCase.groovy | 384 ++++++++++++++++-- ...licNetworkChangeVmIpOutsideCidrCase.groovy | 45 -- .../CloudOperationsErrorCode.java | 4 + 6 files changed, 679 insertions(+), 375 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java index 3ebd89f5a57..92bb139450c 100755 --- a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java @@ -1,6 +1,7 @@ package org.zstack.compute.vm; import org.apache.commons.lang.StringUtils; +import org.apache.commons.net.util.SubnetUtils; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; @@ -20,6 +21,7 @@ import org.zstack.header.tag.SystemTagValidator; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmNicVO; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.tag.SystemTagCreator; import org.zstack.tag.TagManager; import org.zstack.utils.TagUtils; @@ -380,8 +382,223 @@ public void validateSystemTagInCreateMessage(APICreateMessage msg) { validateSystemTagInApiMessage(msg); } - public List fillUpStaticIpInfoToVmNics(Map staticIps) { - List newSystags = new ArrayList<>(); + // ================================================================ + // Context classes for unified resolve logic + // ================================================================ + + /** + * Describes the role of the NIC being resolved, used by resolve methods + * to decide whether gateway is mandatory. + */ + public static class NicRoleContext { + public final boolean isDefaultNic; + public final boolean isOnlyNic; + + public NicRoleContext(boolean isDefaultNic, boolean isOnlyNic) { + this.isDefaultNic = isDefaultNic; + this.isOnlyNic = isOnlyNic; + } + } + + /** + * Holds existing UsedIpVO per (l3Uuid, ipVersion) for case(d) reuse logic. + * Only APISetVmStaticIpMsg populates this; APIChangeVmNicNetworkMsg passes empty. + */ + public static class ExistingIpContext { + private final Map ipv4Map = new HashMap<>(); + private final Map ipv6Map = new HashMap<>(); + + public void putIpv4(String l3Uuid, UsedIpVO vo) { + if (vo != null) { + ipv4Map.put(l3Uuid, vo); + } + } + + public void putIpv6(String l3Uuid, UsedIpVO vo) { + if (vo != null) { + ipv6Map.put(l3Uuid, vo); + } + } + + public UsedIpVO getIpv4(String l3Uuid) { + return ipv4Map.get(l3Uuid); + } + + public UsedIpVO getIpv6(String l3Uuid) { + return ipv6Map.get(l3Uuid); + } + } + + // ================================================================ + // Unified resolve methods (migrated from VmInstanceApiInterceptor) + // ================================================================ + + /** + * Determine whether to use the NIC's existing IPv4 parameters (netmask/gateway). + * Condition: existingIp is non-null with non-empty netmask and non-empty gateway, + * and the IP falls within the CIDR formed by existingIp's gateway + netmask. + */ + public boolean shouldUseExistingIpv4(String ip, UsedIpVO existingIp) { + if (existingIp == null || StringUtils.isEmpty(existingIp.getNetmask())) { + return false; + } + if (StringUtils.isEmpty(existingIp.getGateway())) { + return false; + } + try { + SubnetUtils.SubnetInfo info = NetworkUtils.getSubnetInfo( + new SubnetUtils(existingIp.getGateway(), existingIp.getNetmask())); + return NetworkUtils.isIpv4InRange(ip, info.getLowAddress(), info.getHighAddress()); + } catch (Exception e) { + return false; + } + } + + /** + * Determine whether to use the NIC's existing IPv6 parameters (prefix/gateway). + * Condition: existingIp is non-null with non-null prefixLen and non-empty gateway, + * and the IP falls within the CIDR formed by existingIp's gateway + prefixLen. + */ + public boolean shouldUseExistingIpv6(String ip6, UsedIpVO existingIp) { + if (existingIp == null || existingIp.getPrefixLen() == null) { + return false; + } + if (StringUtils.isEmpty(existingIp.getGateway())) { + return false; + } + try { + return IPv6NetworkUtils.isIpv6InCidrRange(ip6, + existingIp.getGateway() + "/" + existingIp.getPrefixLen()); + } catch (Exception e) { + return false; + } + } + + /** + * Resolve IPv4 netmask and gateway based on 4 cases: + * (a) Both netmask+gateway provided: validate gateway is in CIDR(ip/netmask), then use user input + * (b) Gateway provided, no netmask: if ip and gateway both in L3 CIDR, use CIDR netmask; else error + * (c) Netmask provided, no gateway: if netmask == CIDR netmask, use CIDR gateway; else gateway="" + * (d) Neither provided: if existingIp usable, use it; else if in L3 CIDR, use CIDR; else error + */ + public String[] resolveIpv4NetmaskAndGateway(String ip, String userNetmask, String userGateway, + List ipv4Ranges, NicRoleContext nicRole, UsedIpVO existingIp) { + boolean hasNetmask = StringUtils.isNotEmpty(userNetmask); + boolean hasGateway = StringUtils.isNotEmpty(userGateway); + + // case (a): both provided — validate gateway is in the CIDR formed by ip/netmask + if (hasNetmask && hasGateway) { + String cidr = NetworkUtils.getCidrFromIpMask(ip, userNetmask); + if (!NetworkUtils.isIpv4InCidr(userGateway, cidr)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10329, + "gateway[%s] is not in the CIDR[%s] formed by IP[%s] and netmask[%s]", + userGateway, cidr, ip, userNetmask)); + } + return new String[]{userNetmask, userGateway}; + } + + NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip, ipv4Ranges); + + // case (b): gateway provided, no netmask + if (hasGateway) { + if (matchedRange != null && matchedRange.getNetworkCidr() != null + && NetworkUtils.isIpv4InCidr(userGateway, matchedRange.getNetworkCidr())) { + return new String[]{matchedRange.getNetmask(), userGateway}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10323, + "gateway[%s] is provided but IP[%s] and gateway are not both in L3 network CIDR, netmask must be specified", + userGateway, ip)); + } + + // case (c): netmask provided, no gateway + if (hasNetmask) { + if (matchedRange != null && userNetmask.equals(matchedRange.getNetmask())) { + return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; + } + return new String[]{userNetmask, ""}; + } + + // case (d): neither provided + if (existingIp != null && shouldUseExistingIpv4(ip, existingIp)) { + return new String[]{existingIp.getNetmask(), existingIp.getGateway()}; + } + if (matchedRange != null) { + return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10325, + "IP[%s] is outside all L3 network CIDRs and no existing IP parameters available, netmask and gateway must be specified", + ip)); + } + + /** + * Resolve IPv6 prefix and gateway based on 4 cases (mirrors IPv4 logic): + * (a) Both prefix+gateway provided: validate gateway is in CIDR(ip6/prefix), then use user input + * (b) Gateway provided, no prefix: if ip and gateway both in L3 CIDR, use CIDR prefix; else error + * (c) Prefix provided, no gateway: if prefix == CIDR prefix, use CIDR gateway; else if default/sole NIC, error; else gateway="" + * (d) Neither provided: if existingIp usable, use it; else if in L3 CIDR, use CIDR; else error + */ + public String[] resolveIpv6PrefixAndGateway(String ip6, String userPrefix, String userGateway, + List ipv6Ranges, NicRoleContext nicRole, UsedIpVO existingIp) { + boolean hasPrefix = StringUtils.isNotEmpty(userPrefix); + boolean hasGateway = StringUtils.isNotEmpty(userGateway); + + // case (a): both provided — validate gateway is in the CIDR formed by ip6/prefix + if (hasPrefix && hasGateway) { + String cidr = ip6 + "/" + userPrefix; + if (!IPv6NetworkUtils.isIpv6InCidrRange(userGateway, cidr)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10330, + "gateway[%s] is not in the CIDR[%s] formed by IPv6[%s] and prefix[%s]", + userGateway, cidr, ip6, userPrefix)); + } + return new String[]{userPrefix, userGateway}; + } + + NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip6, ipv6Ranges); + + // case (b): gateway provided, no prefix + if (hasGateway) { + if (matchedRange != null && matchedRange.getNetworkCidr() != null + && IPv6NetworkUtils.isIpv6InCidrRange(userGateway, matchedRange.getNetworkCidr())) { + return new String[]{matchedRange.getPrefixLen().toString(), userGateway}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10326, + "gateway[%s] is provided but IPv6[%s] and gateway are not both in L3 network CIDR, prefix must be specified", + userGateway, ip6)); + } + + // case (c): prefix provided, no gateway + if (hasPrefix) { + if (matchedRange != null && userPrefix.equals(matchedRange.getPrefixLen().toString())) { + return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; + } + if (nicRole.isDefaultNic || nicRole.isOnlyNic) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10327, + "prefix[%s] does not match L3 CIDR prefix and the NIC is the default or sole network, gateway must be specified", + userPrefix)); + } + return new String[]{userPrefix, ""}; + } + + // case (d): neither provided + if (existingIp != null && shouldUseExistingIpv6(ip6, existingIp)) { + return new String[]{existingIp.getPrefixLen().toString(), existingIp.getGateway()}; + } + if (matchedRange != null) { + return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10328, + "IPv6[%s] is outside all L3 network CIDRs and no existing IP parameters available, prefix and gateway must be specified", + ip6)); + } + + // ================================================================ + // IP availability validation (extracted from old fillUpStaticIpInfoToVmNics) + // ================================================================ + + /** + * Validate that all static IPs are available on their respective L3 networks. + */ + public void validateIpAvailability(Map staticIps) { for (Map.Entry e : staticIps.entrySet()) { String l3Uuid = e.getKey(); NicIpAddressInfo nicIp = e.getValue(); @@ -393,72 +610,63 @@ public List fillUpStaticIpInfoToVmNics(Map sta if (!StringUtils.isEmpty(nicIp.ipv6Address)) { checkIpAvailability(l3Uuid, nicIp.ipv6Address); } + } + } - if (!StringUtils.isEmpty(nicIp.ipv4Address)) { - NormalIpRangeVO ipRangeVO = findMatchedNormalIpRange(l3Uuid, nicIp.ipv4Address); - if (ipRangeVO == null) { - if (StringUtils.isEmpty(nicIp.ipv4Netmask)) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10310, "netmask must be set")); - } - } else { - if (StringUtils.isEmpty(nicIp.ipv4Netmask)) { - newSystags.add(VmSystemTags.IPV4_NETMASK.instantiateTag( - map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV4_NETMASK_TOKEN, ipRangeVO.getNetmask())) - )); - } else if (!nicIp.ipv4Netmask.equals(ipRangeVO.getNetmask())) { - newSystags.add(VmSystemTags.IPV4_NETMASK.instantiateTag( - map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV4_NETMASK_TOKEN, nicIp.ipv4Netmask)) - )); - } + // ================================================================ + // fillUpStaticIpInfoToVmNics — orchestration layer + // ================================================================ + + /** + * New signature: resolves netmask/gateway (or prefix/gateway) for each static IP entry, + * using the unified resolve methods with NicRoleContext and ExistingIpContext. + * Returns system tags to be added to the message. + */ + public List fillUpStaticIpInfoToVmNics(Map staticIps, + NicRoleContext nicRole, ExistingIpContext existingIpCtx) { + List newSystags = new ArrayList<>(); + for (Map.Entry entry : staticIps.entrySet()) { + String l3Uuid = entry.getKey(); + NicIpAddressInfo nicIp = entry.getValue(); - if (StringUtils.isEmpty(nicIp.ipv4Gateway)) { - newSystags.add(VmSystemTags.IPV4_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipRangeVO.getGateway())) - )); - } else if (!nicIp.ipv4Gateway.equals(ipRangeVO.getGateway())) { - newSystags.add(VmSystemTags.IPV4_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV4_GATEWAY_TOKEN, nicIp.ipv4Gateway)) - )); - } + // Resolve IPv4 netmask/gateway + if (!StringUtils.isEmpty(nicIp.ipv4Address)) { + List ipv4Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + UsedIpVO existingIpv4 = existingIpCtx != null ? existingIpCtx.getIpv4(l3Uuid) : null; + + String[] ipv4Result = resolveIpv4NetmaskAndGateway(nicIp.ipv4Address, + nicIp.ipv4Netmask, nicIp.ipv4Gateway, ipv4Ranges, nicRole, existingIpv4); + + newSystags.add(VmSystemTags.IPV4_NETMASK.instantiateTag( + map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV4_NETMASK_TOKEN, ipv4Result[0])))); + if (!StringUtils.isEmpty(ipv4Result[1])) { + newSystags.add(VmSystemTags.IPV4_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipv4Result[1])))); } } + // Resolve IPv6 prefix/gateway if (!StringUtils.isEmpty(nicIp.ipv6Address)) { - NormalIpRangeVO ipRangeVO = findMatchedNormalIpRange(l3Uuid, nicIp.ipv6Address); - if (ipRangeVO == null) { - if (StringUtils.isEmpty(nicIp.ipv6Prefix)) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10313, "ipv6 prefix length must be set")); - } - } else { - if (StringUtils.isEmpty(nicIp.ipv6Prefix)) { - newSystags.add(VmSystemTags.IPV6_PREFIX.instantiateTag( - map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV6_PREFIX_TOKEN, ipRangeVO.getPrefixLen())) - )); - } else if (!nicIp.ipv6Prefix.equals(ipRangeVO.getPrefixLen().toString())) { - newSystags.add(VmSystemTags.IPV6_PREFIX.instantiateTag( - map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV6_PREFIX_TOKEN, nicIp.ipv6Prefix)) - )); - } - - if (StringUtils.isEmpty(nicIp.ipv6Gateway)) { - newSystags.add(VmSystemTags.IPV6_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV6_GATEWAY_TOKEN, - IPv6NetworkUtils.ipv6AddressToTagValue(ipRangeVO.getGateway()))) - )); - } else if (!nicIp.ipv6Gateway.equals(ipRangeVO.getGateway())) { - newSystags.add(VmSystemTags.IPV6_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV6_GATEWAY_TOKEN, - IPv6NetworkUtils.ipv6AddressToTagValue(nicIp.ipv6Gateway))) - )); - } + List ipv6Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + UsedIpVO existingIpv6 = existingIpCtx != null ? existingIpCtx.getIpv6(l3Uuid) : null; + + String[] ipv6Result = resolveIpv6PrefixAndGateway(nicIp.ipv6Address, + nicIp.ipv6Prefix, nicIp.ipv6Gateway, ipv6Ranges, nicRole, existingIpv6); + + newSystags.add(VmSystemTags.IPV6_PREFIX.instantiateTag( + map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV6_PREFIX_TOKEN, ipv6Result[0])))); + if (!StringUtils.isEmpty(ipv6Result[1])) { + newSystags.add(VmSystemTags.IPV6_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV6_GATEWAY_TOKEN, + IPv6NetworkUtils.ipv6AddressToTagValue(ipv6Result[1]))))); } } } @@ -466,10 +674,27 @@ public List fillUpStaticIpInfoToVmNics(Map sta return newSystags; } + /** + * Legacy overload: preserves old behavior for existing callers + * (APICreateVmInstanceMsg, APIAttachL3NetworkToVmMsg). + * Uses default NicRoleContext(false, false) and empty ExistingIpContext. + */ + public List fillUpStaticIpInfoToVmNics(Map staticIps) { + return fillUpStaticIpInfoToVmNics(staticIps, + new NicRoleContext(false, false), new ExistingIpContext()); + } + public void validateSystemTagInApiMessage(APIMessage msg) { Map staticIps = getNicNetworkInfoBySystemTag(msg.getSystemTags()); + validateIpAvailability(staticIps); List newSystags = fillUpStaticIpInfoToVmNics(staticIps); if (!newSystags.isEmpty()) { + if (msg.getSystemTags() != null) { + // Remove any existing netmask/gateway/prefix tags before adding resolved ones + msg.getSystemTags().removeIf(tag -> + VmSystemTags.IPV4_NETMASK.isMatch(tag) || VmSystemTags.IPV4_GATEWAY.isMatch(tag) + || VmSystemTags.IPV6_PREFIX.isMatch(tag) || VmSystemTags.IPV6_GATEWAY.isMatch(tag)); + } msg.getSystemTags().addAll(newSystags); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java index 6f9c75963ef..06d175a8d13 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java @@ -3,7 +3,6 @@ import com.google.gson.JsonSyntaxException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; -import org.apache.commons.net.util.SubnetUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.core.Platform; @@ -58,9 +57,7 @@ import static org.zstack.core.Platform.argerr; import static org.zstack.core.Platform.operr; -import static org.zstack.utils.CollectionDSL.e; import static org.zstack.utils.CollectionDSL.list; -import static org.zstack.utils.CollectionDSL.map; import static org.zstack.utils.CollectionUtils.getDuplicateElementsOfList; import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; @@ -115,161 +112,6 @@ private void validateStaticIpCommon(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, St } } - /** - * Determine whether to use the NIC's existing IPv4 parameters (netmask/gateway). - * Condition: existingIp is non-null with non-empty netmask and non-empty gateway, - * and the IP falls within the CIDR formed by existingIp's gateway + netmask. - */ - private boolean shouldUseExistingIpv4(String ip, UsedIpVO existingIp) { - if (existingIp == null || StringUtils.isEmpty(existingIp.getNetmask())) { - return false; - } - if (StringUtils.isEmpty(existingIp.getGateway())) { - return false; - } - try { - SubnetUtils.SubnetInfo info = NetworkUtils.getSubnetInfo( - new SubnetUtils(existingIp.getGateway(), existingIp.getNetmask())); - return NetworkUtils.isIpv4InRange(ip, info.getLowAddress(), info.getHighAddress()); - } catch (Exception e) { - return false; - } - } - - /** - * Determine whether to use the NIC's existing IPv6 parameters (prefix/gateway). - * Condition: existingIp is non-null with non-null prefixLen and non-empty gateway, - * and the IP falls within the CIDR formed by existingIp's gateway + prefixLen. - */ - private boolean shouldUseExistingIpv6(String ip6, UsedIpVO existingIp) { - if (existingIp == null || existingIp.getPrefixLen() == null) { - return false; - } - if (StringUtils.isEmpty(existingIp.getGateway())) { - return false; - } - try { - return IPv6NetworkUtils.isIpv6InCidrRange(ip6, - existingIp.getGateway() + "/" + existingIp.getPrefixLen()); - } catch (Exception e) { - return false; - } - } - - /** - * Resolve IPv4 netmask and gateway based on 4 cases: - * (a) Both netmask+gateway provided: use user input as-is - * (b) Gateway provided, no netmask: if ip and gateway both in L3 CIDR, use CIDR netmask; else error - * (c) Netmask provided, no gateway: if netmask == CIDR netmask, use CIDR gateway; else if default/sole NIC, error; else gateway="" - * (d) Neither provided: if existingIp usable (APISetVmStaticIpMsg), use it; else if in L3 CIDR, use CIDR; else error - * - * @param existingIp pass null for APIChangeVmNicNetworkMsg (no existing IP on dest L3) - */ - private String[] resolveIpv4NetmaskAndGateway(String ip, String userNetmask, String userGateway, - List ipv4Ranges, String l3Uuid, String defaultL3Uuid, int vmNicCount, UsedIpVO existingIp) { - boolean hasNetmask = StringUtils.isNotEmpty(userNetmask); - boolean hasGateway = StringUtils.isNotEmpty(userGateway); - - // case (a): both provided - if (hasNetmask && hasGateway) { - return new String[]{userNetmask, userGateway}; - } - - NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip, ipv4Ranges); - - // case (b): gateway provided, no netmask - if (hasGateway) { - if (matchedRange != null && matchedRange.getNetworkCidr() != null - && NetworkUtils.isIpv4InCidr(userGateway, matchedRange.getNetworkCidr())) { - return new String[]{matchedRange.getNetmask(), userGateway}; - } - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10323, - "gateway[%s] is provided but IP[%s] and gateway are not both in L3 network CIDR, netmask must be specified", - userGateway, ip)); - } - - // case (c): netmask provided, no gateway - if (hasNetmask) { - if (matchedRange != null && userNetmask.equals(matchedRange.getNetmask())) { - return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; - } - if (l3Uuid.equals(defaultL3Uuid) || vmNicCount == 1) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10324, - "netmask[%s] does not match L3 CIDR netmask and the NIC is the default or sole network, gateway must be specified", - userNetmask)); - } - return new String[]{userNetmask, ""}; - } - - // case (d): neither provided - if (existingIp != null && shouldUseExistingIpv4(ip, existingIp)) { - return new String[]{existingIp.getNetmask(), existingIp.getGateway()}; - } - if (matchedRange != null) { - return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; - } - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10325, - "IP[%s] is outside all L3 network CIDRs and no existing IP parameters available, netmask and gateway must be specified", - ip)); - } - - /** - * Resolve IPv6 prefix and gateway based on 4 cases (mirrors IPv4 logic): - * (a) Both prefix+gateway provided: use user input as-is - * (b) Gateway provided, no prefix: if ip and gateway both in L3 CIDR, use CIDR prefix; else error - * (c) Prefix provided, no gateway: if prefix == CIDR prefix, use CIDR gateway; else if default/sole NIC, error; else gateway="" - * (d) Neither provided: if existingIp usable (APISetVmStaticIpMsg), use it; else if in L3 CIDR, use CIDR; else error - * - * @param existingIp pass null for APIChangeVmNicNetworkMsg (no existing IP on dest L3) - */ - private String[] resolveIpv6PrefixAndGateway(String ip6, String userPrefix, String userGateway, - List ipv6Ranges, String l3Uuid, String defaultL3Uuid, int vmNicCount, UsedIpVO existingIp) { - boolean hasPrefix = StringUtils.isNotEmpty(userPrefix); - boolean hasGateway = StringUtils.isNotEmpty(userGateway); - - // case (a): both provided - if (hasPrefix && hasGateway) { - return new String[]{userPrefix, userGateway}; - } - - NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip6, ipv6Ranges); - - // case (b): gateway provided, no prefix - if (hasGateway) { - if (matchedRange != null && matchedRange.getNetworkCidr() != null - && IPv6NetworkUtils.isIpv6InCidrRange(userGateway, matchedRange.getNetworkCidr())) { - return new String[]{matchedRange.getPrefixLen().toString(), userGateway}; - } - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10326, - "gateway[%s] is provided but IPv6[%s] and gateway are not both in L3 network CIDR, prefix must be specified", - userGateway, ip6)); - } - - // case (c): prefix provided, no gateway - if (hasPrefix) { - if (matchedRange != null && userPrefix.equals(matchedRange.getPrefixLen().toString())) { - return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; - } - if (l3Uuid.equals(defaultL3Uuid) || vmNicCount == 1) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10327, - "prefix[%s] does not match L3 CIDR prefix and the NIC is the default or sole network, gateway must be specified", - userPrefix)); - } - return new String[]{userPrefix, ""}; - } - - // case (d): neither provided - if (existingIp != null && shouldUseExistingIpv6(ip6, existingIp)) { - return new String[]{existingIp.getPrefixLen().toString(), existingIp.getGateway()}; - } - if (matchedRange != null) { - return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; - } - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10328, - "IPv6[%s] is outside all L3 network CIDRs and no existing IP parameters available, prefix and gateway must be specified", - ip6)); - } - /** * Check whether an IP is already in use (using error code ORG_ZSTACK_COMPUTE_VM_10105). */ @@ -504,60 +346,34 @@ private void validate(APIChangeVmNicNetworkMsg msg) { } } - new StaticIpOperator().validateSystemTagInApiMessage(msg); + // Build NicRoleContext for resolve logic + String defaultL3Uuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findValue(); + int vmNicCount = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vmUuid).count().intValue(); + boolean isDefaultNic = srcL3Uuid.equals(defaultL3Uuid); + boolean isOnlyNic = vmNicCount == 1; - // Resolve netmask/gateway for static IPs in systemTags, overriding what validateSystemTagInApiMessage may have set - { - String destL3Uuid = msg.getDestL3NetworkUuid(); - Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); - NicIpAddressInfo nicIpInfo = nicNetworkInfo.get(destL3Uuid); - if (nicIpInfo != null) { - List destIpv4Ranges = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, destL3Uuid) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); - List destIpv6Ranges = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, destL3Uuid) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); - String defaultL3Uuid = Q.New(VmInstanceVO.class) - .select(VmInstanceVO_.defaultL3NetworkUuid) - .eq(VmInstanceVO_.uuid, vmUuid) - .findValue(); - int vmNicCount = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vmUuid).count().intValue(); - - // Remove existing netmask/gateway/prefix/ipv6Gateway tags for dest L3 from systemTags - if (msg.getSystemTags() != null) { - msg.getSystemTags().removeIf(tag -> - VmSystemTags.IPV4_NETMASK.isMatch(tag) || VmSystemTags.IPV4_GATEWAY.isMatch(tag) - || VmSystemTags.IPV6_PREFIX.isMatch(tag) || VmSystemTags.IPV6_GATEWAY.isMatch(tag)); - } + StaticIpOperator staticIpOp = new StaticIpOperator(); + Map nicNetworkInfo = staticIpOp.getNicNetworkInfoBySystemTag(msg.getSystemTags()); - // Resolve and add IPv4 netmask/gateway - if (StringUtils.isNotEmpty(nicIpInfo.ipv4Address)) { - String[] ipv4Result = resolveIpv4NetmaskAndGateway(nicIpInfo.ipv4Address, - nicIpInfo.ipv4Netmask, nicIpInfo.ipv4Gateway, - destIpv4Ranges, destL3Uuid, defaultL3Uuid, vmNicCount, null); - msg.getSystemTags().add(VmSystemTags.IPV4_NETMASK.instantiateTag( - map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, destL3Uuid), - e(VmSystemTags.IPV4_NETMASK_TOKEN, ipv4Result[0])))); - msg.getSystemTags().add(VmSystemTags.IPV4_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, destL3Uuid), - e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipv4Result[1])))); - } + // Validate IP availability + staticIpOp.validateIpAvailability(nicNetworkInfo); - // Resolve and add IPv6 prefix/gateway - if (StringUtils.isNotEmpty(nicIpInfo.ipv6Address)) { - String[] ipv6Result = resolveIpv6PrefixAndGateway(nicIpInfo.ipv6Address, - nicIpInfo.ipv6Prefix, nicIpInfo.ipv6Gateway, - destIpv6Ranges, destL3Uuid, defaultL3Uuid, vmNicCount, null); - msg.getSystemTags().add(VmSystemTags.IPV6_PREFIX.instantiateTag( - map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, destL3Uuid), - e(VmSystemTags.IPV6_PREFIX_TOKEN, ipv6Result[0])))); - msg.getSystemTags().add(VmSystemTags.IPV6_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, destL3Uuid), - e(VmSystemTags.IPV6_GATEWAY_TOKEN, - IPv6NetworkUtils.ipv6AddressToTagValue(ipv6Result[1]))))); - } - } + // Resolve netmask/gateway using unified logic (no existingIp reuse for ChangeNicNetwork) + StaticIpOperator.NicRoleContext nicRole = new StaticIpOperator.NicRoleContext(isDefaultNic, isOnlyNic); + List resolvedTags = staticIpOp.fillUpStaticIpInfoToVmNics(nicNetworkInfo, + nicRole, new StaticIpOperator.ExistingIpContext()); + + // Remove any existing netmask/gateway/prefix tags, then add resolved ones + if (msg.getSystemTags() != null) { + msg.getSystemTags().removeIf(tag -> + VmSystemTags.IPV4_NETMASK.isMatch(tag) || VmSystemTags.IPV4_GATEWAY.isMatch(tag) + || VmSystemTags.IPV6_PREFIX.isMatch(tag) || VmSystemTags.IPV6_GATEWAY.isMatch(tag)); + } + if (!resolvedTags.isEmpty()) { + msg.getSystemTags().addAll(resolvedTags); } Map> staticIps = new StaticIpOperator().getStaticIpbySystemTag(msg.getSystemTags()); @@ -749,6 +565,7 @@ protected void scripts() { throw new ApiMessageInterceptionException(argerr( ORG_ZSTACK_COMPUTE_VM_10124, "the VM cannot do cpu hot plug because of disabling cpu hot plug. Please stop the VM then do the cpu hot plug again" )); + } if (memorySize != null && memorySize != vo.getMemorySize()) { @@ -811,12 +628,6 @@ private void validate(APISetVmStaticIpMsg msg) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10135, "could not set ip address, due to no ip address is specified")); } } - List ipv4Ranges = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); - List ipv6Ranges = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); List vmNics = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, msg.getVmInstanceUuid()).list(); boolean l3Found = false; @@ -874,18 +685,32 @@ private void validate(APISetVmStaticIpMsg msg) { .select(VmInstanceVO_.defaultL3NetworkUuid) .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) .findValue(); + boolean isDefaultNic = msg.getL3NetworkUuid().equals(defaultL3NetworkUuid); + boolean isOnlyNic = vmNics.size() == 1; + + StaticIpOperator staticIpOp = new StaticIpOperator(); + StaticIpOperator.NicRoleContext nicRole = new StaticIpOperator.NicRoleContext(isDefaultNic, isOnlyNic); + StaticIpOperator.ExistingIpContext existingIpCtx = new StaticIpOperator.ExistingIpContext(); + existingIpCtx.putIpv4(msg.getL3NetworkUuid(), existingIpv4); + existingIpCtx.putIpv6(msg.getL3NetworkUuid(), existingIpv6); - // Fill parameters and check IP occupation + // Fill parameters and check IP occupation using unified resolve if (normalizedIp != null) { - String[] ipv4Result = resolveIpv4NetmaskAndGateway(normalizedIp, msg.getNetmask(), msg.getGateway(), - ipv4Ranges, msg.getL3NetworkUuid(), defaultL3NetworkUuid, vmNics.size(), existingIpv4); + List ipv4Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + String[] ipv4Result = staticIpOp.resolveIpv4NetmaskAndGateway(normalizedIp, msg.getNetmask(), msg.getGateway(), + ipv4Ranges, nicRole, existingIpv4); msg.setNetmask(ipv4Result[0]); msg.setGateway(ipv4Result[1]); checkIpOccupied(normalizedIp, msg.getL3NetworkUuid()); } if (normalizedIp6 != null) { - String[] ipv6Result = resolveIpv6PrefixAndGateway(normalizedIp6, msg.getIpv6Prefix(), msg.getIpv6Gateway(), - ipv6Ranges, msg.getL3NetworkUuid(), defaultL3NetworkUuid, vmNics.size(), existingIpv6); + List ipv6Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + String[] ipv6Result = staticIpOp.resolveIpv6PrefixAndGateway(normalizedIp6, msg.getIpv6Prefix(), msg.getIpv6Gateway(), + ipv6Ranges, nicRole, existingIpv6); msg.setIpv6Prefix(ipv6Result[0]); msg.setIpv6Gateway(ipv6Result[1]); checkIpOccupied(normalizedIp6, msg.getL3NetworkUuid()); @@ -1711,4 +1536,4 @@ private void validate(APIFstrimVmMsg msg) { } msg.setHostUuid(t.get(1, String.class)); } -} +} \ No newline at end of file diff --git a/docs/modules/network/pages/networkResource/l3Ipam.adoc b/docs/modules/network/pages/networkResource/l3Ipam.adoc index ce3fcda8335..8f0d00fb117 100644 --- a/docs/modules/network/pages/networkResource/l3Ipam.adoc +++ b/docs/modules/network/pages/networkResource/l3Ipam.adoc @@ -155,8 +155,7 @@ Port Forwarding 不能绑定 IP 不在 L3 CIDR 内的网卡;`APIGetPortForward * 用户输入掩码与网关:以用户输入为准。 * 用户仅输入网关:若 IP 与网关均在 L3 CIDR 内,使用 L3 CIDR 掩码;否则报错。 * 用户仅输入掩码:若与 L3 CIDR 掩码一致,使用 L3 CIDR 网关;否则若网卡为默认网卡或唯一网卡,报错;否则使用输入掩码,网关置空。 -* 用户未输入掩码和网关,且 IP 在 L3 CIDR 内:使用 L3 CIDR 的掩码与网关。 -* 用户未输入掩码和网关,且 IP 在 L3 CIDR 外:报错。 +* 用户未输入掩码和网关:若网卡存在相同 IP 版本地址,且输入 IP 在旧 IP 的掩码与网关组成 CIDR 内,则复用旧掩码与网关;否则若 IP 在 L3 CIDR 内,使用 L3 CIDR 的掩码与网关;否则报错。 * IPv6 与 IPv4 的逻辑一致。 * 对 DNS 参数:若 UI 传递 `NULL`,后端保持旧 DNS 参数不变。 * 对 DNS 参数:若 UI 传递 `""` 或 `[]`,后端删除旧 DNS。 @@ -169,7 +168,7 @@ Port Forwarding 不能绑定 IP 不在 L3 CIDR 内的网卡;`APIGetPortForward * 用户输入掩码与网关:以用户输入为准。 * 用户仅输入网关:若 IP 与网关均在 L3 CIDR 内,使用 L3 CIDR 掩码;否则报错。 * 用户仅输入掩码:若与 L3 CIDR 掩码一致,使用 L3 CIDR 网关;否则若网卡为默认网卡或唯一网卡,报错;否则使用输入掩码,网关置空。 -* 用户未输入掩码和网关:若网卡存在相同 IP 版本地址,且输入 IP 在旧 IP 的掩码与网关组成 CIDR 内,则复用旧掩码与网关;否则若 IP 在 L3 CIDR 内,使用 L3 CIDR 掩码与网关;否则报错。 +* 用户未输入掩码和网关:若 IP 在 L3 CIDR 内,使用 L3 CIDR 掩码与网关;否则报错。 * IPv6 与 IPv4 的逻辑一致。 * 对 DNS 参数:若 UI 传递 `NULL`,后端保持旧 DNS 参数不变。 * 对 DNS 参数:若 UI 传递 `""` 或 `[]`,后端删除旧 DNS。 diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy index 62b71e75e04..90875ddbf6a 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy @@ -6,7 +6,10 @@ import org.zstack.core.db.Q import org.zstack.header.network.l3.UsedIpVO import org.zstack.header.network.l3.UsedIpVO_ import org.zstack.header.network.service.NetworkServiceType +import org.zstack.header.vm.VmInstanceVO +import org.zstack.header.vm.VmInstanceVO_ import org.zstack.header.vm.VmNicVO +import org.zstack.header.vm.VmNicVO_ import org.zstack.network.securitygroup.SecurityGroupConstant import org.zstack.network.service.eip.EipConstant import org.zstack.network.service.flat.FlatDhcpBackend @@ -33,6 +36,12 @@ import org.zstack.utils.network.IPv6Constants * * Each scenario tests: setVmStaticIp, changeVmNicNetwork, DHCP skip, EIP rejection. * Additional: orphan IP backfill when adding IP range. + * + * Netmask/gateway auto-resolve tests (Case A–D): + * Uses flatL3_range_noDhcp (CIDR: 192.168.100.0/24) as destination. + * Tests the unified resolveIpv4NetmaskAndGateway logic via changeVmNicNetwork and setVmStaticIp. + * Case C (netmask mismatch) requires multi-NIC VMs via flatL3_second (no IPAM). + * Case D-3 (existing IP reuse) is unique to setVmStaticIp. */ class FlatChangeVmIpOutsideCidrCase extends SubCase { @@ -93,6 +102,7 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { attachL2Network("l2-pub-range-dhcp") attachL2Network("l2-backfill") attachL2Network("l2-dest") + attachL2Network("l2-second") } localPrimaryStorage { @@ -237,6 +247,22 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { } } + // ========== Second L3: no IPAM, used as extra NIC for multi-NIC resolve tests ========== + l2NoVlanNetwork { + name = "l2-second" + physicalInterface = "eth7" + + l3Network { + name = "flatL3_second" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + } + } + attachBackupStorage("sftp") } } @@ -277,6 +303,21 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { // Orphan IP backfill // ========================================== testOrphanIpBackfillOnAddIpRange() + + // ========================================== + // Netmask/gateway auto-resolve (Case A–D) + // Uses flatL3_range_noDhcp (CIDR 192.168.100.0/24) + // ========================================== + testResolve_A1_bothProvided_gatewayInCidr_success() + testResolve_A2_bothProvided_gatewayNotInCidr_error() + testResolve_B1_gatewayAndIpBothInCidr_success() + testResolve_B2_ipInCidrButGatewayNotInCidr_error() + testResolve_B3_ipNotInAnyCidr_error() + testResolve_C1_netmaskMatchesCidr_success() + testResolve_C4_netmaskMismatch_nonDefaultNonSole_success() + testResolve_D1_ipInCidr_success() + testResolve_D2_ipOutsideCidr_error() + testResolve_D3_setStaticIp_existingIpReuse_success() } } @@ -294,6 +335,18 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { } } + VmInstanceInventory createVmOnL3(String vmName, List l3Uuids, String defaultL3Uuid = null) { + return createVmInstance { + name = vmName + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = l3Uuids + if (defaultL3Uuid != null) { + delegate.defaultL3NetworkUuid = defaultL3Uuid + } + } + } + // ================================================================ // Flat: no IP range, no DHCP // ================================================================ @@ -455,28 +508,6 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) assert nicVO.ip == "10.0.0.50" - - // Also verify in-range IP works - VmInstanceInventory vm2 = createVmOnL3("vm-flat-range-noDhcp-inrange", l3.uuid) - List freeIps1 = getFreeIp { - l3NetworkUuid = l3.uuid - ipVersion = IPv6Constants.IPv4 - limit = 1 - } as List - String inRangeIp1 = freeIps1.get(0).getIp() - - setVmStaticIp { - vmInstanceUuid = vm2.uuid - l3NetworkUuid = l3.uuid - ip = inRangeIp1 - } - - UsedIpVO inRangeIp = Q.New(UsedIpVO.class) - .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) - .eq(UsedIpVO_.ip, inRangeIp1) - .find() - assert inRangeIp != null - assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" } /** @@ -603,28 +634,6 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) assert nicVO.ip == "10.0.1.50" - - // Also verify in-range IP works - VmInstanceInventory vm2 = createVmOnL3("vm-flat-range-dhcp-inrange", l3.uuid) - List freeIps2 = getFreeIp { - l3NetworkUuid = l3.uuid - ipVersion = IPv6Constants.IPv4 - limit = 1 - } as List - String inRangeIp2 = freeIps2.get(0).getIp() - - setVmStaticIp { - vmInstanceUuid = vm2.uuid - l3NetworkUuid = l3.uuid - ip = inRangeIp2 - } - - UsedIpVO inRangeIp = Q.New(UsedIpVO.class) - .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) - .eq(UsedIpVO_.ip, inRangeIp2) - .find() - assert inRangeIp != null - assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" } /** @@ -920,4 +929,291 @@ class FlatChangeVmIpOutsideCidrCase extends SubCase { assert afterBackfill.usedIpAddressNumber == beforeBackfill.usedIpAddressNumber + outsideCount : "usedIpAddressNumber should increase by ${outsideCount} after backfill" } + + // ================================================================ + // Netmask/gateway auto-resolve tests (Case A–D) + // Uses flatL3_range_noDhcp (CIDR: 192.168.100.0/24, gw: 192.168.100.1) + // Source L3: flatL3_dest (no IPAM) + // Second L3: flatL3_second (no IPAM, for multi-NIC tests) + // ================================================================ + + /** + * a-1: both netmask+gateway provided, gateway in CIDR(ip/netmask). + * Expected: success, use user input as-is. + */ + void testResolve_A1_bothProvided_gatewayInCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-a1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.40", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", destL3.uuid), + String.format("ipv4Gateway::%s::192.168.100.1", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == destL3.uuid + assert nicVO.ip == "192.168.100.40" + assert nicVO.netmask == "255.255.255.0" + assert nicVO.gateway == "192.168.100.1" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "192.168.100.40") + .find() + assert usedIp != null + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "192.168.100.1" + } + + /** + * a-2: both netmask+gateway provided, gateway NOT in CIDR(ip/netmask). + * Expected: error. + */ + void testResolve_A2_bothProvided_gatewayNotInCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-a2", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.41", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", destL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", destL3.uuid) + ] + } + } + } + + /** + * b-1: gateway provided (no netmask), IP and gateway both in L3 CIDR. + * Expected: success, netmask from CIDR. + */ + void testResolve_B1_gatewayAndIpBothInCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-b1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.50", destL3.uuid), + String.format("ipv4Gateway::%s::192.168.100.1", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.50" + assert nicVO.netmask == "255.255.255.0" : "netmask should be inferred from L3 CIDR" + assert nicVO.gateway == "192.168.100.1" + } + + /** + * b-2: gateway provided (no netmask), IP in CIDR but gateway NOT in CIDR. + * Expected: error. + */ + void testResolve_B2_ipInCidrButGatewayNotInCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-b2", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.51", destL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", destL3.uuid) + ] + } + } + } + + /** + * b-3: gateway provided (no netmask), IP NOT in any CIDR. + * Expected: error. + */ + void testResolve_B3_ipNotInAnyCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-b3", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.0.50", destL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", destL3.uuid) + ] + } + } + } + + /** + * c-1: netmask provided (no gateway), netmask == CIDR netmask. + * Expected: success, gateway from CIDR. + */ + void testResolve_C1_netmaskMatchesCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-c1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.60", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.60" + assert nicVO.netmask == "255.255.255.0" + assert nicVO.gateway == "192.168.100.1" : "gateway should be inferred from L3 CIDR" + } + + /** + * c-4: netmask != CIDR, non-default & non-sole. + * VM has 2 NICs [flatL3_dest(default), flatL3_second]. + * Change flatL3_second NIC → flatL3_range_noDhcp (not default, vmNicCount=2). + * Expected: success, netmask=user input, gateway="". + */ + void testResolve_C4_netmaskMismatch_nonDefaultNonSole_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + L3NetworkInventory secondL3 = env.inventoryByName("flatL3_second") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-c4", [srcL3.uuid, secondL3.uuid], srcL3.uuid) + + int nicCount = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vm.uuid).count().intValue() + assert nicCount == 2 + + String defaultL3 = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, vm.uuid) + .findValue() + assert defaultL3 == srcL3.uuid + assert defaultL3 != destL3.uuid + + VmNicInventory nicOnSecond = vm.vmNics.find { it.l3NetworkUuid == secondL3.uuid } + assert nicOnSecond != null + + changeVmNicNetwork { + vmNicUuid = nicOnSecond.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.90", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.0.0", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(nicOnSecond.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.90" + assert nicVO.netmask == "255.255.0.0" : "netmask should be user input" + assert nicVO.gateway == "" || nicVO.gateway == null : "gateway should be empty" + } + + /** + * d-1: neither netmask nor gateway, IP in L3 CIDR. + * Expected: success, netmask+gateway from CIDR. + */ + void testResolve_D1_ipInCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-d1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.100", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.100" + assert nicVO.netmask == "255.255.255.0" : "netmask should be inferred from L3 CIDR" + assert nicVO.gateway == "192.168.100.1" : "gateway should be inferred from L3 CIDR" + } + + /** + * d-2: neither netmask nor gateway, IP NOT in any CIDR. + * Expected: error. + */ + void testResolve_D2_ipOutsideCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-d2", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.0.100", destL3.uuid) + ] + } + } + } + + /** + * d-3 (setVmStaticIp only): existing IP has params, new IP in old CIDR → reuse. + * This scenario is unique to APISetVmStaticIpMsg (existing IP reuse via ExistingIpContext). + */ + void testResolve_D3_setStaticIp_existingIpReuse_success() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-d3", l3.uuid) + + // First set a known IP with explicit netmask/gateway + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "192.168.100.110" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + + // Now change to a new IP in the same CIDR, without specifying netmask/gateway + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "192.168.100.111" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, l3.uuid) + .eq(UsedIpVO_.ip, "192.168.100.111") + .find() + assert usedIp != null + assert usedIp.netmask == "255.255.255.0" : "should reuse old netmask" + assert usedIp.gateway == "192.168.100.1" : "should reuse old gateway" + } } diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy index cd226cac406..3b1ae524137 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy @@ -17,7 +17,6 @@ import org.zstack.testlib.EnvSpec import org.zstack.testlib.SubCase import org.zstack.utils.data.SizeUnit import org.zstack.utils.gson.JSONObjectUtil -import org.zstack.utils.network.IPv6Constants /** * Test IP outside CIDR behavior for public networks. @@ -230,28 +229,6 @@ class PublicNetworkChangeVmIpOutsideCidrCase extends SubCase { VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) assert nicVO.ip == "10.0.2.50" - - // Also verify in-range IP works - VmInstanceInventory vm2 = createVmOnL3("vm-pub-range-noDhcp-inrange", l3.uuid) - List freeIps1 = getFreeIp { - l3NetworkUuid = l3.uuid - ipVersion = IPv6Constants.IPv4 - limit = 1 - } as List - String inRangeIp1 = freeIps1.get(0).getIp() - - setVmStaticIp { - vmInstanceUuid = vm2.uuid - l3NetworkUuid = l3.uuid - ip = inRangeIp1 - } - - UsedIpVO inRangeIp = Q.New(UsedIpVO.class) - .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) - .eq(UsedIpVO_.ip, inRangeIp1) - .find() - assert inRangeIp != null - assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" } /** @@ -348,28 +325,6 @@ class PublicNetworkChangeVmIpOutsideCidrCase extends SubCase { VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) assert nicVO.ip == "10.0.3.50" - - // Also verify in-range IP works - VmInstanceInventory vm2 = createVmOnL3("vm-pub-range-dhcp-inrange", l3.uuid) - List freeIps2 = getFreeIp { - l3NetworkUuid = l3.uuid - ipVersion = IPv6Constants.IPv4 - limit = 1 - } as List - String inRangeIp2 = freeIps2.get(0).getIp() - - setVmStaticIp { - vmInstanceUuid = vm2.uuid - l3NetworkUuid = l3.uuid - ip = inRangeIp2 - } - - UsedIpVO inRangeIp = Q.New(UsedIpVO.class) - .eq(UsedIpVO_.vmNicUuid, vm2.vmNics[0].uuid) - .eq(UsedIpVO_.ip, inRangeIp2) - .find() - assert inRangeIp != null - assert inRangeIp.ipRangeUuid != null : "ipRangeUuid should not be null for in-range IP" } /** 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 e84e33e7412..80d13d1a949 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -9874,6 +9874,10 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_COMPUTE_VM_10328 = "ORG_ZSTACK_COMPUTE_VM_10328"; + public static final String ORG_ZSTACK_COMPUTE_VM_10329 = "ORG_ZSTACK_COMPUTE_VM_10329"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10330 = "ORG_ZSTACK_COMPUTE_VM_10330"; + public static final String ORG_ZSTACK_IDENTITY_LOGIN_10000 = "ORG_ZSTACK_IDENTITY_LOGIN_10000"; public static final String ORG_ZSTACK_STORAGE_VOLUME_BLOCK_EXPON_10000 = "ORG_ZSTACK_STORAGE_VOLUME_BLOCK_EXPON_10000"; From 831fdf435f2ae8769d526e90456c96f9e2ce071f Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Mon, 23 Mar 2026 17:18:30 +0800 Subject: [PATCH 44/77] [test]: fix TagPatternResourceTypeCase CI failure - Set accountUuid on all dbf.persist() calls (OwnedByAccount requires it) - Replace Q.New() metamodel queries with SQL.New() HQL (metamodel NPE) ZSTAC-74908 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../TagPatternResourceTypeCase.groovy | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy b/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy index 9dfb24ac3f8..337ba29dfcd 100644 --- a/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy @@ -2,11 +2,10 @@ package org.zstack.test.integration.configuration.systemTag import org.zstack.core.Platform import org.zstack.core.db.DatabaseFacade -import org.zstack.core.db.Q import org.zstack.core.db.SQL +import org.zstack.header.identity.AccountConstant import org.zstack.header.tag.TagPatternType import org.zstack.header.tag.TagPatternVO -import org.zstack.header.tag.TagPatternVO_ import org.zstack.testlib.EnvSpec import org.zstack.testlib.SubCase @@ -57,6 +56,7 @@ class TagPatternResourceTypeCase extends SubCase { universal.setColor("red") universal.setType(TagPatternType.simple) universal.setResourceType(null) // null = universal + universal.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) dbf.persist(universal) // Verify it can be found without any resourceType filter @@ -66,11 +66,11 @@ class TagPatternResourceTypeCase extends SubCase { // Verify it appears in queries for any resource type // Simulating the filter: resourceType IS NULL OR resourceType = 'ZoneVO' - long count = Q.New(TagPatternVO.class) - .eq(TagPatternVO_.uuid, universal.getUuid()) - .isNull(TagPatternVO_.resourceType) - .count() - assert count == 1 + List results = SQL.New( + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType is null", + TagPatternVO.class + ).param("uuid", universal.getUuid()).list() + assert results.size() == 1 // Clean up dbf.removeByPrimaryKey(universal.getUuid(), TagPatternVO.class) @@ -89,6 +89,7 @@ class TagPatternResourceTypeCase extends SubCase { aiTag.setColor("blue") aiTag.setType(TagPatternType.simple) aiTag.setResourceType("ModelVO") + aiTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) dbf.persist(aiTag) TagPatternVO found = dbf.findByUuid(aiTag.getUuid(), TagPatternVO.class) @@ -96,18 +97,18 @@ class TagPatternResourceTypeCase extends SubCase { assert found.getResourceType() == "ModelVO" // Should be found when filtering for ModelVO - long modelCount = Q.New(TagPatternVO.class) - .eq(TagPatternVO_.uuid, aiTag.getUuid()) - .eq(TagPatternVO_.resourceType, "ModelVO") - .count() - assert modelCount == 1 + List modelResults = SQL.New( + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType = :resType", + TagPatternVO.class + ).param("uuid", aiTag.getUuid()).param("resType", "ModelVO").list() + assert modelResults.size() == 1 // Should NOT be found when filtering for VmInstanceVO - long vmCount = Q.New(TagPatternVO.class) - .eq(TagPatternVO_.uuid, aiTag.getUuid()) - .eq(TagPatternVO_.resourceType, "VmInstanceVO") - .count() - assert vmCount == 0 + List vmResults = SQL.New( + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType = :resType", + TagPatternVO.class + ).param("uuid", aiTag.getUuid()).param("resType", "VmInstanceVO").list() + assert vmResults.size() == 0 // Clean up dbf.removeByPrimaryKey(aiTag.getUuid(), TagPatternVO.class) @@ -131,6 +132,7 @@ class TagPatternResourceTypeCase extends SubCase { universal.setColor("green") universal.setType(TagPatternType.simple) universal.setResourceType(null) + universal.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) dbf.persist(universal) // Create an AI-scoped tag @@ -141,6 +143,7 @@ class TagPatternResourceTypeCase extends SubCase { aiTag.setColor("purple") aiTag.setType(TagPatternType.simple) aiTag.setResourceType("ModelVO") + aiTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) dbf.persist(aiTag) // Create a VM-scoped tag @@ -151,6 +154,7 @@ class TagPatternResourceTypeCase extends SubCase { vmTag.setColor("orange") vmTag.setType(TagPatternType.simple) vmTag.setResourceType("VmInstanceVO") + vmTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) dbf.persist(vmTag) // Query for VmInstanceVO page: should see universal + VM tag, NOT AI tag From d9f0d4de0cb5cfa1a340461433c7de776273873a Mon Sep 17 00:00:00 2001 From: J M Date: Tue, 24 Mar 2026 12:22:31 +0800 Subject: [PATCH 45/77] [storage]: return defensive copy from getPreferBackupStorageTypes Resolves: ZSTAC-80789 Change-Id: I2b2d3ce68970c094ab5ba681bcb726b037e0c327 --- .../src/main/java/org/zstack/expon/ExponStorageFactory.java | 3 ++- .../main/java/org/zstack/xinfini/XInfiniStorageFactory.java | 3 ++- .../main/java/org/zstack/storage/zbs/ZbsStorageFactory.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java index 4086058d198..db194c9a415 100644 --- a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java +++ b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java @@ -8,6 +8,7 @@ import org.zstack.header.volume.VolumeInventory; import org.zstack.header.volume.VolumeProtocol; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -48,7 +49,7 @@ public String getIdentity() { @Override public List getPreferBackupStorageTypes() { - return preferBackupStorageTypes; + return new ArrayList<>(preferBackupStorageTypes); } public void setPreferBackupStorageTypes(List preferBackupStorageTypes) { diff --git a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java index 70d33ea08d0..c3f98670bb6 100644 --- a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java +++ b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java @@ -11,6 +11,7 @@ import org.zstack.header.xinfini.XInfiniConstants; import org.zstack.storage.addon.primary.ExternalPrimaryStorageFactory; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -55,7 +56,7 @@ public String getIdentity() { @Override public List getPreferBackupStorageTypes() { - return preferBackupStorageTypes; + return new ArrayList<>(preferBackupStorageTypes); } public void setPreferBackupStorageTypes(List preferBackupStorageTypes) { diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java index 5b7c491814e..9df6cd4c97a 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java @@ -11,6 +11,7 @@ import org.zstack.utils.ssh.Ssh; import org.zstack.utils.ssh.SshResult; +import java.util.ArrayList; import java.util.List; import static org.zstack.core.Platform.operr; @@ -93,7 +94,7 @@ public void setPreferBackupStorageTypes(List preferBackupStorageTypes) { @Override public List getPreferBackupStorageTypes() { - return preferBackupStorageTypes; + return new ArrayList<>(preferBackupStorageTypes); } @Override From 6d177d44b2e1e8c7474a2f861c98d3bd682484df Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 24 Mar 2026 16:34:00 +0800 Subject: [PATCH 46/77] [schema]: move TagPatternVO.resourceType migration to V5.5.12 The ADD_COLUMN for TagPatternVO.resourceType was incorrectly placed in V5.5.6__schema.sql but should be in V5.5.12__schema.sql since the fix targets the 5.5.12 branch. Ref: ZSTAC-74908 Co-Authored-By: Claude Opus 4.6 (1M context) --- conf/db/upgrade/V5.5.12__schema.sql | 3 +++ conf/db/upgrade/V5.5.6__schema.sql | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql index b3443c65dfc..8b3b0d0d07e 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -183,3 +183,6 @@ DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('f1a6c2d85e7b8c9d3e4f5a6b7c8d9e0f','OvnVmInstance-DiskAllUsedCapacityInPercent','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','DiskAllUsedCapacityInPercent',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('b3c8e4f07a9d0e1f5a6b7c8d9e0f1a2b','OvnVmInstance-MemoryUsedInPercent','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','MemoryUsedInPercent',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('d5e0a6b29c1f2a3b7c8d9e0f1a2b3c4d','OvnVmInstance-CPUAverageUsedUtilization','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','CPUAverageUsedUtilization',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); + +-- ZSTAC-74908: Add resourceType to TagPatternVO to scope AI model tags away from VM pages +CALL ADD_COLUMN('TagPatternVO', 'resourceType', 'VARCHAR(128)', 1, NULL); diff --git a/conf/db/upgrade/V5.5.6__schema.sql b/conf/db/upgrade/V5.5.6__schema.sql index 2bbb7de1f7c..597ded44e52 100644 --- a/conf/db/upgrade/V5.5.6__schema.sql +++ b/conf/db/upgrade/V5.5.6__schema.sql @@ -229,6 +229,3 @@ SET g.`allocateStatus` = 'Unallocatable' WHERE p.`virtStatus` IN ('VFIO_MDEV_VIRTUALIZED', 'SRIOV_VIRTUALIZED') AND p.`vmInstanceUuid` IS NULL AND g.`allocateStatus` != 'Unallocatable'; - --- ZSTAC-74908: Add resourceType to TagPatternVO to scope AI model tags away from VM pages -CALL ADD_COLUMN('TagPatternVO', 'resourceType', 'VARCHAR(128)', 1, NULL); From 48e5b3eaeb5056bbd48ad7d5c43b22f4073b0400 Mon Sep 17 00:00:00 2001 From: "chao.he" Date: Thu, 26 Mar 2026 09:51:02 +0800 Subject: [PATCH 47/77] [sdk]: add force field to SetIAM2ProjectContainerClusterAction Resolves: ZSTAC-80406 Change-Id: I7d47639adccabbaa8a7446217ce2b2aef05d3c47 --- .../iam2/container/SetIAM2ProjectContainerClusterAction.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java b/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java index 0abbe00ba87..8ee944d7ba8 100644 --- a/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java +++ b/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java @@ -34,6 +34,9 @@ public Result throwExceptionIfError() { @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long clusterId; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean force; + @Param(required = false) public java.util.List systemTags; From 993afb1087fa6c5d28ac22a5da88b7c83d2de659 Mon Sep 17 00:00:00 2001 From: "shan.wu" Date: Mon, 23 Mar 2026 11:37:45 +0800 Subject: [PATCH 48/77] [dpu-bm2]: support dpu bm2 instance 1. add BareMetal2DpuChassisSpec for UT 2. Return baremetal2 IPMI information and dpu information through Query API, and desensitize the password 3. Some simple code optimization 4. support online resizing of volume of dpu baremetal2 instance Resolves/Related: ZSTAC-83415 Change-Id: I636d637a7168656a6c726c6769777a726e616974 --- .../zstack/header/volume/VolumeProtocol.java | 3 +- .../volume/VolumeProtocolCapability.java | 8 ++++ sdk/src/main/java/SourceClassMap.java | 2 + .../sdk/BareMetal2DpuChassisConfig.java | 39 +++++++++++++++++++ .../sdk/BareMetal2DpuChassisInventory.java | 19 ++++----- .../CloudOperationsErrorCode.java | 2 + 6 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java diff --git a/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java b/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java index 8cfebdf6080..aadc4cf15d3 100644 --- a/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java @@ -5,5 +5,6 @@ public enum VolumeProtocol { iSCSI, Vhost, CBD, - NBD + NBD, + RBD } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java b/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java index c0ce747a9bb..344badbe8a2 100644 --- a/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java @@ -65,4 +65,12 @@ public boolean isSupportReadonly() { public void setSupportReadonly(boolean supportReadonly) { this.supportReadonly = supportReadonly; } + + public String getProtocol() { + return protocol; + } + + public String getHypervisor() { + return hypervisor; + } } diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 614350ecd69..ab5ec3770f3 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -66,6 +66,7 @@ public class SourceClassMap { put("org.zstack.baremetal2.chassis.BareMetal2ChassisInventory", "org.zstack.sdk.BareMetal2ChassisInventory"); put("org.zstack.baremetal2.chassis.BareMetal2ChassisNicInventory", "org.zstack.sdk.BareMetal2ChassisNicInventory"); put("org.zstack.baremetal2.chassis.BareMetal2ChassisPciDeviceInventory", "org.zstack.sdk.BareMetal2ChassisPciDeviceInventory"); + put("org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisConfig", "org.zstack.sdk.BareMetal2DpuChassisConfig"); put("org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisInventory", "org.zstack.sdk.BareMetal2DpuChassisInventory"); put("org.zstack.baremetal2.chassis.ipmi.BareMetal2IpmiChassisInventory", "org.zstack.sdk.BareMetal2IpmiChassisInventory"); put("org.zstack.baremetal2.configuration.BareMetal2ChassisOfferingInventory", "org.zstack.sdk.BareMetal2ChassisOfferingInventory"); @@ -960,6 +961,7 @@ public class SourceClassMap { put("org.zstack.sdk.BareMetal2ChassisNicInventory", "org.zstack.baremetal2.chassis.BareMetal2ChassisNicInventory"); put("org.zstack.sdk.BareMetal2ChassisOfferingInventory", "org.zstack.baremetal2.configuration.BareMetal2ChassisOfferingInventory"); put("org.zstack.sdk.BareMetal2ChassisPciDeviceInventory", "org.zstack.baremetal2.chassis.BareMetal2ChassisPciDeviceInventory"); + put("org.zstack.sdk.BareMetal2DpuChassisConfig", "org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisConfig"); put("org.zstack.sdk.BareMetal2DpuChassisInventory", "org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisInventory"); put("org.zstack.sdk.BareMetal2DpuHostInventory", "org.zstack.baremetal2.dpu.BareMetal2DpuHostInventory"); put("org.zstack.sdk.BareMetal2GatewayInventory", "org.zstack.baremetal2.gateway.BareMetal2GatewayInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java new file mode 100644 index 00000000000..a4c6b03fd08 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java @@ -0,0 +1,39 @@ +package org.zstack.sdk; + + + +public class BareMetal2DpuChassisConfig { + + public java.lang.String ipmiAddress; + public void setIpmiAddress(java.lang.String ipmiAddress) { + this.ipmiAddress = ipmiAddress; + } + public java.lang.String getIpmiAddress() { + return this.ipmiAddress; + } + + public int ipmiPort; + public void setIpmiPort(int ipmiPort) { + this.ipmiPort = ipmiPort; + } + public int getIpmiPort() { + return this.ipmiPort; + } + + public java.lang.String ipmiUsername; + public void setIpmiUsername(java.lang.String ipmiUsername) { + this.ipmiUsername = ipmiUsername; + } + public java.lang.String getIpmiUsername() { + return this.ipmiUsername; + } + + public java.lang.String ipmiPassword; + public void setIpmiPassword(java.lang.String ipmiPassword) { + this.ipmiPassword = ipmiPassword; + } + public java.lang.String getIpmiPassword() { + return this.ipmiPassword; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java index a68225a7c45..0eaffdc2162 100644 --- a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java @@ -1,23 +1,24 @@ package org.zstack.sdk; - +import org.zstack.sdk.BareMetal2DpuChassisConfig; +import org.zstack.sdk.BareMetal2DpuHostInventory; public class BareMetal2DpuChassisInventory extends org.zstack.sdk.BareMetal2ChassisInventory { - public java.lang.String config; - public void setConfig(java.lang.String config) { + public BareMetal2DpuChassisConfig config; + public void setConfig(BareMetal2DpuChassisConfig config) { this.config = config; } - public java.lang.String getConfig() { + public BareMetal2DpuChassisConfig getConfig() { return this.config; } - public java.lang.String hostUuid; - public void setHostUuid(java.lang.String hostUuid) { - this.hostUuid = hostUuid; + public BareMetal2DpuHostInventory dpuHost; + public void setDpuHost(BareMetal2DpuHostInventory dpuHost) { + this.dpuHost = dpuHost; } - public java.lang.String getHostUuid() { - return this.hostUuid; + public BareMetal2DpuHostInventory getDpuHost() { + return this.dpuHost; } } 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..4abe4832481 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -5490,6 +5490,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_DPU_10001 = "ORG_ZSTACK_BAREMETAL2_DPU_10001"; + public static final String ORG_ZSTACK_BAREMETAL2_DPU_10002 = "ORG_ZSTACK_BAREMETAL2_DPU_10002"; + public static final String ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10000 = "ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10000"; public static final String ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10001 = "ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10001"; From fa7105532f5ccd5b2037b46dc4319ac40a017d9a Mon Sep 17 00:00:00 2001 From: qiuyu <2094801894@qq.com> Date: Mon, 30 Mar 2026 13:39:26 +0800 Subject: [PATCH 49/77] [conf]: MySQL timeout is not defined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL timeout is not defined,when MySQL stop, the heartbeat and zstack-hamon hang. Resolves: ZSTAC-83499 Change-Id: I746a6f696e66696873646c7161766563646f7378 --- conf/springConfigXml/DatabaseFacade.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conf/springConfigXml/DatabaseFacade.xml b/conf/springConfigXml/DatabaseFacade.xml index 97b03278a02..d7afee98688 100755 --- a/conf/springConfigXml/DatabaseFacade.xml +++ b/conf/springConfigXml/DatabaseFacade.xml @@ -52,6 +52,7 @@ + sync - true + true 50 From 78389bb2fb599fc6528a2377eb0051525e3a12bf Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Mon, 30 Mar 2026 17:46:52 +0800 Subject: [PATCH 50/77] [compute]: handle empty string dstHostUuid in host allocator When performing "change host and primary storage" without specifying a target host, the frontend sends dstHostUuid="" (empty string) instead of null. 1. Why is this change necessary? DesignatedHostAllocatorFlow only checked hostUuid for null, not empty string. Empty string caused SQL to generate "h.uuid = ''" which matched no hosts, making all storage migrations without a specified host fail. 2. How does it address the problem? Normalize empty string hostUuid, zoneUuid, and clusterUuids to null in DesignatedHostAllocatorFlow.allocate() before any filtering logic. This treats empty string as "not specified", which is the correct semantic. Also fix a pre-existing NPE in the error-message path where clusterUuids.isEmpty() was called without null check. 3. Are there any side effects? None. A valid UUID is never an empty string. All existing callers passing null or valid UUIDs are unaffected. # Summary of changes (by module): - compute: normalize empty strings to null for hostUuid, zoneUuid, and clusterUuids in DesignatedHostAllocatorFlow.allocate(); fix NPE in error-message path Related: ZSTAC-83733 Change-Id: Ib669b3f7087e9fcda7f5dd5aa6362c5e653d803d --- .../DesignatedHostAllocatorFlow.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java index 72386b3dd3e..f6d721ebb8c 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java @@ -1,6 +1,7 @@ package org.zstack.compute.allocator; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; @@ -81,15 +82,29 @@ public void allocate() { List clusterUuids = (List) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.cluster); String hostUuid = (String) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.host); - if (zoneUuid == null && CollectionUtils.isEmpty(clusterUuids) && hostUuid == null && spec.getHypervisorType() == null) { + String hypervisorType = spec.getHypervisorType(); + + // normalize empty strings to null — treat empty string as "not specified" + zoneUuid = StringUtils.isEmpty(zoneUuid) ? null : zoneUuid; + hostUuid = StringUtils.isEmpty(hostUuid) ? null : hostUuid; + hypervisorType = StringUtils.isEmpty(hypervisorType) ? null : hypervisorType; + if (!CollectionUtils.isEmpty(clusterUuids)) { + clusterUuids = new ArrayList<>(clusterUuids); + clusterUuids.removeIf(s -> s == null || s.isEmpty()); + if (clusterUuids.isEmpty()) { + clusterUuids = null; + } + } + + if (zoneUuid == null && CollectionUtils.isEmpty(clusterUuids) && hostUuid == null && hypervisorType == null) { next(candidates); return; } if (amITheFirstFlow()) { - candidates = allocate(zoneUuid, clusterUuids, hostUuid, spec.getHypervisorType()); + candidates = allocate(zoneUuid, clusterUuids, hostUuid, hypervisorType); } else { - candidates = allocate(candidates, zoneUuid, clusterUuids, hostUuid, spec.getHypervisorType()); + candidates = allocate(candidates, zoneUuid, clusterUuids, hostUuid, hypervisorType); } if (candidates.isEmpty()) { @@ -97,14 +112,14 @@ public void allocate() { if (zoneUuid != null) { args.append(String.format("zoneUuid=%s", zoneUuid)).append(" "); } - if (!clusterUuids.isEmpty()) { + if (!CollectionUtils.isEmpty(clusterUuids)) { args.append(String.format("clusterUuid in %s", clusterUuids)).append(" "); } if (hostUuid != null) { args.append(String.format("hostUuid=%s", hostUuid)).append(" "); } - if (spec.getHypervisorType() != null) { - args.append(String.format("hypervisorType=%s", spec.getHypervisorType())).append(" "); + if (hypervisorType != null) { + args.append(String.format("hypervisorType=%s", hypervisorType)).append(" "); } fail(Platform.operr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10036, "No host with %s found", args)); } else { From ee3f541737c297e157a4135425522f635352cb1c Mon Sep 17 00:00:00 2001 From: "chao.he" Date: Wed, 17 Dec 2025 16:13:06 +0800 Subject: [PATCH 51/77] [plugin-premium]: Reparing GPU/VM page keeps loading when shutting down or encountering errors in Zaku cluster. Resolves: ZSTAC-80202 Change-Id: I7778676171646874706164777869707279776172 --- .../zql/BeforeCallZWatchExtensionPoint.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java diff --git a/header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java b/header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java new file mode 100644 index 00000000000..d0ec90621d6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java @@ -0,0 +1,23 @@ +package org.zstack.header.zql; + +import java.util.List; + +/** + * BeforeCallZWatchExtensionPoint is an extension point that allows plugins + * to perform custom operations before calling zwatch. + */ +public interface BeforeCallZWatchExtensionPoint { + /** + * Check if this extension supports the given VO class + * @param voClass the VO class to check + * @return true if this extension supports the VO class, false otherwise + */ + boolean supports(Class voClass); + + /** + * Perform custom operations before calling ZWatch, for example: health-check + * @param voClass the VO class type + * @param uuids the list of resource UUIDs to process + */ + void beforeCallZWatch(Class voClass, List uuids); +} From 3c8bc53956b31c9126ae92da1b5e6390b877f6fe Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Tue, 31 Mar 2026 13:08:16 +0800 Subject: [PATCH 52/77] [pci-device]: add SDK for UpdateVmInstancePciDeviceSpecRef API Resolves: ZSTAC-71156 Change-Id: I55390356c284a763f80253b81eb63055e031db77 --- ...pdateVmInstancePciDeviceSpecRefAction.java | 107 ++++++++++++++++++ ...pdateVmInstancePciDeviceSpecRefResult.java | 14 +++ 2 files changed, 121 insertions(+) create mode 100644 sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java new file mode 100644 index 00000000000..8def583403f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateVmInstancePciDeviceSpecRefAction 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.UpdateVmInstancePciDeviceSpecRefResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String pciSpecUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, numberRange = {1L,100L}, noTrim = false) + public int pciDeviceNumber = 0; + + @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.UpdateVmInstancePciDeviceSpecRefResult value = res.getResult(org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefResult.class); + ret.value = value == null ? new org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefResult() : 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 = "PUT"; + info.path = "/pci-device-specs/{pciSpecUuid}/vm-instances/{vmInstanceUuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java new file mode 100644 index 00000000000..cb771fa0a73 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmInstancePciDeviceSpecRefInventory; + +public class UpdateVmInstancePciDeviceSpecRefResult { + public VmInstancePciDeviceSpecRefInventory inventory; + public void setInventory(VmInstancePciDeviceSpecRefInventory inventory) { + this.inventory = inventory; + } + public VmInstancePciDeviceSpecRefInventory getInventory() { + return this.inventory; + } + +} From ee4b6bea2b7c38565f72e44d400318e2a0edfbe4 Mon Sep 17 00:00:00 2001 From: "tian.huang" Date: Fri, 27 Mar 2026 14:32:41 +0800 Subject: [PATCH 53/77] [compute]: fix VM clone quota check fail 1.Clone a tenant VM using admin account, quota check didn't base on current role. 2.quota error is overwritten during exception handling. http://jira.zstack.io/browse/ZSTAC-83646 Resolves: ZSTAC-83646 Change-Id: I776c6b7a786d79746f616b716f736f646e686868 --- .../compute/allocator/QuotaAllocatorFlow.java | 18 ++++- .../zstack/compute/vm/VmQuotaOperator.java | 71 +++++++++++++++---- .../header/allocator/AllocateHostMsg.java | 5 ++ .../header/allocator/HostAllocatorSpec.java | 8 +++ .../java/org/zstack/identity/QuotaUtil.java | 15 +++- 5 files changed, 97 insertions(+), 20 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java index 4d9d469c796..999e797a8da 100644 --- a/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java @@ -4,6 +4,8 @@ import org.springframework.beans.factory.annotation.Configurable; import org.zstack.compute.vm.VmQuotaOperator; import org.zstack.header.allocator.AbstractHostAllocatorFlow; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.identity.AccountConstant; import org.zstack.identity.Account; import org.zstack.identity.QuotaUtil; @@ -51,6 +53,12 @@ public void allocate() { } throwExceptionIfIAmTheFirstFlow(); + // skip checkquota if the operator is admin + String currentAccountUuid = spec.getAccountUuid(); + if (currentAccountUuid != null && AccountConstant.isAdminPermission(currentAccountUuid)) { + next(candidates); + return; + } final String vmInstanceUuid = spec.getVmInstance().getUuid(); final String accountUuid = Account.getAccountUuidOfResource(vmInstanceUuid); @@ -60,21 +68,27 @@ public void allocate() { } if (!spec.isFullAllocate()) { - new VmQuotaOperator().checkVmCupAndMemoryCapacity(accountUuid, + ErrorCode error = new VmQuotaOperator().checkVmCupAndMemoryCapacityWithResult(accountUuid, accountUuid, spec.getCpuCapacity(), spec.getMemoryCapacity(), new QuotaUtil().makeQuotaPairs(accountUuid)); + if (error != null) { + throw new OperationFailureException(error); + } next(candidates); return; } - new VmQuotaOperator().checkVmInstanceQuota( + ErrorCode error = new VmQuotaOperator().checkVmInstanceQuotaWithResult( accountUuid, accountUuid, vmInstanceUuid, new QuotaUtil().makeQuotaPairs(accountUuid)); + if (error != null) { + throw new OperationFailureException(error); + } next(candidates); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java b/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java index bb63bbc9991..b8aa803644a 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java @@ -9,6 +9,7 @@ import org.zstack.core.db.SimpleQuery; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.apimediator.ApiMessageInterceptionException; +import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.identity.APIChangeResourceOwnerMsg; import org.zstack.header.identity.AccountType; import org.zstack.header.identity.Quota; @@ -151,12 +152,27 @@ private void checkTotalVMQuota(String currentAccountUuid, String vmInstanceUuid, long totalVmNumQuota, long totalVmNum) { + ErrorCode error = checkTotalVMQuotaWithResult(currentAccountUuid, + resourceTargetOwnerAccountUuid, + vmInstanceUuid, + totalVmNumQuota, + totalVmNum); + if (error != null) { + throw new ApiMessageInterceptionException(error); + } + } + + private ErrorCode checkTotalVMQuotaWithResult(String currentAccountUuid, + String resourceTargetOwnerAccountUuid, + String vmInstanceUuid, + long totalVmNumQuota, + long totalVmNum) { if (Q.New(VmInstanceVO.class) .eq(VmInstanceVO_.uuid, vmInstanceUuid) .notNull(VmInstanceVO_.lastHostUuid) .isExists()) { // Dirty hack - VM with last host UUID means existing VM. - return; + return null; } QuotaUtil.QuotaCompareInfo quotaCompareInfo; @@ -167,18 +183,17 @@ private void checkTotalVMQuota(String currentAccountUuid, quotaCompareInfo.quotaValue = totalVmNumQuota; quotaCompareInfo.currentUsed = totalVmNum; quotaCompareInfo.request = 1; - new QuotaUtil().CheckQuota(quotaCompareInfo); + return new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); } @Transactional(readOnly = true) - public void checkVmInstanceQuota(String currentAccountUuid, - String resourceTargetOwnerAccountUuid, - String vmInstanceUuid, - Map pairs) { + public ErrorCode checkVmInstanceQuotaWithResult(String currentAccountUuid, + String resourceTargetOwnerAccountUuid, + String vmInstanceUuid, + Map pairs) { long vmNumQuota = pairs.get(VmQuotaConstant.VM_RUNNING_NUM).getValue(); VmQuotaUtil.VmQuota vmQuotaUsed = new VmQuotaUtil().getUsedVmCpuMemory(resourceTargetOwnerAccountUuid, null); - // { QuotaUtil.QuotaCompareInfo quotaCompareInfo; quotaCompareInfo = new QuotaUtil.QuotaCompareInfo(); @@ -188,22 +203,38 @@ public void checkVmInstanceQuota(String currentAccountUuid, quotaCompareInfo.quotaValue = vmNumQuota; quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmNum; quotaCompareInfo.request = 1; - new QuotaUtil().CheckQuota(quotaCompareInfo); + ErrorCode error = new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); + if (error != null) { + return error; + } } - // - checkTotalVMQuota(currentAccountUuid, + + ErrorCode error = checkTotalVMQuotaWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, vmInstanceUuid, pairs.get(VmQuotaConstant.VM_TOTAL_NUM).getValue(), vmQuotaUsed.totalVmNum); - // + if (error != null) { + return error; + } + VmInstanceVO vm = dbf.getEntityManager().find(VmInstanceVO.class, vmInstanceUuid); - checkVmCupAndMemoryCapacity(currentAccountUuid, resourceTargetOwnerAccountUuid, vm.getCpuNum(), vm.getMemorySize(), pairs); + return checkVmCupAndMemoryCapacityWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, vm.getCpuNum(), vm.getMemorySize(), pairs); + } + + public void checkVmInstanceQuota(String currentAccountUuid, + String resourceTargetOwnerAccountUuid, + String vmInstanceUuid, + Map pairs) { + ErrorCode error = checkVmInstanceQuotaWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, vmInstanceUuid, pairs); + if (error != null) { + throw new ApiMessageInterceptionException(error); + } } @Transactional(readOnly = true) - public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resourceTargetOwnerAccountUuid, long cpu, long memory, Map pairs) { + public ErrorCode checkVmCupAndMemoryCapacityWithResult(String currentAccountUuid, String resourceTargetOwnerAccountUuid, long cpu, long memory, Map pairs) { VmQuotaUtil.VmQuota vmQuotaUsed = new VmQuotaUtil().getUsedVmCpuMemory(resourceTargetOwnerAccountUuid); long cpuNumQuota = pairs.get(VmQuotaConstant.VM_RUNNING_CPU_NUM).getValue(); long memoryQuota = pairs.get(VmQuotaConstant.VM_RUNNING_MEMORY_SIZE).getValue(); @@ -217,7 +248,10 @@ public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resour quotaCompareInfo.quotaValue = cpuNumQuota; quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmCpuNum; quotaCompareInfo.request = cpu; - new QuotaUtil().CheckQuota(quotaCompareInfo); + ErrorCode error = new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); + if (error != null) { + return error; + } } { QuotaUtil.QuotaCompareInfo quotaCompareInfo; @@ -228,7 +262,14 @@ public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resour quotaCompareInfo.quotaValue = memoryQuota; quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmMemorySize; quotaCompareInfo.request = memory; - new QuotaUtil().CheckQuota(quotaCompareInfo); + return new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); + } + } + + public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resourceTargetOwnerAccountUuid, long cpu, long memory, Map pairs) { + ErrorCode error = checkVmCupAndMemoryCapacityWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, cpu, memory, pairs); + if (error != null) { + throw new ApiMessageInterceptionException(error); } } diff --git a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java index 23869aa1327..11f911dff0b 100755 --- a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java +++ b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java @@ -31,6 +31,7 @@ public class AllocateHostMsg extends NeedReplyMessage { private long oldMemoryCapacity = 0; private AllocationScene allocationScene; private String architecture; + private String accountUuid; public List> getOptionalPrimaryStorageUuids() { return optionalPrimaryStorageUuids; @@ -211,4 +212,8 @@ public String getArchitecture() { public void setArchitecture(String architecture) { this.architecture = architecture; } + + public String getAccountUuid() { return accountUuid; } + + public void setAccountUuid(String accountUuid) { this.accountUuid = accountUuid; } } diff --git a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java index c8d8ddc3af8..e0ed69c47a1 100755 --- a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java +++ b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java @@ -37,6 +37,7 @@ public class HostAllocatorSpec { private long oldMemoryCapacity = 0; private AllocationScene allocationScene; private String architecture; + private String accountUuid; public AllocationScene getAllocationScene() { return allocationScene; @@ -161,7 +162,13 @@ public List getL3NetworkUuids() { } return l3NetworkUuids; } + public String getAccountUuid() { + return accountUuid; + } + public void setAccountUuid(String accountUuid) { + this.accountUuid = accountUuid; + } public void setL3NetworkUuids(List l3NetworkUuids) { this.l3NetworkUuids = l3NetworkUuids; } @@ -250,6 +257,7 @@ public static HostAllocatorSpec fromAllocationMsg(AllocateHostMsg msg) { msg.getOptionalPrimaryStorageUuids().forEach(spec::addOptionalPrimaryStorageUuids); spec.setAllocationScene(msg.getAllocationScene()); spec.setArchitecture(msg.getArchitecture()); + spec.setAccountUuid(msg.getAccountUuid()); if (msg.getSystemTags() != null && !msg.getSystemTags().isEmpty()){ spec.setSystemTags(new ArrayList(msg.getSystemTags())); } diff --git a/identity/src/main/java/org/zstack/identity/QuotaUtil.java b/identity/src/main/java/org/zstack/identity/QuotaUtil.java index d7b29015624..34fce1b5f80 100644 --- a/identity/src/main/java/org/zstack/identity/QuotaUtil.java +++ b/identity/src/main/java/org/zstack/identity/QuotaUtil.java @@ -72,7 +72,7 @@ public String getResourceOwnerAccountUuid(String resourceUuid) { } @Transactional(readOnly = true) - public void CheckQuota(QuotaCompareInfo quotaCompareInfo) { + public ErrorCode checkQuotaAndReturn(QuotaCompareInfo quotaCompareInfo) { logger.trace(String.format("dump quota QuotaCompareInfo: \n %s", JSONObjectUtil.toJsonString(quotaCompareInfo))); String accountName = Q.New(AccountVO.class) @@ -80,14 +80,23 @@ public void CheckQuota(QuotaCompareInfo quotaCompareInfo) { .eq(AccountVO_.uuid, quotaCompareInfo.resourceTargetOwnerAccountUuid) .findValue(); if (quotaCompareInfo.currentUsed + quotaCompareInfo.request > quotaCompareInfo.quotaValue) { - throw new ApiMessageInterceptionException(err(ORG_ZSTACK_IDENTITY_10002, IdentityErrors.QUOTA_EXCEEDING, + return err(ORG_ZSTACK_IDENTITY_10002, IdentityErrors.QUOTA_EXCEEDING, "quota exceeding." + "The resource owner(or target resource owner) account[uuid: %s name: %s] exceeds a quota[name: %s, value: %s], " + "Current used:%s, Request:%s. Please contact the administrator.", quotaCompareInfo.resourceTargetOwnerAccountUuid, StringUtils.trimToEmpty(accountName), quotaCompareInfo.quotaName, quotaCompareInfo.quotaValue, quotaCompareInfo.currentUsed, quotaCompareInfo.request - )); + ); + } + + return null; + } + + public void CheckQuota(QuotaCompareInfo quotaCompareInfo) { + ErrorCode error = checkQuotaAndReturn(quotaCompareInfo); + if (error != null) { + throw new ApiMessageInterceptionException(error); } } From 7ea5aada141171cc4351a3ba3c9b88936ef95b4e Mon Sep 17 00:00:00 2001 From: Jiajun Xu Date: Wed, 1 Apr 2026 13:52:21 +0800 Subject: [PATCH 54/77] [kvm]: pass guestOsType to kvmagent for SMBIOS auto-configuration Resolves: ZSTAC-81735 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/main/java/org/zstack/kvm/KVMAgentCommands.java | 10 ++++++++++ plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 1 + 2 files changed, 11 insertions(+) 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 2a36bb5aba3..f19e680605c 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2283,6 +2283,8 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private boolean isApplianceVm; @GrayVersion(value = "5.0.0") private String systemSerialNumber; + @GrayVersion(value = "5.5.12") + private String guestOsType; @GrayVersion(value = "5.0.0") private String bootMode; // used when bootMode == 'UEFI' @@ -2474,6 +2476,14 @@ public void setSystemSerialNumber(String systemSerialNumber) { this.systemSerialNumber = systemSerialNumber; } + public String getGuestOsType() { + return guestOsType; + } + + public void setGuestOsType(String guestOsType) { + this.guestOsType = guestOsType; + } + public String getVmCpuModel() { return vmCpuModel; } 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 b8b38d1b802..4b0977d03b7 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -4435,6 +4435,7 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi cmd.setAdditionalQmp(VmGlobalConfig.ADDITIONAL_QMP.value(Boolean.class)); cmd.setApplianceVm(spec.getVmInventory().getType().equals("ApplianceVm")); cmd.setSystemSerialNumber(makeAndSaveVmSystemSerialNumber(spec.getVmInventory().getUuid())); + cmd.setGuestOsType(spec.getVmInventory().getGuestOsType()); if (!NetworkGlobalProperty.CHASSIS_ASSET_TAG.isEmpty()) { cmd.setChassisAssetTag(NetworkGlobalProperty.CHASSIS_ASSET_TAG); } From 4f895c3e480889ffd8627fa0c4baf8c37093237e Mon Sep 17 00:00:00 2001 From: J M Date: Thu, 2 Apr 2026 13:38:23 +0800 Subject: [PATCH 55/77] [testlib]: make Python SDK template compatible with Python 3 1. Replace print statements with print() function calls 2. Remove deprecated 'import sha' (hashlib.sha1 already imported) 3. Handle 'long' type removed in Python 3 with module-level try/except 4. Encode hmac/base64 arguments to bytes for Python 3 5. Fix urllib3 typo in error message Resolves: ZSTAC-83925 Change-Id: I6e746775686f6e64646b627a6f68706f77736f73 --- testlib/src/main/resources/zssdk.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/testlib/src/main/resources/zssdk.py b/testlib/src/main/resources/zssdk.py index e82ddde0820..a188e387746 100755 --- a/testlib/src/main/resources/zssdk.py +++ b/testlib/src/main/resources/zssdk.py @@ -4,7 +4,7 @@ try: import urllib3 except ImportError: - print 'urlib3 is not installed, run "pip install urlib3"' + print('urllib3 is not installed, run "pip install urllib3"') sys.exit(1) import string @@ -16,11 +16,15 @@ import traceback import base64 import hmac -import sha from hashlib import sha1 import datetime import time +try: + int_types = (int, long) +except NameError: + int_types = (int,) + CONFIG_HOSTNAME = 'hostname' CONFIG_PORT = 'port' CONFIG_POLLING_TIMEOUT = 'default_polling_timeout' @@ -55,7 +59,7 @@ def wrap(*args, **kwargs): try: func(*args, **kwargs) except: - print traceback.format_exc() + print(traceback.format_exc()) return wrap @@ -189,7 +193,7 @@ def _check_params(self): if value is not None and isinstance(value, str) and annotation.empty_string is False and len(value) == 0: raise SdkError('invalid parameter[%s], it cannot be an empty string' % param_name) - if value is not None and (isinstance(value, int) or isinstance(value, long)) \ + if value is not None and isinstance(value, int_types) \ and annotation.number_range is not None and len(annotation.number_range) == 2: low = annotation.number_range[0] high = annotation.number_range[1] @@ -253,10 +257,14 @@ def calculateAccessKey(self, url, date): path = elements[2].split("/", 2) path = path[2].split("?") - h = hmac.new(self.accessKeySecret, self.HTTP_METHOD + "\n" - + date + "\n" - + "/" + path[0], sha1) - Signature = base64.b64encode(h.digest()) + msg = self.HTTP_METHOD + "\n" + date + "\n" + "/" + path[0] + if isinstance(msg, str): + msg = msg.encode('utf-8') + secret = self.accessKeySecret + if isinstance(secret, str): + secret = secret.encode('utf-8') + h = hmac.new(secret, msg, sha1) + Signature = base64.b64encode(h.digest()).decode('utf-8') return "ZStack %s:%s" % (self.accessKeyId, Signature) def call(self, cb=None): @@ -482,13 +490,13 @@ def _json_http( if body is not None and not isinstance(body, str): body = json.dumps(body).encode('utf-8') - print '[Request]: %s url=%s, headers=%s, body=%s' % (method, uri, headers, body) + print('[Request]: %s url=%s, headers=%s, body=%s' % (method, uri, headers, body)) if body: headers['Content-Length'] = len(body) rsp = pool.request(method, uri, body=body, headers=headers) else: rsp = pool.request(method, uri, headers=headers) - print '[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, rsp.data) + print('[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, rsp.data)) return rsp From 61174c8ec67c08e756e6e444c48e72b6c6a295a2 Mon Sep 17 00:00:00 2001 From: Jiajun Xu Date: Mon, 30 Mar 2026 18:38:03 +0800 Subject: [PATCH 56/77] fix(kvm): disable PMU on aarch64 for Kunpeng-920 panic On Kunpeng-920 V200 7270Z/5230Z, openEuler 24.03 guest kernel panics during boot because __armv8pmu_probe_pmu reads PMMIR_EL1 (PMUv3.4 register) which the host KVM (Kylin 4.19) does not handle, causing kvm_inject_undefined and guest kernel panic. Add vm.pmu GlobalConfig (ResourceConfig, VM/Cluster level) defaulting to false. On aarch64, read this config and set in domain XML. Users can re-enable via ResourceConfig if needed. Resolves: ZSTAC-76375 Change-Id: I6096bf5215fd4ff0b3170b9d299e38c6 --- .../org/zstack/compute/vm/VmGlobalConfig.java | 4 + .../java/org/zstack/kvm/KVMAgentCommands.java | 10 + .../src/main/java/org/zstack/kvm/KVMHost.java | 9 + .../integration/kvm/vm/VmPmuConfigCase.groovy | 194 ++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy diff --git a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java index dc39bce94fe..5f18eb7fd3f 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -52,6 +52,10 @@ public class VmGlobalConfig { @GlobalConfigValidation(validValues = {"true", "false"}) @BindResourceConfig(value = {VmInstanceVO.class}) public static GlobalConfig VM_PORT_OFF = new GlobalConfig(CATEGORY, "vmPortOff"); + @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "enable PMU for VM, disabled by default on aarch64 to avoid Kunpeng-920 kernel panic") + @GlobalConfigValidation(validValues = {"true", "false"}) + @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) + public static GlobalConfig VM_PMU = new GlobalConfig(CATEGORY, "vm.pmu"); @GlobalConfigValidation(validValues = {"true", "false"}) @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) public static GlobalConfig EMULATE_HYPERV = new GlobalConfig(CATEGORY, "emulateHyperV"); 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 2a36bb5aba3..36c5d35ecde 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2312,6 +2312,8 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private boolean consoleLogToFile; @GrayVersion(value = "5.0.0") private boolean acpi; + @GrayVersion(value = "5.5.12") + private boolean pmu = true; @GrayVersion(value = "5.0.0") private boolean x2apic = true; // cpuid hypervisor feature @@ -2835,6 +2837,14 @@ public void setAcpi(boolean acpi) { this.acpi = acpi; } + public boolean isPmu() { + return pmu; + } + + public void setPmu(boolean pmu) { + this.pmu = pmu; + } + public boolean getX2apic() { return x2apic; } 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 b8b38d1b802..96c2bd7bfa5 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -66,6 +66,7 @@ import org.zstack.header.message.MessageReply; import org.zstack.header.message.NeedReplyMessage; import org.zstack.header.network.l2.*; +import org.zstack.header.os.OSArchitecture; import org.zstack.header.network.l3.L3NetworkInventory; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.JsonAsyncRESTCallback; @@ -4573,6 +4574,14 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi cmd.setCreatePaused(true); } cmd.setAcpi(true); + // aarch64: disable PMU by default to avoid kernel panic on new Kunpeng-920 (7270Z/5230Z) + // where PMMIR_EL1 register is not supported by KVM. See ZSTAC-76375 + // GlobalConfig vm.pmu defaults to false; users can re-enable via ResourceConfig. + if (OSArchitecture.AARCH64.normalizedArchName().equals(architecture)) { + Boolean pmuEnabled = rcf.getResourceConfigValue( + VmGlobalConfig.VM_PMU, spec.getVmInventory().getUuid(), Boolean.class); + cmd.setPmu(Boolean.TRUE.equals(pmuEnabled)); + } GuestOsCharacter.Config config = GuestOsHelper.getInstance().getGuestOsCharacter( spec.getVmInventory().getArchitecture(), diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy new file mode 100644 index 00000000000..5fa2bd94001 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy @@ -0,0 +1,194 @@ +package org.zstack.test.integration.kvm.vm + +import org.springframework.http.HttpEntity +import org.zstack.compute.vm.VmGlobalConfig +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.sdk.GlobalConfigInventory +import org.zstack.sdk.VmInstanceInventory +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +/** + * Test VM PMU configuration. + * See ZSTAC-76375: Kunpeng-920 7270Z kernel panic due to PMMIR_EL1. + */ +class VmPmuConfigCase extends SubCase { + EnvSpec env + + @Override + void clean() { + env.delete() + } + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(2) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "localhost" + username = "root" + password = "password" + } + + attachPrimaryStorage("local") + attachL2Network("l2") + } + + attachBackupStorage("sftp") + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.100" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + } + } + } + + @Override + void test() { + env.create() + testPmuGlobalConfigExists() + testPmuDefaultOnX86() + testPmuResourceConfigOverride() + } + + void testPmuGlobalConfigExists() { + def configs = queryGlobalConfig { + conditions = ["category=${VmGlobalConfig.CATEGORY}", "name=${VmGlobalConfig.VM_PMU.name}"] + } + + assert configs.size() == 1 : "vm.pmu GlobalConfig should exist" + def config = configs[0] as GlobalConfigInventory + assert config.defaultValue == "false" : "vm.pmu should default to false" + } + + void testPmuDefaultOnX86() { + def image = env.inventoryByName("image1") + def l3 = env.inventoryByName("l3") + def instance = env.inventoryByName("instanceOffering") + + KVMAgentCommands.StartVmCmd startCmd = null + env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { KVMAgentCommands.StartVmResponse rsp, HttpEntity e -> + startCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) + return rsp + } + + def vm = createVmInstance { + name = "test-pmu-x86" + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + instanceOfferingUuid = instance.uuid + } as VmInstanceInventory + + assert startCmd != null + // On x86 (non-aarch64), PMU code path is not triggered, + // so StartVmCmd.pmu stays at its field default (true) + if ("x86_64".equals(vm.architecture) || vm.architecture == null) { + assert startCmd.pmu == true : "x86 VM should have PMU enabled by default" + } else if ("aarch64".equals(vm.architecture)) { + assert startCmd.pmu == false : "aarch64 VM should have PMU disabled by default" + } + + destroyVmInstance { uuid = vm.uuid } + expungeVmInstance { uuid = vm.uuid } + } + + void testPmuResourceConfigOverride() { + def image = env.inventoryByName("image1") + def l3 = env.inventoryByName("l3") + def instance = env.inventoryByName("instanceOffering") + + def vm = createVmInstance { + name = "test-pmu-override" + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + instanceOfferingUuid = instance.uuid + } as VmInstanceInventory + + // Set vm.pmu=true via ResourceConfig (different from default false) + updateResourceConfig { + category = VmGlobalConfig.CATEGORY + name = VmGlobalConfig.VM_PMU.name + value = "true" + resourceUuid = vm.uuid + } + + KVMAgentCommands.StartVmCmd startCmd = null + env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { KVMAgentCommands.StartVmResponse rsp, HttpEntity e -> + startCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert startCmd != null + // On x86, PMU stays true regardless of ResourceConfig (code only reads for aarch64) + // This verifies the ResourceConfig record exists and reboot doesn't crash + assert startCmd.pmu == true : "PMU should be true after reboot on x86" + + // Verify ResourceConfig was persisted + def configs = queryResourceConfig { + conditions = [ + "category=${VmGlobalConfig.CATEGORY}", + "name=${VmGlobalConfig.VM_PMU.name}", + "resourceUuid=${vm.uuid}" + ] + } + assert configs.size() == 1 : "ResourceConfig should be persisted" + + destroyVmInstance { uuid = vm.uuid } + expungeVmInstance { uuid = vm.uuid } + } +} From 12e74942671ecd116e2d4984725e680426c7f2f0 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 2 Apr 2026 19:18:36 +0800 Subject: [PATCH 57/77] [pci-device]: update SDK for UpdateVmInstancePciDeviceSpecRef API Resolves: ZSTAC-71156 Change-Id: I7724b3e43c45833291576883b544a032398c9c62 --- .../zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java index 8def583403f..41277b9a48a 100644 --- a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java @@ -97,10 +97,10 @@ protected Map getNonAPIParameterMap() { protected RestInfo getRestInfo() { RestInfo info = new RestInfo(); info.httpMethod = "PUT"; - info.path = "/pci-device-specs/{pciSpecUuid}/vm-instances/{vmInstanceUuid}"; + info.path = "/pci-device-specs/{pciSpecUuid}/vm-instances/{vmInstanceUuid}/actions"; info.needSession = true; info.needPoll = true; - info.parameterName = "params"; + info.parameterName = "updateVmInstancePciDeviceSpecRef"; return info; } From 17a0ec8c60b6188389948f9bb5878db93b7999eb Mon Sep 17 00:00:00 2001 From: J M Date: Thu, 2 Apr 2026 15:31:59 +0800 Subject: [PATCH 58/77] [testlib]: fix multiple bugs in Python SDK template 1. Rewrite async poll URL to use client-configured hostname 2. Fix _error() field: dual-write err.description + err.desc 3. Fix mutable default arg headers={} in _json_http 4. Fix re.findall regex missing escape for path param validation 5. Fix min_length validation using > instead of < 6. Fix path param check to 'if u not in params' 7. Fix bare except to except Exception 8. Remove duplicate import time 9. Decode rsp.data bytes to str for readable error messages 10. Handle IPv6 addresses in poll URL with bracket notation 11. Fix missing bracket and format arg in poll error message Resolves: ZSTAC-83925 Change-Id: I7a7062676b676a6f67646c7862706365666a6b6e --- testlib/src/main/resources/zssdk.py | 43 +++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/testlib/src/main/resources/zssdk.py b/testlib/src/main/resources/zssdk.py index a188e387746..d8463cb54a8 100755 --- a/testlib/src/main/resources/zssdk.py +++ b/testlib/src/main/resources/zssdk.py @@ -18,7 +18,6 @@ import hmac from hashlib import sha1 import datetime -import time try: int_types = (int, long) @@ -58,7 +57,7 @@ def _exception_safe(func): def wrap(*args, **kwargs): try: func(*args, **kwargs) - except: + except Exception: print(traceback.format_exc()) return wrap @@ -80,6 +79,7 @@ def _http_error(status, body=None): def _error(code, desc, details): err = ErrorCode() err.code = code + err.description = desc err.desc = desc err.details = details return {'error': err} @@ -181,7 +181,7 @@ def _check_params(self): if value is not None and isinstance(value, str) and annotation.max_length and len(value) > annotation.max_length: raise SdkError('invalid length[%s] of the parameter[%s], the max allowed length is %s' % (len(value), param_name, annotation.max_length)) - if value is not None and isinstance(value, str) and annotation.min_length and len(value) > annotation.min_length: + if value is not None and isinstance(value, str) and annotation.min_length and len(value) < annotation.min_length: raise SdkError('invalid length[%s] of the parameter[%s], the minimal allowed length is %s' % (len(value), param_name, annotation.min_length)) if value is not None and isinstance(value, list) and annotation.non_empty is True and len(value) == 0: @@ -235,11 +235,11 @@ def _url(self): elements.append('/v1') path = self.PATH.replace('{', '${') - unresolved = re.findall('${(.+?)}', path) + unresolved = re.findall(r'\$\{(.+?)\}', path) params = self._params() if unresolved: for u in unresolved: - if u in params: + if u not in params: raise SdkError('missing a mandatory parameter[%s]' % u) path = string.Template(path).substitute(params) @@ -362,7 +362,21 @@ def _poll_result(self, rsp, cb): m = json.loads(rsp.data) location = m[LOCATION] if not location: - raise SdkError("Internal Error] the api[%s] is an async API but the server doesn't return the polling location url") + raise SdkError("[Internal Error] the api[%s] is an async API but the server doesn't return the polling location url" % self.PATH) + + # Rewrite poll URL to use client-configured hostname:port, + # in case server returns an internal IP unreachable from client + try: + from urllib.parse import urlparse, urlunparse + except ImportError: + from urlparse import urlparse, urlunparse + parsed = urlparse(location) + configured_host = __config__[CONFIG_HOSTNAME] + configured_port = str(__config__[CONFIG_PORT]) + if ':' in configured_host and not configured_host.startswith('['): + configured_host = '[%s]' % configured_host + location = urlunparse(parsed._replace( + netloc='%s:%s' % (configured_host, configured_port))) if cb: # async polling @@ -480,11 +494,13 @@ def _uuid(): def _json_http( uri, body=None, - headers={}, + headers=None, method='POST', timeout=120.0 ): pool = urllib3.PoolManager(timeout=timeout, retries=urllib3.util.retry.Retry(15)) + if headers is None: + headers = {} headers.update({'Content-Type': 'application/json', 'Connection': 'close'}) if body is not None and not isinstance(body, str): @@ -497,6 +513,15 @@ def _json_http( else: rsp = pool.request(method, uri, headers=headers) - print('[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, rsp.data)) - return rsp + data = rsp.data + if isinstance(data, bytes): + data = data.decode('utf-8') + print('[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, data)) + + class _Rsp(object): + pass + r = _Rsp() + r.status = rsp.status + r.data = data + return r From 87171abc1d24e467780269d3b1f9a74755d066ca Mon Sep 17 00:00:00 2001 From: Jiajun Xu Date: Fri, 3 Apr 2026 11:45:13 +0800 Subject: [PATCH 59/77] [identity]: restrict APIQueryAccountMsg to admin-only in RBAC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RBAC权限策略中normalAPIs使用通配符org.zstack.header.identity.** 默认开放了APIQueryAccountMsg权限,导致低权限IAM用户可以查询全部 账户信息,存在越权安全漏洞。 将APIQueryAccountMsg加入adminOnlyAPIs列表,仅允许管理员调用。 Resolves: ZSTAC-83960 Change-Id: Iba4048b65e24922417c23df3cc126a26372d321a --- .../org/zstack/header/identity/RBACInfo.java | 1 + .../TestAPIQueryAccountMsgRBACCase.java | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java diff --git a/header/src/main/java/org/zstack/header/identity/RBACInfo.java b/header/src/main/java/org/zstack/header/identity/RBACInfo.java index 609a916d20c..81223064cd9 100755 --- a/header/src/main/java/org/zstack/header/identity/RBACInfo.java +++ b/header/src/main/java/org/zstack/header/identity/RBACInfo.java @@ -12,6 +12,7 @@ public void permissions() { .name("identity") .adminOnlyAPIs( APICreateAccountMsg.class, + APIQueryAccountMsg.class, APIShareResourceMsg.class, APIRevokeResourceSharingMsg.class, APIUpdateQuotaMsg.class, diff --git a/test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java b/test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java new file mode 100644 index 00000000000..15216cb50bb --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java @@ -0,0 +1,43 @@ +package org.zstack.test.unittest.identity; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.zstack.header.identity.APIQueryAccountMsg; +import org.zstack.header.identity.RBACInfo; +import org.zstack.header.identity.rbac.RBAC; + +public class TestAPIQueryAccountMsgRBACCase { + + @Before + public void setUp() { + RBAC.permissions.clear(); + } + + @Test + public void testAPIQueryAccountMsgIsAdminOnly() { + RBACInfo rbacInfo = new RBACInfo(); + rbacInfo.permissions(); + + boolean isAdminOnly = RBAC.isAdminOnlyAPI(APIQueryAccountMsg.class.getName()); + Assert.assertTrue( + "APIQueryAccountMsg should be admin-only to prevent privilege escalation, " + + "but it is currently accessible to normal IAM users via the wildcard normalAPIs pattern", + isAdminOnly + ); + } + + @Test + public void testAPIQueryAccountMsgNotInNormalAPIs() { + RBACInfo rbacInfo = new RBACInfo(); + rbacInfo.permissions(); + + String apiName = APIQueryAccountMsg.class.getName(); + boolean isInNormalOnly = RBAC.permissions.stream() + .anyMatch(p -> p.getNormalAPIs().contains(apiName) && !p.getAdminOnlyAPIs().contains(apiName)); + Assert.assertFalse( + "APIQueryAccountMsg should NOT be accessible as a normal API", + isInNormalOnly + ); + } +} From 39bd70534e73a4669d357ea5ac620ead58fc79ec Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Fri, 3 Apr 2026 17:27:36 +0800 Subject: [PATCH 60/77] [testlib]: add updateVmInstancePciDeviceSpecRef to ApiHelper Resolves: ZSTAC-71156 Change-Id: Ic10f480a42faf43b77169af3c49ca4df25d3ec5f --- .../java/org/zstack/testlib/ApiHelper.groovy | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 8a8f2b91416..4293a160725 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()) @@ -47342,6 +47342,33 @@ abstract class ApiHelper { } + def updateVmInstancePciDeviceSpecRef(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefAction.class) Closure c) { + def a = new org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefAction() + 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 updateVmNetworkConfig(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateVmNetworkConfigAction.class) Closure c) { def a = new org.zstack.sdk.UpdateVmNetworkConfigAction() a.sessionId = Test.currentEnvSpec?.session?.uuid From 61f565775df138e3bb80c922fc073acc5553d9c1 Mon Sep 17 00:00:00 2001 From: J M Date: Tue, 7 Apr 2026 16:22:19 +0800 Subject: [PATCH 61/77] [build]: auto-detect worktree .m2 runMavenProfile now auto-detects .m2/repository under the script directory. All mvn calls append $MVN_LOCAL so worktree-isolated builds work without manual flags. Resolves: ZSTAC-84007 Change-Id: Id3670d6e0ee594be2f2591e44a2f5ebc30632535 --- runMavenProfile | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/runMavenProfile b/runMavenProfile index c911240dee3..5415b97953a 100755 --- a/runMavenProfile +++ b/runMavenProfile @@ -1,5 +1,14 @@ #!/bin/bash +# Auto-detect isolated .m2 in worktree +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +if [ -d "$SCRIPT_DIR/.m2/repository" ]; then + MVN_LOCAL="-Dmaven.repo.local=$SCRIPT_DIR/.m2/repository" + echo "[runMavenProfile] Using isolated .m2: $SCRIPT_DIR/.m2/repository" +else + MVN_LOCAL="" +fi + usage() { echo "Command line tool of zstack project @@ -65,7 +74,7 @@ Options: " } -MVNTest="mvn test -Djacoco.skip=true" +MVNTest="mvn test -Djacoco.skip=true $MVN_LOCAL" py() { if [ -d premium/test-premium ]; then @@ -111,7 +120,7 @@ openapi() { fi cd tool/doclet - mvn -Dmaven.test.skip=true package + mvn -Dmaven.test.skip=true package $MVN_LOCAL cd - javadoc -private -doclet org.zstack.tool.doclet.JsonDocLet -docletpath tool/doclet/target/doclet-*-jar-with-dependencies.jar $(find -name *.java) @@ -172,7 +181,7 @@ mdpremium() { errorcode() { cd test - mvn test -Dtest=TestGenerateErrorCodeDoc + mvn test -Dtest=TestGenerateErrorCodeDoc $MVN_LOCAL cd - >/dev/null } @@ -205,7 +214,7 @@ triggerexpression() { fi cd premium/mevoco - mvn -P trigger-expression generate-sources + mvn -P trigger-expression generate-sources $MVN_LOCAL mkdir -p src/main/java/org/zstack/monitoring/trigger/expression/antlr4 yes | cp target/generated-sources/antlr4/TriggerExpression* src/main/java/org/zstack/monitoring/trigger/expression/antlr4 rm -f target/generated-sources/antlr4/TriggerExpression* @@ -214,14 +223,14 @@ triggerexpression() { zql() { cd search - mvn -P zql generate-sources + mvn -P zql generate-sources $MVN_LOCAL if [ $? -ne 0 ]; then exit 1 fi mkdir -p src/main/java/org/zstack/zql/antlr4/ yes | cp target/generated-sources/antlr4/* src/main/java/org/zstack/zql/antlr4/ rm -f target/generated-sources/antlr4/* - mvn -Dmaven.test.skip=true clean install + mvn -Dmaven.test.skip=true clean install $MVN_LOCAL cd - >/dev/null } @@ -232,7 +241,7 @@ cloudwatchfunction() { fi cd premium/zwatch - mvn -P function generate-sources + mvn -P function generate-sources $MVN_LOCAL mkdir -p src/main/java/org/zstack/zwatch/api/antlr4/ yes | cp target/generated-sources/antlr4/MetricFunction* src/main/java/org/zstack/zwatch/api/antlr4/ rm -f target/generated-sources/antlr4/MetricFunction* @@ -246,7 +255,7 @@ zwatchzql() { fi cd premium/zwatch - mvn -P function generate-sources + mvn -P function generate-sources $MVN_LOCAL mkdir -p src/main/java/org/zstack/zwatch/returnwith/antlr4/ yes | cp target/generated-sources/antlr4/ReturnWith* src/main/java/org/zstack/zwatch/returnwith/antlr4/ rm -f target/generated-sources/antlr4/ReturnWith* @@ -499,7 +508,7 @@ clear_git_config() { run_profile() { if test x$1 = x'premium'; then - mvn -Dmaven.test.skip=true -P premium clean install + mvn -Dmaven.test.skip=true -P premium clean install $MVN_LOCAL elif test x$1 = x'md'; then md elif test x$1 = x'sdk'; then @@ -549,7 +558,7 @@ run_profile() { elif test x$1 = x'gosdk'; then gosdk else - mvn -pl build -P $1 exec:exec -D$1 + mvn -pl build -P $1 exec:exec -D$1 $MVN_LOCAL fi } From 179dfbf6d1a4425bf41a585dc3267b9a4f6ee8a5 Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Tue, 31 Mar 2026 13:01:21 +0800 Subject: [PATCH 62/77] [kvm]: prevent metadata deletion when DVD returns empty When hypervisor metadata collection from DVD returns an empty list, the existing metadata was deleted before the empty check, causing all hosts to lose their hypervisor version metadata and matchState to become Unknown. 1. Why is this change necessary? saveHostOsCategoryList first deleted all existing metadata for the management node, then checked if the input was empty. When DVD collection returned empty, this wiped all metadata, causing matchTargetVersion to be null and matchState to become Unknown. 2. How does it address the problem? Move the empty-list check before the delete operation so that an empty input preserves existing metadata. A warning is logged to indicate the skip. 3. Are there any side effects? None. Non-empty input behavior is unchanged. # Summary of changes (by module): - kvm: move empty check before metadata delete in KvmHypervisorInfoManagerImpl.saveHostOsCategoryList() Related: ZSTAC-83682 Change-Id: I81f9baacac7fce9af2363a0ce5c960532d383890 --- .../kvm/hypervisor/KvmHypervisorInfoManagerImpl.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java index f9501fe279e..df330171cf6 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java @@ -275,13 +275,15 @@ private boolean saveMetadataList(List definitions) @Transactional protected boolean saveHostOsCategoryList(List categoryVOS) { + if (CollectionUtils.isEmpty(categoryVOS)) { + logger.warn("no hypervisor metadata collected from DVD, skip refresh to preserve existing metadata"); + return false; + } + // refresh all metadata with current management node SQL.New(KvmHostHypervisorMetadataVO.class) .eq(KvmHostHypervisorMetadataVO_.managementNodeUuid, Platform.getManagementServerId()) .delete(); - if (CollectionUtils.isEmpty(categoryVOS)) { - return false; - } Set requestArchitectures = categoryVOS.stream() .map(HostOsCategoryVO::getArchitecture) From 8472bf25a802a89f04f643fbc41c1fd49f12de03 Mon Sep 17 00:00:00 2001 From: "zhong.xian" Date: Mon, 9 Mar 2026 16:40:42 +0800 Subject: [PATCH 63/77] [sdk]: add requestCpu and add errCode add requestCpuNum and requestMemorySize; add loudOperationsErrorCode Resolves: ZSTAC-80103 Change-Id: Ib1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0 --- .../sdk/DeployAppDevelopmentServiceAction.java | 6 ++++++ .../sdk/DeployModelEvalServiceAction.java | 6 ++++++ .../zstack/sdk/DeployModelServiceAction.java | 6 ++++++ ...tchModelServiceTemplateWithModelAction.java | 6 ++++++ .../main/java/org/zstack/sdk/ModelService.java | 16 ++++++++++++++++ .../CloudOperationsErrorCode.java | 18 ++++++++++++++++++ 6 files changed, 58 insertions(+) diff --git a/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java b/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java index 0d4ef73088b..4fdecd4a475 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java @@ -55,6 +55,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -73,6 +76,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java b/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java index f29c06a748e..a75b1f851ec 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java @@ -106,6 +106,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -124,6 +127,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java b/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java index 105e7083ae2..cbfe5811d6a 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java @@ -55,6 +55,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -73,6 +76,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java b/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java index 7c0eca70894..36df79c8c7c 100644 --- a/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java +++ b/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java @@ -64,6 +64,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -82,6 +85,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/ModelService.java b/sdk/src/main/java/org/zstack/sdk/ModelService.java index 32cb11e9495..6f7bb93c223 100644 --- a/sdk/src/main/java/org/zstack/sdk/ModelService.java +++ b/sdk/src/main/java/org/zstack/sdk/ModelService.java @@ -108,6 +108,14 @@ public java.lang.Integer getCpuNum() { return this.cpuNum; } + public java.lang.Integer requestCpuNum; + public void setRequestCpuNum(java.lang.Integer requestCpuNum) { + this.requestCpuNum = requestCpuNum; + } + public java.lang.Integer getRequestCpuNum() { + return this.requestCpuNum; + } + public java.lang.String name; public void setName(java.lang.String name) { this.name = name; @@ -156,6 +164,14 @@ public java.lang.Long getMemorySize() { return this.memorySize; } + public java.lang.Long requestMemorySize; + public void setRequestMemorySize(java.lang.Long requestMemorySize) { + this.requestMemorySize = requestMemorySize; + } + public java.lang.Long getRequestMemorySize() { + return this.requestMemorySize; + } + public java.util.List l3NetworkUuids; public void setL3NetworkUuids(java.util.List l3NetworkUuids) { this.l3NetworkUuids = l3NetworkUuids; 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 ab1c62da4c1..3c773613b2f 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -14875,6 +14875,24 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10134 = "ORG_ZSTACK_AI_10134"; + public static final String ORG_ZSTACK_AI_10135 = "ORG_ZSTACK_AI_10135"; + + public static final String ORG_ZSTACK_AI_10136 = "ORG_ZSTACK_AI_10136"; + + public static final String ORG_ZSTACK_AI_10137 = "ORG_ZSTACK_AI_10137"; + + public static final String ORG_ZSTACK_AI_10138 = "ORG_ZSTACK_AI_10138"; + + public static final String ORG_ZSTACK_AI_10139 = "ORG_ZSTACK_AI_10139"; + + public static final String ORG_ZSTACK_AI_10140 = "ORG_ZSTACK_AI_10140"; + + public static final String ORG_ZSTACK_AI_10141 = "ORG_ZSTACK_AI_10141"; + + public static final String ORG_ZSTACK_AI_10142 = "ORG_ZSTACK_AI_10142"; + + public static final String ORG_ZSTACK_AI_10143 = "ORG_ZSTACK_AI_10143"; + public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10001 = "ORG_ZSTACK_CORE_CLOUDBUS_10001"; From c85fab3e4919beffb1e393430c40d134dcc0c713 Mon Sep 17 00:00:00 2001 From: "yingzhe.hu" Date: Tue, 7 Apr 2026 17:12:29 +0800 Subject: [PATCH 64/77] [kvm]: update TLS certs via kvmagent on host reconnect Resolves: ZSTAC-83696 Change-Id: I368cc5af8fb3d553bedc3be5d031015719e68ddc --- .../java/org/zstack/kvm/KVMAgentCommands.java | 17 +++++ .../main/java/org/zstack/kvm/KVMConstant.java | 1 + .../src/main/java/org/zstack/kvm/KVMHost.java | 71 +++++++++++++++++++ 3 files changed, 89 insertions(+) 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 90e576cad7f..244d146d5e3 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -455,6 +455,23 @@ public void setFailedInterfaceNames(List failedInterfaceNames) { public static class HostFactCmd extends AgentCommand { } + public static class UpdateTlsCertCmd extends AgentCommand implements Serializable { + private String caCert; + @NoLogging + private String caKey; + private String certIps; + + public String getCaCert() { return caCert; } + public void setCaCert(String caCert) { this.caCert = caCert; } + public String getCaKey() { return caKey; } + public void setCaKey(String caKey) { this.caKey = caKey; } + public String getCertIps() { return certIps; } + public void setCertIps(String certIps) { this.certIps = certIps; } + } + + public static class UpdateTlsCertResponse extends AgentResponse { + } + public static class HostFactResponse extends AgentResponse { @GrayVersion(value = "5.0.0") private String osDistribution; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 1b2df9f8f2a..5281d2f5707 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -81,6 +81,7 @@ public interface KVMConstant { String KVM_DELETE_CONSOLE_FIREWALL_PATH = "/vm/console/deletefirewall"; String KVM_UPDATE_HOST_OS_PATH = "/host/updateos"; String KVM_HOST_UPDATE_DEPENDENCY_PATH = "/host/updatedependency"; + String KVM_UPDATE_TLS_CERT_PATH = "/host/updatetlscert"; String HOST_SHUTDOWN = "/host/shutdown"; String HOST_REBOOT = "/host/reboot"; String HOST_UPDATE_SPICE_CHANNEL_CONFIG_PATH = "/host/updateSpiceChannelConfig"; 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 27ba23476ed..65dfaf55002 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -33,6 +33,7 @@ import org.zstack.core.db.SQLBatch; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; +import org.zstack.core.jsonlabel.JsonLabel; import org.zstack.core.thread.*; import org.zstack.core.timeout.ApiTimeoutManager; import org.zstack.core.timeout.TimeHelper; @@ -195,6 +196,7 @@ public class KVMHost extends HostBase implements Host { private String checkSnapshotPath; private String mergeSnapshotPath; private String hostFactPath; + private String updateTlsCertPath; private String hostCheckFilePath; private String attachIsoPath; private String detachIsoPath; @@ -328,6 +330,10 @@ public KVMHost(KVMHostVO self, KVMHostContext context) { ub.path(KVMConstant.KVM_HOST_FACT_PATH); hostFactPath = ub.build().toString(); + ub = UriComponentsBuilder.fromHttpUrl(baseUrl); + ub.path(KVMConstant.KVM_UPDATE_TLS_CERT_PATH); + updateTlsCertPath = ub.build().toString(); + ub = UriComponentsBuilder.fromHttpUrl(baseUrl); ub.path(KVMConstant.KVM_HOST_CHECK_FILE_PATH); hostCheckFilePath = ub.build().toString(); @@ -6025,6 +6031,71 @@ public void fail(ErrorCode errorCode) { flow(createCollectHostFactsFlow(info)); + flow(new NoRollbackFlow() { + String __name__ = "update-tls-certs-if-needed"; + + @Override + public boolean skip(Map data) { + return CoreGlobalProperty.UNIT_TEST_ON + || !KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) + || !KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.value(Boolean.class); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + String managementIp = getSelf().getManagementIp(); + String extraIps = HostSystemTags.EXTRA_IPS.getTokenByResourceUuid( + self.getUuid(), HostSystemTags.EXTRA_IPS_TOKEN); + + List allIps = new ArrayList<>(); + allIps.add(managementIp); + if (extraIps != null && !extraIps.isEmpty()) { + for (String ip : extraIps.split(",")) { + String trimmed = ip.trim(); + if (!trimmed.isEmpty() && !allIps.contains(trimmed)) { + allIps.add(trimmed); + } + } + } + + String certIps = String.join(",", allIps); + + String caCert = new JsonLabel().get("libvirtTLSCA", String.class); + String caKey = new JsonLabel().get("libvirtTLSPrivateKey", String.class); + if (caCert == null || caKey == null) { + logger.warn("TLS CA cert/key not found in database, skipping cert update"); + trigger.next(); + return; + } + + UpdateTlsCertCmd cmd = new UpdateTlsCertCmd(); + cmd.setCaCert(caCert); + cmd.setCaKey(caKey); + cmd.setCertIps(certIps); + + new Http<>(updateTlsCertPath, cmd, UpdateTlsCertResponse.class) + .call(new ReturnValueCompletion(trigger) { + @Override + public void success(UpdateTlsCertResponse ret) { + if (!ret.isSuccess()) { + logger.warn(String.format("Failed to update TLS certs on host[uuid:%s]: %s", + self.getUuid(), ret.getError())); + } + // cert update failure should not block reconnect + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("Failed to update TLS certs on host[uuid:%s]: %s", + self.getUuid(), errorCode)); + // cert update failure should not block reconnect + trigger.next(); + } + }); + } + }); + if (info.isNewAdded()) { flow(new NoRollbackFlow() { String __name__ = "check-qemu-libvirt-version"; From eebc5434ce31c908ae8eccbc84d80efd7f99c2f4 Mon Sep 17 00:00:00 2001 From: J M Date: Fri, 10 Apr 2026 15:51:16 +0800 Subject: [PATCH 65/77] [build]: add .m2 to gitignore Resolves: ZSTAC-84007 Change-Id: I36199fa21cb76a825bacf6ca036a13c24b8a5c9f --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e823298f7f6..5d4c43e3fe4 100755 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ premium/test-premium/zstack-api.log **/bin/ CLAUDE.md .claude/* +.m2/ From a04cd588b18f8f140cd15dd5256899240095d4f4 Mon Sep 17 00:00:00 2001 From: "boce.wang" Date: Tue, 3 Feb 2026 13:25:19 +0800 Subject: [PATCH 66/77] [vpc]: support snat log with LogServer Resolves: ZSTAC-82670 Change-Id: I6868776c4c3f169b7e9347a6a428151e8d51b230 --- .../appliancevm/ApplianceVmConstant.java | 1 + .../appliancevm/ApplianceVmFacadeImpl.java | 1 + .../org/zstack/sdk/AddLogServerAction.java | 2 +- .../sdk/GetVpcVRouterSnatLogStateAction.java | 95 ++++++++++++++++ .../sdk/GetVpcVRouterSnatLogStateResult.java | 14 +++ .../main/java/org/zstack/sdk/LogCategory.java | 1 + .../sdk/SetVpcVRouterSnatLogStateAction.java | 104 ++++++++++++++++++ .../sdk/SetVpcVRouterSnatLogStateResult.java | 14 +++ .../java/org/zstack/testlib/ApiHelper.groovy | 52 +++++++++ 9 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java index 1d3f453f312..dcada8edeee 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java @@ -31,6 +31,7 @@ public enum BootstrapParams { sshPort, uuid, managementNodeIp, + managementNodeVip, managementNodeCidr, additionalL3Uuids, } diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java index 0faf04c9f65..6d63ce3522f 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java @@ -462,6 +462,7 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { ret.put(ApplianceVmConstant.BootstrapParams.publicKey.toString(), publicKey); ret.put(BootstrapParams.uuid.toString(), spec.getVmInventory().getUuid()); ret.put(BootstrapParams.managementNodeIp.toString(), Platform.getManagementServerIp()); + ret.put(BootstrapParams.managementNodeVip.toString(), Platform.getManagementServerVip()); ret.put(BootstrapParams.managementNodeCidr.toString(), Platform.getManagementServerCidr()); /* this is only used by ApplianceVmPrepareBootstrapInfoExtensionPoint extension point, will be deleted after extension point */ ret.put(BootstrapParams.additionalL3Uuids.toString(), additionalNics.stream().map(VmNicInventory::getL3NetworkUuid).collect(Collectors.toList())); diff --git a/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java b/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java index 54344e22dc4..1fceee88af9 100644 --- a/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java +++ b/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java @@ -31,7 +31,7 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String description; - @Param(required = true, validValues = {"ManagementNodeLog","PlatformOperationLog"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, validValues = {"ManagementNodeLog","PlatformOperationLog","SnatLog"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String category; @Param(required = true, validValues = {"Log4j2","FluentBit"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) diff --git a/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java new file mode 100644 index 00000000000..bccc21c32e3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java @@ -0,0 +1,95 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetVpcVRouterSnatLogStateAction 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.GetVpcVRouterSnatLogStateResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @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; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.GetVpcVRouterSnatLogStateResult value = res.getResult(org.zstack.sdk.GetVpcVRouterSnatLogStateResult.class); + ret.value = value == null ? new org.zstack.sdk.GetVpcVRouterSnatLogStateResult() : 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 = "GET"; + info.path = "/vpc/virtual-routers/{uuid}/snat-log"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java new file mode 100644 index 00000000000..cee48dd497c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetVpcVRouterSnatLogStateResult { + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/LogCategory.java b/sdk/src/main/java/org/zstack/sdk/LogCategory.java index 96662c4bfb3..03ccc56a295 100644 --- a/sdk/src/main/java/org/zstack/sdk/LogCategory.java +++ b/sdk/src/main/java/org/zstack/sdk/LogCategory.java @@ -3,4 +3,5 @@ public enum LogCategory { ManagementNodeLog, PlatformOperationLog, + SnatLog, } diff --git a/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java new file mode 100644 index 00000000000..8628bf66021 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SetVpcVRouterSnatLogStateAction 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.SetVpcVRouterSnatLogStateResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = true, validValues = {"enable","disable"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String state; + + @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.SetVpcVRouterSnatLogStateResult value = res.getResult(org.zstack.sdk.SetVpcVRouterSnatLogStateResult.class); + ret.value = value == null ? new org.zstack.sdk.SetVpcVRouterSnatLogStateResult() : 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 = "PUT"; + info.path = "/vpc/virtual-routers/{uuid}/snat-log"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "setVpcVRouterSnatLogState"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java new file mode 100644 index 00000000000..41da7da65ba --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class SetVpcVRouterSnatLogStateResult { + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 8a8f2b91416..71bc9979235 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -26827,6 +26827,32 @@ abstract class ApiHelper { } } + def getVpcVRouterSnatLogState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetVpcVRouterSnatLogStateAction.class) Closure c) { + def a = new org.zstack.sdk.GetVpcVRouterSnatLogStateAction() + 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 getVpcVpnConfigurationFromRemote(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetVpcVpnConfigurationFromRemoteAction.class) Closure c) { def a = new org.zstack.sdk.GetVpcVpnConfigurationFromRemoteAction() @@ -40618,6 +40644,32 @@ abstract class ApiHelper { } } + def setVpcVRouterSnatLogState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetVpcVRouterSnatLogStateAction.class) Closure c) { + def a = new org.zstack.sdk.SetVpcVRouterSnatLogStateAction() + 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 shareResource(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ShareResourceAction.class) Closure c) { def a = new org.zstack.sdk.ShareResourceAction() From fc3df1f5eb698a10104a235bbf9f3bec93789399 Mon Sep 17 00:00:00 2001 From: "xinhao.huang" Date: Fri, 3 Apr 2026 16:07:41 +0800 Subject: [PATCH 67/77] [sdk]: support dgpu DBImpact Resolves: ZSTAC-83477 Change-Id: I61746e646972787a6f72757565776d6570617768 --- ...5.5.12__schema.sql => V5.5.16__schema.sql} | 158 ++++++++++ .../global-error-en_US.json | 7 +- .../global-error-zh_CN.json | 7 +- .../ResourceMetricBindingExtensionPoint.java | 88 ++++++ sdk/src/main/java/SourceClassMap.java | 14 + .../org/zstack/sdk/DGpuDeviceInventory.java | 127 ++++++++ .../org/zstack/sdk/DGpuProfileInventory.java | 55 ++++ .../zstack/sdk/DGpuSpecStatsInventory.java | 79 +++++ .../main/java/org/zstack/sdk/DGpuStatus.java | 8 + .../zstack/sdk/DetachDGpuFromVmAction.java | 104 +++++++ .../zstack/sdk/DetachDGpuFromVmResult.java | 7 + .../org/zstack/sdk/DisableDGpuModeAction.java | 101 +++++++ .../org/zstack/sdk/DisableDGpuModeResult.java | 7 + .../org/zstack/sdk/EnableDGpuModeAction.java | 101 +++++++ .../org/zstack/sdk/EnableDGpuModeResult.java | 7 + .../zstack/sdk/GetDGpuSpecStatsAction.java | 101 +++++++ .../zstack/sdk/GetDGpuSpecStatsResult.java | 14 + .../sdk/GetGpuDeviceCandidatesAction.java | 110 +++++++ .../sdk/GetGpuDeviceCandidatesResult.java | 14 + .../sdk/GetGpuDeviceSpecCandidatesAction.java | 3 + .../org/zstack/sdk/GpuDeviceInventory.java | 8 + .../sdk/GpuDeviceSpecCandidateInventory.java | 31 ++ .../org/zstack/sdk/PciDeviceInventory.java | 26 ++ .../org/zstack/sdk/PciDeviceVirtMode.java | 9 + .../org/zstack/sdk/PciDeviceVirtState.java | 8 + .../org/zstack/sdk/PciDeviceVirtStatus.java | 2 + .../org/zstack/sdk/QueryDGpuDeviceAction.java | 75 +++++ .../org/zstack/sdk/QueryDGpuDeviceResult.java | 22 ++ .../zstack/sdk/QueryDGpuProfileAction.java | 75 +++++ .../zstack/sdk/QueryDGpuProfileResult.java | 22 ++ .../sdk/RemoveVmDGpuStrategyAction.java | 101 +++++++ .../sdk/RemoveVmDGpuStrategyResult.java | 7 + .../org/zstack/sdk/SetDGpuProfileAction.java | 107 +++++++ .../org/zstack/sdk/SetDGpuProfileResult.java | 14 + .../zstack/sdk/SetVmDGpuStrategyAction.java | 113 ++++++++ .../zstack/sdk/SetVmDGpuStrategyResult.java | 7 + .../java/org/zstack/testlib/ApiHelper.groovy | 274 ++++++++++++++++++ .../CloudOperationsErrorCode.java | 33 ++- 38 files changed, 2041 insertions(+), 5 deletions(-) rename conf/db/upgrade/{V5.5.12__schema.sql => V5.5.16__schema.sql} (56%) create mode 100644 header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DGpuStatus.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java create mode 100644 sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.16__schema.sql similarity index 56% rename from conf/db/upgrade/V5.5.12__schema.sql rename to conf/db/upgrade/V5.5.16__schema.sql index 8b3b0d0d07e..0d8412840ce 100644 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ b/conf/db/upgrade/V5.5.16__schema.sql @@ -186,3 +186,161 @@ INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperat -- ZSTAC-74908: Add resourceType to TagPatternVO to scope AI model tags away from VM pages CALL ADD_COLUMN('TagPatternVO', 'resourceType', 'VARCHAR(128)', 1, NULL); + +-- PCI virtualization capability metadata + +CREATE TABLE IF NOT EXISTS `zstack`.`PciDeviceVirtCapabilityVO` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `pciDeviceUuid` VARCHAR(32) NOT NULL, + `capability` VARCHAR(32) NOT NULL, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pci_device_virt_capability` (`pciDeviceUuid`, `capability`), + KEY `idx_pci_device_virt_capability_pci` (`pciDeviceUuid`), + CONSTRAINT `fk_pci_device_virt_capability_pci` + FOREIGN KEY (`pciDeviceUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CALL ADD_COLUMN('PciDeviceVO', 'virtState', 'varchar(32)', 1, NULL); + +UPDATE `zstack`.`PciDeviceVO` +SET `virtState` = + CASE + WHEN `virtStatus` IN ('SRIOV_VIRTUALIZABLE', 'VFIO_MDEV_VIRTUALIZABLE', 'TENSORFUSION_VIRTUALIZABLE') THEN 'VIRTUALIZABLE' + WHEN `virtStatus` IN ('SRIOV_VIRTUALIZED', 'VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK', + 'HAMI_VIRTUALIZED', 'TENSORFUSION_VIRTUALIZED') THEN 'VIRTUALIZED' + WHEN `virtStatus` = 'SRIOV_VIRTUAL' THEN 'VIRTUAL' + ELSE 'UNVIRTUALIZABLE' + END +WHERE `virtState` IS NULL; + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'SRIOV', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` IN ('SRIOV_VIRTUALIZABLE', 'SRIOV_VIRTUALIZED'); + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'VFIO_MDEV', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` IN ('VFIO_MDEV_VIRTUALIZABLE', 'VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK'); + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'TENSORFUSION', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` IN ('TENSORFUSION_VIRTUALIZABLE', 'TENSORFUSION_VIRTUALIZED'); + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'HAMI', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` = 'HAMI_VIRTUALIZED'; + +CALL ADD_COLUMN('PciDeviceVO', 'virtMode', 'varchar(32)', 1, NULL); + +UPDATE `zstack`.`PciDeviceVO` +SET `virtMode` = + CASE + WHEN `virtStatus` IN ('SRIOV_VIRTUALIZED') THEN 'SRIOV' + WHEN `virtStatus` = 'SRIOV_VIRTUAL' THEN 'SRIOV' + WHEN `virtStatus` IN ('VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK') THEN 'VFIO_MDEV' + WHEN `virtStatus` = 'TENSORFUSION_VIRTUALIZED' THEN 'TENSORFUSION' + WHEN `virtStatus` = 'HAMI_VIRTUALIZED' THEN 'HAMI' + ELSE `virtMode` + END +WHERE `virtStatus` IN ( + 'SRIOV_VIRTUALIZED', 'SRIOV_VIRTUAL', + 'VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK', + 'TENSORFUSION_VIRTUALIZED', 'HAMI_VIRTUALIZED' +); + +CALL ADD_COLUMN('GpuDeviceVO', 'mode', 'varchar(32)', 1, NULL); +CALL CREATE_INDEX('GpuDeviceVO', 'idx_gpu_device_mode', 'mode'); + +UPDATE `zstack`.`GpuDeviceVO` g +INNER JOIN `zstack`.`PciDeviceVO` p ON g.`uuid` = p.`uuid` +SET g.`mode` = CASE + WHEN p.`virtState` = 'VIRTUALIZED' AND p.`virtMode` = 'TENSORFUSION' THEN 'DGPU' + WHEN p.`virtState` = 'VIRTUALIZED' AND p.`virtMode` IN ('VFIO_MDEV', 'SRIOV') THEN 'VGPU' + ELSE 'PCI' +END; + +UPDATE `zstack`.`GpuDeviceVO` g +INNER JOIN `zstack`.`PciDeviceVO` p ON g.`uuid` = p.`uuid` +SET g.`allocateStatus` = CASE + WHEN p.`vmInstanceUuid` IS NOT NULL THEN 'Allocated' + WHEN p.`virtState` = 'VIRTUALIZED' AND p.`virtMode` IS NOT NULL THEN 'Unallocatable' + ELSE 'Unallocated' +END; + +-- dGPU (TensorFusion) support tables + +CREATE TABLE IF NOT EXISTS `zstack`.`DGpuProfileVO` ( + `uuid` VARCHAR(32) NOT NULL, + `gpuSpecUuid` VARCHAR(32) NOT NULL, + `memorySize` BIGINT UNSIGNED NOT NULL, + `shmemSize` BIGINT UNSIGNED NOT NULL DEFAULT 268435456, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`uuid`), + UNIQUE KEY `uk_dgpu_profile` (`gpuSpecUuid`, `memorySize`), + CONSTRAINT `fk_dgpu_profile_spec` + FOREIGN KEY (`gpuSpecUuid`) REFERENCES `zstack`.`GpuDeviceSpecVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`DGpuDeviceVO` ( + `uuid` VARCHAR(32) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `parentGpuUuid` VARCHAR(32) NOT NULL, + `gpuSpecUuid` VARCHAR(32) NOT NULL, + `hostUuid` VARCHAR(32) NOT NULL, + `vmInstanceUuid` VARCHAR(32) DEFAULT NULL, + `allocatedMemory` BIGINT UNSIGNED NOT NULL, + `shmemSize` BIGINT UNSIGNED NOT NULL DEFAULT 268435456, + `smPercentLimit` INT NOT NULL DEFAULT 0, + `protocol` VARCHAR(16) NOT NULL DEFAULT 'shmem', + `status` VARCHAR(32) NOT NULL, + `vendorId` VARCHAR(64) DEFAULT NULL, + `vendor` VARCHAR(255) DEFAULT NULL, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`uuid`), + INDEX `idx_dgpu_device_parent` (`parentGpuUuid`), + INDEX `idx_dgpu_device_spec` (`gpuSpecUuid`), + INDEX `idx_dgpu_device_host` (`hostUuid`), + INDEX `idx_dgpu_device_vm` (`vmInstanceUuid`), + CONSTRAINT `fk_dgpu_device_parent` + FOREIGN KEY (`parentGpuUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_dgpu_device_spec` + FOREIGN KEY (`gpuSpecUuid`) REFERENCES `zstack`.`GpuDeviceSpecVO`(`uuid`) ON DELETE RESTRICT, + CONSTRAINT `fk_dgpu_device_host` + FOREIGN KEY (`hostUuid`) REFERENCES `zstack`.`HostEO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_dgpu_device_vm` + FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO`(`uuid`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`VmInstanceDGpuStrategyVO` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `gpuSpecUuid` VARCHAR(32) NOT NULL, + `memorySize` BIGINT UNSIGNED NOT NULL, + `shmemSize` BIGINT UNSIGNED NOT NULL DEFAULT 268435456, + `gpuDeviceUuid` VARCHAR(32) DEFAULT NULL, + `chooser` VARCHAR(16) NOT NULL, + `autoDetachOnStop` TINYINT(1) NOT NULL DEFAULT 1, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_vm_dgpu_strategy` (`vmInstanceUuid`), + INDEX `idx_vm_dgpu_strategy_spec` (`gpuSpecUuid`), + CONSTRAINT `fk_vm_dgpu_strategy_vm` + FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_dgpu_strategy_spec` + FOREIGN KEY (`gpuSpecUuid`) REFERENCES `zstack`.`GpuDeviceSpecVO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_dgpu_strategy_device` + FOREIGN KEY (`gpuDeviceUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index 7df63d4098c..af8d1b29ac6 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -4717,5 +4717,8 @@ "ORG_ZSTACK_ZWATCH_FUNCTION_10013": "Unknown arguments detected. Please ensure that all command-line parameters are valid and refer to the documentation for a list of accepted arguments.", "ORG_ZSTACK_VPCFIREWALL_10034": "could not add firewall rule[%d] as only TCP protocol supports TCP flags in this environment", "ORG_ZSTACK_NETWORK_SERVICE_VIRTUALROUTER_VYOS_10007": "unable to stop DHCP server on virtual router instance [uuid:%s] because %s", - "ORG_ZSTACK_VPCFIREWALL_10035": "could not add firewall rule[%d] because of a %s error" -} \ No newline at end of file + "ORG_ZSTACK_VPCFIREWALL_10035": "could not add firewall rule[%d] because of a %s error", + "ORG_ZSTACK_DGPU_10010": "Available License not found, please apply addon license for product dGPU.", + "ORG_ZSTACK_DGPU_10011": "Addon license for product dGPU has expired, please renew it.", + "ORG_ZSTACK_DGPU_10012": "Insufficient dGPU GPU number licensed. Your license permits %d GPU, there are %d GPU used, shared: %d, need: %d." +} diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index a6a01a7abe3..be35880ac65 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -4717,5 +4717,8 @@ "ORG_ZSTACK_ZWATCH_FUNCTION_10013": "未知参数", "ORG_ZSTACK_VPCFIREWALL_10034": "无法添加防火墙规则[%d],因为只有TCP协议可以使用TCP标志", "ORG_ZSTACK_NETWORK_SERVICE_VIRTUALROUTER_VYOS_10007": "无法停止虚拟路由器VM[uuid:%s]上的DHCP服务器,因为%s", - "ORG_ZSTACK_VPCFIREWALL_10035": "无法添加防火墙规则[%d]因为%s" -} \ No newline at end of file + "ORG_ZSTACK_VPCFIREWALL_10035": "无法添加防火墙规则[%d]因为%s", + "ORG_ZSTACK_DGPU_10010": "未找到可用的 dGPU AddOn License,请为 dGPU 产品申请并上传对应授权。", + "ORG_ZSTACK_DGPU_10011": "dGPU 产品的 AddOn License 已过期,请及时续期。", + "ORG_ZSTACK_DGPU_10012": "dGPU 授权 GPU 数量不足。License 允许 %d 个 GPU,当前已使用 %d 个,其他节点共享使用 %d 个,本次还需要 %d 个。" +} diff --git a/header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java b/header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java new file mode 100644 index 00000000000..1ec04b18fb4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java @@ -0,0 +1,88 @@ +package org.zstack.header.zwatch; + +import java.util.List; + +public interface ResourceMetricBindingExtensionPoint { + class ResourceMetricBinding { + private Class resourceType; + private String logicalMetricName; + private String sourceNamespace; + private String sourceMetricName; + private String resourceField; + private String sourceLabel; + private boolean requireUniqueSourceKey; + + private static T requireValue(String fieldName, T value) { + if (value == null) { + throw new IllegalStateException(String.format("ResourceMetricBinding.%s must not be null", fieldName)); + } + return value; + } + + private static String requireText(String fieldName, String value) { + requireValue(fieldName, value); + if (value.trim().isEmpty()) { + throw new IllegalStateException(String.format("ResourceMetricBinding.%s must not be empty", fieldName)); + } + return value; + } + + public Class getResourceType() { + return requireValue("resourceType", resourceType); + } + + public void setResourceType(Class resourceType) { + this.resourceType = resourceType; + } + + public String getLogicalMetricName() { + return requireText("logicalMetricName", logicalMetricName); + } + + public void setLogicalMetricName(String logicalMetricName) { + this.logicalMetricName = logicalMetricName; + } + + public String getSourceNamespace() { + return requireText("sourceNamespace", sourceNamespace); + } + + public void setSourceNamespace(String sourceNamespace) { + this.sourceNamespace = sourceNamespace; + } + + public String getSourceMetricName() { + return requireText("sourceMetricName", sourceMetricName); + } + + public void setSourceMetricName(String sourceMetricName) { + this.sourceMetricName = sourceMetricName; + } + + public String getResourceField() { + return requireText("resourceField", resourceField); + } + + public void setResourceField(String resourceField) { + this.resourceField = resourceField; + } + + public String getSourceLabel() { + return requireText("sourceLabel", sourceLabel); + } + + public void setSourceLabel(String sourceLabel) { + this.sourceLabel = sourceLabel; + } + + public boolean isRequireUniqueSourceKey() { + return requireUniqueSourceKey; + } + + public void setRequireUniqueSourceKey(boolean requireUniqueSourceKey) { + this.requireUniqueSourceKey = requireUniqueSourceKey; + } + } + + List getResourceMetricBindings(); +} diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index ab5ec3770f3..1206558cff1 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -641,8 +641,13 @@ public class SourceClassMap { put("org.zstack.pciDevice.PciDeviceType", "org.zstack.sdk.PciDeviceType"); put("org.zstack.pciDevice.gpu.GpuAllocateStatus", "org.zstack.sdk.GpuAllocateStatus"); put("org.zstack.pciDevice.gpu.GpuDeviceInventory", "org.zstack.sdk.GpuDeviceInventory"); + put("org.zstack.pciDevice.gpu.GpuDeviceSpecCandidateInventory", "org.zstack.sdk.GpuDeviceSpecCandidateInventory"); put("org.zstack.pciDevice.gpu.GpuDeviceSpecInventory", "org.zstack.sdk.GpuDeviceSpecInventory"); put("org.zstack.pciDevice.gpu.GpuVendor", "org.zstack.sdk.GpuVendor"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuDeviceInventory", "org.zstack.sdk.DGpuDeviceInventory"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuProfileInventory", "org.zstack.sdk.DGpuProfileInventory"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuSpecStatsInventory", "org.zstack.sdk.DGpuSpecStatsInventory"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuStatus", "org.zstack.sdk.DGpuStatus"); put("org.zstack.pciDevice.specification.mdev.MdevDeviceSpecInventory", "org.zstack.sdk.MdevDeviceSpecInventory"); put("org.zstack.pciDevice.specification.mdev.MdevDeviceSpecState", "org.zstack.sdk.MdevDeviceSpecState"); put("org.zstack.pciDevice.specification.mdev.PciDeviceMdevSpecRefInventory", "org.zstack.sdk.PciDeviceMdevSpecRefInventory"); @@ -650,6 +655,8 @@ public class SourceClassMap { put("org.zstack.pciDevice.specification.pci.PciDeviceSpecInventory", "org.zstack.sdk.PciDeviceSpecInventory"); put("org.zstack.pciDevice.specification.pci.PciDeviceSpecState", "org.zstack.sdk.PciDeviceSpecState"); put("org.zstack.pciDevice.specification.pci.VmInstancePciDeviceSpecRefInventory", "org.zstack.sdk.VmInstancePciDeviceSpecRefInventory"); + put("org.zstack.pciDevice.virtual.PciDeviceVirtMode", "org.zstack.sdk.PciDeviceVirtMode"); + put("org.zstack.pciDevice.virtual.PciDeviceVirtState", "org.zstack.sdk.PciDeviceVirtState"); put("org.zstack.pciDevice.virtual.PciDeviceVirtStatus", "org.zstack.sdk.PciDeviceVirtStatus"); put("org.zstack.pciDevice.virtual.vfio_mdev.MdevDeviceChooser", "org.zstack.sdk.MdevDeviceChooser"); put("org.zstack.pciDevice.virtual.vfio_mdev.MdevDeviceInventory", "org.zstack.sdk.MdevDeviceInventory"); @@ -1036,6 +1043,10 @@ public class SourceClassMap { put("org.zstack.sdk.CpuMemoryCapacityData", "org.zstack.header.allocator.datatypes.CpuMemoryCapacityData"); put("org.zstack.sdk.CreateDataVolumeTemplateFromVolumeSnapshotFailure", "org.zstack.header.image.APICreateDataVolumeTemplateFromVolumeSnapshotEvent$Failure"); put("org.zstack.sdk.CreateRootVolumeTemplateFromVolumeSnapshotFailure", "org.zstack.header.image.APICreateRootVolumeTemplateFromVolumeSnapshotEvent$Failure"); + put("org.zstack.sdk.DGpuDeviceInventory", "org.zstack.pciDevice.gpu.dgpu.DGpuDeviceInventory"); + put("org.zstack.sdk.DGpuProfileInventory", "org.zstack.pciDevice.gpu.dgpu.DGpuProfileInventory"); + put("org.zstack.sdk.DGpuSpecStatsInventory", "org.zstack.pciDevice.gpu.dgpu.DGpuSpecStatsInventory"); + put("org.zstack.sdk.DGpuStatus", "org.zstack.pciDevice.gpu.dgpu.DGpuStatus"); put("org.zstack.sdk.DRSAdviceInventory", "org.zstack.drs.entity.DRSAdviceInventory"); put("org.zstack.sdk.DRSVmMigrationActivityInventory", "org.zstack.drs.entity.DRSVmMigrationActivityInventory"); put("org.zstack.sdk.DataCenterInventory", "org.zstack.header.datacenter.DataCenterInventory"); @@ -1098,6 +1109,7 @@ public class SourceClassMap { put("org.zstack.sdk.GlobalConfigTemplateInventory", "org.zstack.templateConfig.GlobalConfigTemplateInventory"); put("org.zstack.sdk.GpuAllocateStatus", "org.zstack.pciDevice.gpu.GpuAllocateStatus"); put("org.zstack.sdk.GpuDeviceInventory", "org.zstack.pciDevice.gpu.GpuDeviceInventory"); + put("org.zstack.sdk.GpuDeviceSpecCandidateInventory", "org.zstack.pciDevice.gpu.GpuDeviceSpecCandidateInventory"); put("org.zstack.sdk.GpuDeviceSpecInventory", "org.zstack.pciDevice.gpu.GpuDeviceSpecInventory"); put("org.zstack.sdk.GpuVendor", "org.zstack.pciDevice.gpu.GpuVendor"); put("org.zstack.sdk.GuestOsCharacterInventory", "org.zstack.core.config.GuestOsCharacterInventory"); @@ -1343,6 +1355,8 @@ public class SourceClassMap { put("org.zstack.sdk.PciDeviceState", "org.zstack.pciDevice.PciDeviceState"); put("org.zstack.sdk.PciDeviceStatus", "org.zstack.pciDevice.PciDeviceStatus"); put("org.zstack.sdk.PciDeviceType", "org.zstack.pciDevice.PciDeviceType"); + put("org.zstack.sdk.PciDeviceVirtMode", "org.zstack.pciDevice.virtual.PciDeviceVirtMode"); + put("org.zstack.sdk.PciDeviceVirtState", "org.zstack.pciDevice.virtual.PciDeviceVirtState"); put("org.zstack.sdk.PciDeviceVirtStatus", "org.zstack.pciDevice.virtual.PciDeviceVirtStatus"); put("org.zstack.sdk.PendingTaskInfo", "org.zstack.header.core.progress.PendingTaskInfo"); put("org.zstack.sdk.PhysicalDriveSmartSelfTestHistoryInventory", "org.zstack.storage.device.localRaid.PhysicalDriveSmartSelfTestHistoryInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java new file mode 100644 index 00000000000..c8fb16ef2cf --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java @@ -0,0 +1,127 @@ +package org.zstack.sdk; + +import org.zstack.sdk.DGpuStatus; + +public class DGpuDeviceInventory { + + 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 parentGpuUuid; + public void setParentGpuUuid(java.lang.String parentGpuUuid) { + this.parentGpuUuid = parentGpuUuid; + } + public java.lang.String getParentGpuUuid() { + return this.parentGpuUuid; + } + + public java.lang.String gpuSpecUuid; + public void setGpuSpecUuid(java.lang.String gpuSpecUuid) { + this.gpuSpecUuid = gpuSpecUuid; + } + public java.lang.String getGpuSpecUuid() { + return this.gpuSpecUuid; + } + + public java.lang.String hostUuid; + public void setHostUuid(java.lang.String hostUuid) { + this.hostUuid = hostUuid; + } + public java.lang.String getHostUuid() { + return this.hostUuid; + } + + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.lang.Long allocatedMemory; + public void setAllocatedMemory(java.lang.Long allocatedMemory) { + this.allocatedMemory = allocatedMemory; + } + public java.lang.Long getAllocatedMemory() { + return this.allocatedMemory; + } + + public java.lang.Long shmemSize; + public void setShmemSize(java.lang.Long shmemSize) { + this.shmemSize = shmemSize; + } + public java.lang.Long getShmemSize() { + return this.shmemSize; + } + + public java.lang.String protocol; + public void setProtocol(java.lang.String protocol) { + this.protocol = protocol; + } + public java.lang.String getProtocol() { + return this.protocol; + } + + public java.lang.Integer smPercentLimit; + public void setSmPercentLimit(java.lang.Integer smPercentLimit) { + this.smPercentLimit = smPercentLimit; + } + public java.lang.Integer getSmPercentLimit() { + return this.smPercentLimit; + } + + public DGpuStatus status; + public void setStatus(DGpuStatus status) { + this.status = status; + } + public DGpuStatus getStatus() { + return this.status; + } + + 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; + } + + public java.lang.String vendorId; + public void setVendorId(java.lang.String vendorId) { + this.vendorId = vendorId; + } + public java.lang.String getVendorId() { + return this.vendorId; + } + + public java.lang.String vendor; + public void setVendor(java.lang.String vendor) { + this.vendor = vendor; + } + public java.lang.String getVendor() { + return this.vendor; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java b/sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java new file mode 100644 index 00000000000..96a5568810c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk; + + + +public class DGpuProfileInventory { + + 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 gpuSpecUuid; + public void setGpuSpecUuid(java.lang.String gpuSpecUuid) { + this.gpuSpecUuid = gpuSpecUuid; + } + public java.lang.String getGpuSpecUuid() { + return this.gpuSpecUuid; + } + + public java.lang.Long memorySize; + public void setMemorySize(java.lang.Long memorySize) { + this.memorySize = memorySize; + } + public java.lang.Long getMemorySize() { + return this.memorySize; + } + + public java.lang.Long shmemSize; + public void setShmemSize(java.lang.Long shmemSize) { + this.shmemSize = shmemSize; + } + public java.lang.Long getShmemSize() { + return this.shmemSize; + } + + 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/sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java b/sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java new file mode 100644 index 00000000000..2191e5c49c3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java @@ -0,0 +1,79 @@ +package org.zstack.sdk; + + + +public class DGpuSpecStatsInventory { + + public java.lang.String gpuSpecUuid; + public void setGpuSpecUuid(java.lang.String gpuSpecUuid) { + this.gpuSpecUuid = gpuSpecUuid; + } + public java.lang.String getGpuSpecUuid() { + return this.gpuSpecUuid; + } + + public java.lang.String gpuSpecName; + public void setGpuSpecName(java.lang.String gpuSpecName) { + this.gpuSpecName = gpuSpecName; + } + public java.lang.String getGpuSpecName() { + return this.gpuSpecName; + } + + public java.lang.String gpuType; + public void setGpuType(java.lang.String gpuType) { + this.gpuType = gpuType; + } + public java.lang.String getGpuType() { + return this.gpuType; + } + + public java.lang.Long gpuCount; + public void setGpuCount(java.lang.Long gpuCount) { + this.gpuCount = gpuCount; + } + public java.lang.Long getGpuCount() { + return this.gpuCount; + } + + public java.lang.Long dgpuCount; + public void setDgpuCount(java.lang.Long dgpuCount) { + this.dgpuCount = dgpuCount; + } + public java.lang.Long getDgpuCount() { + return this.dgpuCount; + } + + public java.lang.Long totalMemory; + public void setTotalMemory(java.lang.Long totalMemory) { + this.totalMemory = totalMemory; + } + public java.lang.Long getTotalMemory() { + return this.totalMemory; + } + + public java.lang.Long allocatedMemory; + public void setAllocatedMemory(java.lang.Long allocatedMemory) { + this.allocatedMemory = allocatedMemory; + } + public java.lang.Long getAllocatedMemory() { + return this.allocatedMemory; + } + + public java.lang.Long availableMemory; + public void setAvailableMemory(java.lang.Long availableMemory) { + this.availableMemory = availableMemory; + } + public java.lang.Long getAvailableMemory() { + return this.availableMemory; + } + + public java.lang.Double allocationRate; + public void setAllocationRate(java.lang.Double allocationRate) { + this.allocationRate = allocationRate; + } + public java.lang.Double getAllocationRate() { + return this.allocationRate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuStatus.java b/sdk/src/main/java/org/zstack/sdk/DGpuStatus.java new file mode 100644 index 00000000000..f2c72fc7ba7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuStatus.java @@ -0,0 +1,8 @@ +package org.zstack.sdk; + +public enum DGpuStatus { + Normal, + Fault, + Unknown, + Disconnected, +} diff --git a/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java new file mode 100644 index 00000000000..23995825b1b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DetachDGpuFromVmAction 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.DetachDGpuFromVmResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String dgpuDeviceUuid; + + @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.DetachDGpuFromVmResult value = res.getResult(org.zstack.sdk.DetachDGpuFromVmResult.class); + ret.value = value == null ? new org.zstack.sdk.DetachDGpuFromVmResult() : 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 = "PUT"; + info.path = "/vm-instances/{vmInstanceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "detachDGpuFromVm"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java new file mode 100644 index 00000000000..3574a0e85f9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class DetachDGpuFromVmResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java new file mode 100644 index 00000000000..8711977ee4d --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DisableDGpuModeAction 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.DisableDGpuModeResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuDeviceUuid; + + @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.DisableDGpuModeResult value = res.getResult(org.zstack.sdk.DisableDGpuModeResult.class); + ret.value = value == null ? new org.zstack.sdk.DisableDGpuModeResult() : 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 = "PUT"; + info.path = "/gpu-device/gpu-devices/{gpuDeviceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "disableDGpuMode"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java new file mode 100644 index 00000000000..51e129cf234 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class DisableDGpuModeResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java new file mode 100644 index 00000000000..a73ac6a3cd1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class EnableDGpuModeAction 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.EnableDGpuModeResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuDeviceUuid; + + @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.EnableDGpuModeResult value = res.getResult(org.zstack.sdk.EnableDGpuModeResult.class); + ret.value = value == null ? new org.zstack.sdk.EnableDGpuModeResult() : 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 = "PUT"; + info.path = "/gpu-device/gpu-devices/{gpuDeviceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "enableDGpuMode"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java new file mode 100644 index 00000000000..40eb1c5f2d3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class EnableDGpuModeResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java new file mode 100644 index 00000000000..d80fe253c6b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetDGpuSpecStatsAction 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.GetDGpuSpecStatsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String zoneUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String clusterUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String hostUuid; + + @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; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.GetDGpuSpecStatsResult value = res.getResult(org.zstack.sdk.GetDGpuSpecStatsResult.class); + ret.value = value == null ? new org.zstack.sdk.GetDGpuSpecStatsResult() : 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 = "GET"; + info.path = "/gpu-device/dgpu-spec-stats"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java new file mode 100644 index 00000000000..b15d1af436f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetDGpuSpecStatsResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java new file mode 100644 index 00000000000..d99a6378869 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java @@ -0,0 +1,110 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetGpuDeviceCandidatesAction 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.GetGpuDeviceCandidatesResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List clusterUuids; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String hostUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List vmInstanceUuids; + + @Param(required = false, validValues = {"PCI","DGPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String mode; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List pciSpecUuids; + + @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; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.GetGpuDeviceCandidatesResult value = res.getResult(org.zstack.sdk.GetGpuDeviceCandidatesResult.class); + ret.value = value == null ? new org.zstack.sdk.GetGpuDeviceCandidatesResult() : 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 = "GET"; + info.path = "/gpu-devices/candidates"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java new file mode 100644 index 00000000000..f0f06878a66 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetGpuDeviceCandidatesResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java index 3b32815e973..c8d3de5aadc 100644 --- a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java +++ b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java @@ -37,6 +37,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List vmInstanceUuids; + @Param(required = false, validValues = {"PCI","DGPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String mode; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java index 587c9cbeae3..0a47629a7b3 100644 --- a/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java @@ -60,4 +60,12 @@ public GpuAllocateStatus getAllocateStatus() { return this.allocateStatus; } + public java.lang.String mode; + public void setMode(java.lang.String mode) { + this.mode = mode; + } + public java.lang.String getMode() { + return this.mode; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java b/sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java new file mode 100644 index 00000000000..60eb7da3345 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java @@ -0,0 +1,31 @@ +package org.zstack.sdk; + + + +public class GpuDeviceSpecCandidateInventory extends org.zstack.sdk.GpuDeviceSpecInventory { + + public java.lang.String mode; + public void setMode(java.lang.String mode) { + this.mode = mode; + } + public java.lang.String getMode() { + return this.mode; + } + + public java.lang.Long maxAvailableMemory; + public void setMaxAvailableMemory(java.lang.Long maxAvailableMemory) { + this.maxAvailableMemory = maxAvailableMemory; + } + public java.lang.Long getMaxAvailableMemory() { + return this.maxAvailableMemory; + } + + public java.util.List dgpuProfiles; + public void setDgpuProfiles(java.util.List dgpuProfiles) { + this.dgpuProfiles = dgpuProfiles; + } + public java.util.List getDgpuProfiles() { + return this.dgpuProfiles; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java index a695b6c1b26..b3d7728f91e 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java @@ -4,6 +4,8 @@ import org.zstack.sdk.PciDeviceState; import org.zstack.sdk.PciDeviceStatus; import org.zstack.sdk.PciDeviceVirtStatus; +import org.zstack.sdk.PciDeviceVirtState; +import org.zstack.sdk.PciDeviceVirtMode; import org.zstack.sdk.PciDeviceChooser; import org.zstack.sdk.PciDeviceMetaData; @@ -97,6 +99,30 @@ public PciDeviceVirtStatus getVirtStatus() { return this.virtStatus; } + public PciDeviceVirtState virtState; + public void setVirtState(PciDeviceVirtState virtState) { + this.virtState = virtState; + } + public PciDeviceVirtState getVirtState() { + return this.virtState; + } + + public java.util.List virtCapabilities; + public void setVirtCapabilities(java.util.List virtCapabilities) { + this.virtCapabilities = virtCapabilities; + } + public java.util.List getVirtCapabilities() { + return this.virtCapabilities; + } + + public PciDeviceVirtMode virtMode; + public void setVirtMode(PciDeviceVirtMode virtMode) { + this.virtMode = virtMode; + } + public PciDeviceVirtMode getVirtMode() { + return this.virtMode; + } + public PciDeviceChooser chooser; public void setChooser(PciDeviceChooser chooser) { this.chooser = chooser; diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java new file mode 100644 index 00000000000..f9fcb6a79aa --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java @@ -0,0 +1,9 @@ +package org.zstack.sdk; + +public enum PciDeviceVirtMode { + SRIOV, + VFIO_MDEV, + K8S_BYPASS, + TENSORFUSION, + HAMI, +} diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java new file mode 100644 index 00000000000..7a56eabf9c0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java @@ -0,0 +1,8 @@ +package org.zstack.sdk; + +public enum PciDeviceVirtState { + UNVIRTUALIZABLE, + VIRTUALIZABLE, + VIRTUALIZED, + VIRTUAL, +} diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java index e59f4237d25..3612d6fb754 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java @@ -9,5 +9,7 @@ public enum PciDeviceVirtStatus { SRIOV_VIRTUAL, VIRTUALIZED_BYPASS_ZSTACK, HAMI_VIRTUALIZED, + TENSORFUSION_VIRTUALIZABLE, + TENSORFUSION_VIRTUALIZED, UNKNOWN, } diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java new file mode 100644 index 00000000000..ce5fc12e4af --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryDGpuDeviceAction extends QueryAction { + + 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.QueryDGpuDeviceResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryDGpuDeviceResult value = res.getResult(org.zstack.sdk.QueryDGpuDeviceResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryDGpuDeviceResult() : 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 = "GET"; + info.path = "/gpu-device/dgpu-devices"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java new file mode 100644 index 00000000000..54bbf450e4b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryDGpuDeviceResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java new file mode 100644 index 00000000000..b0f5b38380c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryDGpuProfileAction extends QueryAction { + + 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.QueryDGpuProfileResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryDGpuProfileResult value = res.getResult(org.zstack.sdk.QueryDGpuProfileResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryDGpuProfileResult() : 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 = "GET"; + info.path = "/gpu-device/dgpu-profiles"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java new file mode 100644 index 00000000000..7be7fc6e021 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryDGpuProfileResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java new file mode 100644 index 00000000000..e0d74840df8 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RemoveVmDGpuStrategyAction 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.RemoveVmDGpuStrategyResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @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.RemoveVmDGpuStrategyResult value = res.getResult(org.zstack.sdk.RemoveVmDGpuStrategyResult.class); + ret.value = value == null ? new org.zstack.sdk.RemoveVmDGpuStrategyResult() : 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 = "DELETE"; + info.path = "/vm-instances/{vmInstanceUuid}/dgpu-strategy"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java new file mode 100644 index 00000000000..866bc7622dc --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class RemoveVmDGpuStrategyResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java new file mode 100644 index 00000000000..c0fedea7396 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SetDGpuProfileAction 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.SetDGpuProfileResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuSpecUuid; + + @Param(required = true, nonempty = true, nullElements = false, emptyString = true, noTrim = false) + public java.util.List memorySizes; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long shmemSize; + + @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.SetDGpuProfileResult value = res.getResult(org.zstack.sdk.SetDGpuProfileResult.class); + ret.value = value == null ? new org.zstack.sdk.SetDGpuProfileResult() : 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 = "PUT"; + info.path = "/gpu-device/gpu-device-specs/{gpuSpecUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "setDGpuProfile"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java new file mode 100644 index 00000000000..6dc9258fdb7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class SetDGpuProfileResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java new file mode 100644 index 00000000000..e8f773e19de --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SetVmDGpuStrategyAction 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.SetVmDGpuStrategyResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String dgpuProfileUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuDeviceUuid; + + @Param(required = true, validValues = {"BySpec","ByDevice"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String chooser; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean autoDetachOnStop; + + @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.SetVmDGpuStrategyResult value = res.getResult(org.zstack.sdk.SetVmDGpuStrategyResult.class); + ret.value = value == null ? new org.zstack.sdk.SetVmDGpuStrategyResult() : 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 = "PUT"; + info.path = "/vm-instances/{vmInstanceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "setVmDGpuStrategy"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java new file mode 100644 index 00000000000..d7815916628 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class SetVmDGpuStrategyResult { + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 4293a160725..c85497d5222 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -18566,6 +18566,33 @@ abstract class ApiHelper { } + def detachDGpuFromVm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DetachDGpuFromVmAction.class) Closure c) { + def a = new org.zstack.sdk.DetachDGpuFromVmAction() + 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 detachDataVolumeFromHost(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DetachDataVolumeFromHostAction.class) Closure c) { def a = new org.zstack.sdk.DetachDataVolumeFromHostAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -19673,6 +19700,33 @@ abstract class ApiHelper { } + def disableDGpuMode(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DisableDGpuModeAction.class) Closure c) { + def a = new org.zstack.sdk.DisableDGpuModeAction() + 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 discoverExternalPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DiscoverExternalPrimaryStorageAction.class) Closure c) { def a = new org.zstack.sdk.DiscoverExternalPrimaryStorageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -19808,6 +19862,33 @@ abstract class ApiHelper { } + def enableDGpuMode(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.EnableDGpuModeAction.class) Closure c) { + def a = new org.zstack.sdk.EnableDGpuModeAction() + 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 executeAutoScalingRule(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ExecuteAutoScalingRuleAction.class) Closure c) { def a = new org.zstack.sdk.ExecuteAutoScalingRuleAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -22022,6 +22103,33 @@ abstract class ApiHelper { } + def getDGpuSpecStats(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetDGpuSpecStatsAction.class) Closure c) { + def a = new org.zstack.sdk.GetDGpuSpecStatsAction() + 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 getDataCenterFromRemote(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetDataCenterFromRemoteAction.class) Closure c) { def a = new org.zstack.sdk.GetDataCenterFromRemoteAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -22481,6 +22589,33 @@ abstract class ApiHelper { } + def getGpuDeviceCandidates(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetGpuDeviceCandidatesAction.class) Closure c) { + def a = new org.zstack.sdk.GetGpuDeviceCandidatesAction() + 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 getGpuDeviceSpecCandidates(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetGpuDeviceSpecCandidatesAction.class) Closure c) { def a = new org.zstack.sdk.GetGpuDeviceSpecCandidatesAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -30279,6 +30414,64 @@ abstract class ApiHelper { } + def queryDGpuDevice(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDGpuDeviceAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDGpuDeviceAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + 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 queryDGpuProfile(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDGpuProfileAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDGpuProfileAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + 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 queryDRSAdvice(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDRSAdviceAction.class) Closure c) { def a = new org.zstack.sdk.QueryDRSAdviceAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -38675,6 +38868,33 @@ abstract class ApiHelper { } + def removeVmDGpuStrategy(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.RemoveVmDGpuStrategyAction.class) Closure c) { + def a = new org.zstack.sdk.RemoveVmDGpuStrategyAction() + 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 removeVmFromAffinityGroup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.RemoveVmFromAffinityGroupAction.class) Closure c) { def a = new org.zstack.sdk.RemoveVmFromAffinityGroupAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39431,6 +39651,33 @@ abstract class ApiHelper { } + def setDGpuProfile(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetDGpuProfileAction.class) Closure c) { + def a = new org.zstack.sdk.SetDGpuProfileAction() + 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 setFlowMeterRouterId(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetFlowMeterRouterIdAction.class) Closure c) { def a = new org.zstack.sdk.SetFlowMeterRouterIdAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -40025,6 +40272,33 @@ abstract class ApiHelper { } + def setVmDGpuStrategy(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetVmDGpuStrategyAction.class) Closure c) { + def a = new org.zstack.sdk.SetVmDGpuStrategyAction() + 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 setVmEmulatorPinning(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetVmEmulatorPinningAction.class) Closure c) { def a = new org.zstack.sdk.SetVmEmulatorPinningAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 3c773613b2f..df5827516f6 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -364,6 +364,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_ZWATCH_10004 = "ORG_ZSTACK_ZWATCH_10004"; + public static final String ORG_ZSTACK_ZWATCH_10005 = "ORG_ZSTACK_ZWATCH_10005"; + public static final String ORG_ZSTACK_SSO_SAML2_SERVICE_10000 = "ORG_ZSTACK_SSO_SAML2_SERVICE_10000"; public static final String ORG_ZSTACK_SSO_SAML2_SERVICE_10001 = "ORG_ZSTACK_SSO_SAML2_SERVICE_10001"; @@ -6878,6 +6880,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10034 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10034"; + public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10035 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10035"; + + public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10036 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10036"; + + public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10037 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10037"; + public static final String ORG_ZSTACK_XDRAGON_10000 = "ORG_ZSTACK_XDRAGON_10000"; public static final String ORG_ZSTACK_PLUGINPREMIUM_COMPUTE_ALLOCATOR_10000 = "ORG_ZSTACK_PLUGINPREMIUM_COMPUTE_ALLOCATOR_10000"; @@ -14892,7 +14900,6 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10142 = "ORG_ZSTACK_AI_10142"; public static final String ORG_ZSTACK_AI_10143 = "ORG_ZSTACK_AI_10143"; - public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10001 = "ORG_ZSTACK_CORE_CLOUDBUS_10001"; @@ -15964,4 +15971,28 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10005 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10005"; public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10006 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10006"; + + public static final String ORG_ZSTACK_DGPU_10001 = "ORG_ZSTACK_DGPU_10001"; + + public static final String ORG_ZSTACK_DGPU_10002 = "ORG_ZSTACK_DGPU_10002"; + + public static final String ORG_ZSTACK_DGPU_10003 = "ORG_ZSTACK_DGPU_10003"; + + public static final String ORG_ZSTACK_DGPU_10004 = "ORG_ZSTACK_DGPU_10004"; + + public static final String ORG_ZSTACK_DGPU_10005 = "ORG_ZSTACK_DGPU_10005"; + + public static final String ORG_ZSTACK_DGPU_10006 = "ORG_ZSTACK_DGPU_10006"; + + public static final String ORG_ZSTACK_DGPU_10007 = "ORG_ZSTACK_DGPU_10007"; + + public static final String ORG_ZSTACK_DGPU_10008 = "ORG_ZSTACK_DGPU_10008"; + + public static final String ORG_ZSTACK_DGPU_10009 = "ORG_ZSTACK_DGPU_10009"; + + public static final String ORG_ZSTACK_DGPU_10010 = "ORG_ZSTACK_DGPU_10010"; + + public static final String ORG_ZSTACK_DGPU_10011 = "ORG_ZSTACK_DGPU_10011"; + + public static final String ORG_ZSTACK_DGPU_10012 = "ORG_ZSTACK_DGPU_10012"; } From 8ca173f5d0767b330159d42098c5d6c77e9b30da Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Mon, 13 Apr 2026 17:10:24 +0800 Subject: [PATCH 68/77] Revert "Merge branch 'fix/ZSTAC-79709' into '5.5.12'" This reverts merge request !9162 --- .../org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java index 140e94c13a8..c3b01dc3c8b 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java @@ -5448,7 +5448,7 @@ private void deleteSnapshotOnPrimaryStorage(final DeleteSnapshotOnPrimaryStorage httpCall(DELETE_SNAPSHOT_PATH, cmd, DeleteSnapshotRsp.class, new ReturnValueCompletion(msg) { @Override public void success(DeleteSnapshotRsp returnValue) { - osdHelper.releaseAvailableCapWithRatio(msg.getSnapshot().getPrimaryStorageInstallPath(), msg.getSnapshot().getSize()); + osdHelper.releaseAvailableCapacity(msg.getSnapshot().getPrimaryStorageInstallPath(), msg.getSnapshot().getSize()); bus.reply(msg, reply); completion.done(); } From 31c9680322fa4c470197197e6f93ec2cecf4b253 Mon Sep 17 00:00:00 2001 From: J M Date: Mon, 13 Apr 2026 13:36:58 +0800 Subject: [PATCH 69/77] [thread]: support coalesce queue for batch dhcp Add CoalesceQueue framework to coalesce same-signature requests into batch execution. FlatDhcpBackend uses CoalesceQueue to merge DHCP apply requests per host. Adapt operr to new error code API on 5.5.12. Resolves: ZSTAC-83039 Change-Id: Idaab1486b6fbc572b513ae72981337737baaf026 --- .../org/zstack/compute/zone/AbstractZone.java | 1 - .../core/thread/AbstractCoalesceQueue.java | 200 +++++ .../org/zstack/core/thread/CoalesceQueue.java | 46 ++ .../core/thread/ReturnValueCoalesceQueue.java | 43 + .../network/service/flat/FlatDhcpBackend.java | 49 +- .../core/taskqueue/CoalesceQueueCase.groovy | 740 ++++++++++++++++++ ...ifyPrepareDhcpWhenReconnectHostCase.groovy | 143 ++++ testlib/pom.xml | 33 + .../testlib/core/FailCoalesceQueue.java | 22 + .../core/FailReturnValueCoalesceQueue.java | 27 + .../testlib/core/ThrowOnFailCompletion.java | 31 + .../ThrowOnFailReturnValueCompletion.java | 31 + .../core/ThrowOnSuccessCompletion.java | 31 + .../ThrowOnSuccessReturnValueCompletion.java | 31 + .../CloudOperationsErrorCode.java | 4 + 15 files changed, 1428 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java create mode 100644 core/src/main/java/org/zstack/core/thread/CoalesceQueue.java create mode 100644 core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java create mode 100644 test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy create mode 100644 testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java create mode 100644 testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java create mode 100644 testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java create mode 100644 testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java create mode 100644 testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java create mode 100644 testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java diff --git a/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java b/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java index dd5e7f1c2fc..e327cd7f367 100755 --- a/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java +++ b/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java @@ -8,7 +8,6 @@ import org.zstack.header.zone.ZoneStateEvent; abstract class AbstractZone implements Zone { - private static DatabaseFacade dbf = Platform.getComponentLoader().getComponent(DatabaseFacade.class); private final static StateMachine stateMachine; static { diff --git a/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java new file mode 100644 index 00000000000..7a97e08b490 --- /dev/null +++ b/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java @@ -0,0 +1,200 @@ +package org.zstack.core.thread; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.header.core.AbstractCompletion; +import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10003; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Base implementation for coalesce queues. + * + * @param Request Item Type + * @param Batch Execution Result Type + * @param Single Request Result Type + */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public abstract class AbstractCoalesceQueue { + private static final CLogger logger = Utils.getLogger(AbstractCoalesceQueue.class); + + @Autowired + private ThreadFacade thdf; + + private final ConcurrentHashMap signatureQueues = new ConcurrentHashMap<>(); + + protected class PendingRequest { + final T item; + final AbstractCompletion completion; + + PendingRequest(T item, AbstractCompletion completion) { + this.item = item; + this.completion = completion; + } + + @SuppressWarnings("unchecked") + void notifySuccess(V result) { + if (completion == null) { + return; + } + + if (completion instanceof ReturnValueCompletion) { + ((ReturnValueCompletion) completion).success(result); + } else if (completion instanceof Completion) { + ((Completion) completion).success(); + } + } + + void notifyFailure(ErrorCode errorCode) { + if (completion == null) { + return; + } + + if (completion instanceof ReturnValueCompletion) { + ((ReturnValueCompletion) completion).fail(errorCode); + } else if (completion instanceof Completion) { + ((Completion) completion).fail(errorCode); + } + } + } + + private class SignatureQueue { + final String syncSignature; + List pendingList = Collections.synchronizedList(new ArrayList<>()); + + SignatureQueue(String syncSignature) { + this.syncSignature = syncSignature; + } + + synchronized List takeAll() { + List toProcess = pendingList; + pendingList = Collections.synchronizedList(new ArrayList<>()); + return toProcess; + } + + synchronized void add(PendingRequest request) { + pendingList.add(request); + } + + synchronized boolean isEmpty() { + return pendingList.isEmpty(); + } + } + + protected abstract String getName(); + + // Changed to take AbstractCompletion, subclasses cast it to specific type + protected abstract void executeBatch(List items, AbstractCompletion completion); + + protected abstract AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain); + + protected abstract V calculateResult(T item, R batchResult); + + protected final void handleSuccess(String syncSignature, List requests, R batchResult, SyncTaskChain chain) { + for (PendingRequest req : requests) { + try { + V singleResult = calculateResult(req.item, batchResult); + req.notifySuccess(singleResult); + } catch (Throwable t) { + logger.warn(String.format("[%s] failed to calculate result for item %s", getName(), req.item), t); + req.notifyFailure(operr(ORG_ZSTACK_CORE_THREAD_10003, "failed to calculate result: %s", t.getMessage())); + } + } + cleanup(syncSignature); + chain.next(); + } + + protected final void handleFailure(String syncSignature, List requests, ErrorCode errorCode, SyncTaskChain chain) { + for (PendingRequest req : requests) { + req.notifyFailure(errorCode); + } + cleanup(syncSignature); + chain.next(); + } + + void setThreadFacade(ThreadFacade thdf) { + this.thdf = thdf; + } + + protected final void submitRequest(String syncSignature, T item, AbstractCompletion completion) { + doSubmit(syncSignature, new PendingRequest(item, completion)); + } + + private void doSubmit(String syncSignature, PendingRequest request) { + SignatureQueue queue = signatureQueues.computeIfAbsent(syncSignature, SignatureQueue::new); + queue.add(request); + + thdf.chainSubmit(new ChainTask(null) { + @Override + public String getSyncSignature() { + return String.format("coalesce-queue-%s-%s", AbstractCoalesceQueue.this.getName(), syncSignature); + } + + @Override + public void run(SyncTaskChain chain) { + List requests = queue.takeAll(); + + if (requests.isEmpty()) { + chain.next(); + return; + } + + String name = getName(); + logger.debug(String.format("[%s] coalescing %d requests for signature[%s]", + name, requests.size(), syncSignature)); + + + // Create the specific completion type (Completion or ReturnValueCompletion) + AbstractCompletion batchCompletion = createBatchCompletion(syncSignature, requests, chain); + + // Execute batch with the direct completion object + List items = requests.stream().map(req -> req.item).collect(Collectors.toList()); + try { + executeBatch(items, batchCompletion); + } catch (Throwable t) { + logger.warn(String.format("[%s] executeBatch threw exception for signature[%s]", + name, syncSignature), t); + handleFailure(syncSignature, requests, + operr(ORG_ZSTACK_CORE_THREAD_10004, "executeBatch threw exception: %s", t.getMessage()), chain); + } + } + + @Override + public String getName() { + return String.format("%s-coalesced-batch-%s", AbstractCoalesceQueue.this.getName(), syncSignature); + } + + @Override + protected int getSyncLevel() { + return 1; + } + }); + } + + private void cleanup(String syncSignature) { + signatureQueues.computeIfPresent(syncSignature, (k, queue) -> { + if (queue.isEmpty()) { + return null; + } + return queue; + }); + } + + // For testing + int getActiveQueueCount() { + return signatureQueues.size(); + } +} diff --git a/core/src/main/java/org/zstack/core/thread/CoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/CoalesceQueue.java new file mode 100644 index 00000000000..45473efb751 --- /dev/null +++ b/core/src/main/java/org/zstack/core/thread/CoalesceQueue.java @@ -0,0 +1,46 @@ +package org.zstack.core.thread; + +import org.zstack.header.core.AbstractCompletion; +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.List; + +/** + * A coalesce queue for requests that do NOT expect a return value. + * + * @param Request Item Type + */ +public abstract class CoalesceQueue extends AbstractCoalesceQueue { + + public void submit(String syncSignature, T item, Completion completion) { + submitRequest(syncSignature, item, completion); + } + + protected abstract void executeBatch(List items, Completion completion); + + @Override + protected final void executeBatch(List items, AbstractCompletion batchCompletion) { + executeBatch(items, (Completion) batchCompletion); + } + + @Override + protected final AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain) { + return new Completion(chain) { + @Override + public void success() { + handleSuccess(syncSignature, requests, null, chain); + } + + @Override + public void fail(ErrorCode errorCode) { + handleFailure(syncSignature, requests, errorCode, chain); + } + }; + } + + @Override + protected final Void calculateResult(T item, Void batchResult) { + return null; + } +} diff --git a/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java new file mode 100644 index 00000000000..346824647fd --- /dev/null +++ b/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java @@ -0,0 +1,43 @@ +package org.zstack.core.thread; + +import org.zstack.header.core.AbstractCompletion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.List; + +/** + * A coalesce queue for requests that expect a return value. + * + * @param Request Item Type + * @param Batch Execution Result Type + * @param Single Request Result Type + */ +public abstract class ReturnValueCoalesceQueue extends AbstractCoalesceQueue { + + public void submit(String syncSignature, T item, ReturnValueCompletion completion) { + submitRequest(syncSignature, item, completion); + } + + protected abstract void executeBatch(List items, ReturnValueCompletion completion); + + @Override + protected final void executeBatch(List items, AbstractCompletion batchCompletion) { + executeBatch(items, (ReturnValueCompletion) batchCompletion); + } + + @Override + protected final AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain) { + return new ReturnValueCompletion(null) { + @Override + public void success(R batchResult) { + handleSuccess(syncSignature, requests, batchResult, chain); + } + + @Override + public void fail(ErrorCode errorCode) { + handleFailure(syncSignature, requests, errorCode, chain); + } + }; + } +} diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java index ecc0c14e2e3..5823bb2b9ee 100755 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java @@ -18,8 +18,7 @@ import org.zstack.core.db.SQL; import org.zstack.core.defer.Defer; import org.zstack.core.defer.Deferred; -import org.zstack.core.thread.SyncTask; -import org.zstack.core.thread.ThreadFacade; +import org.zstack.core.thread.*; import org.zstack.core.upgrade.GrayVersion; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; @@ -121,6 +120,48 @@ public class FlatDhcpBackend extends AbstractService implements NetworkServiceDh private Map getIpStatisticExts = new HashMap<>(); + private static class DhcpApplyRequest { + final String hostUuid; + final List dhcpInfos; + final boolean rebuild; + + DhcpApplyRequest(String hostUuid, List dhcpInfos, boolean rebuild) { + this.hostUuid = hostUuid; + this.dhcpInfos = dhcpInfos; + this.rebuild = rebuild; + } + } + + private class DhcpApplyQueue extends CoalesceQueue { + @Override + protected String getName() { + return "flat-dhcp-apply"; + } + + @Override + protected void executeBatch(List requests, Completion completion) { + if (requests.isEmpty()) { + completion.success(); + return; + } + + String hostUuid = requests.get(0).hostUuid; + + boolean anyRebuild = false; + List mergedInfos = new ArrayList<>(); + for (DhcpApplyRequest req : requests) { + anyRebuild = anyRebuild || req.rebuild; + mergedInfos.addAll(req.dhcpInfos); + } + + logger.debug(String.format("Coalesced %d DHCP apply requests for host[uuid:%s]", requests.size(), hostUuid)); + + applyDhcpToHosts(mergedInfos, hostUuid, anyRebuild, completion); + } + } + + private final DhcpApplyQueue dhcpApplyCoalesceQueue = new DhcpApplyQueue(); + public static final String APPLY_DHCP_PATH = "/flatnetworkprovider/dhcp/apply"; public static final String BATCH_APPLY_DHCP_PATH = "/flatnetworkprovider/dhcp/batchApply"; public static final String PREPARE_DHCP_PATH = "/flatnetworkprovider/dhcp/prepare"; @@ -2074,7 +2115,9 @@ public void applyDhcpService(List dhcpStructList, VmInstanceSpec spe return; } - applyDhcpToHosts(toDhcpInfo(dhcpStructList), spec.getDestHost().getUuid(), false, completion); + String hostUuid = spec.getDestHost().getUuid(); + DhcpApplyRequest request = new DhcpApplyRequest(hostUuid, toDhcpInfo(dhcpStructList), false); + dhcpApplyCoalesceQueue.submit(hostUuid, request, completion); } private void releaseDhcpService(List info, final String vmUuid, final String hostUuid, final NoErrorCompletion completion) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy b/test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy new file mode 100644 index 00000000000..e81eb8b19b4 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy @@ -0,0 +1,740 @@ +package org.zstack.test.integration.core.chaintask + +import org.zstack.core.thread.CoalesceQueue +import org.zstack.core.thread.ReturnValueCoalesceQueue +import org.zstack.header.core.Completion +import org.zstack.header.core.ReturnValueCompletion +import org.zstack.header.errorcode.ErrorCode +import org.zstack.testlib.core.FailCoalesceQueue +import org.zstack.testlib.core.ThrowOnSuccessCompletion +import org.zstack.testlib.core.ThrowOnFailCompletion +import org.zstack.testlib.core.FailReturnValueCoalesceQueue +import org.zstack.testlib.core.ThrowOnSuccessReturnValueCompletion +import org.zstack.testlib.core.ThrowOnFailReturnValueCompletion +import org.zstack.testlib.SubCase + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class CoalesceQueueCase extends SubCase { + @Override + void clean() { + } + + @Override + void setup() { + } + + @Override + void environment() { + } + + @Override + void test() { + testCoalesceMultipleRequests() + testDifferentSignaturesNotCoalesced() + testBatchFailureNotifiesAllRequests() + testBatchThrowExceptionNotifiesAllRequests() + testReturnValueCompletion() + testResultCalculationFailure() + testSequentialBatches() + testHighVolumeNoLossAcrossBatches() + testCompletionSuccessThrowDoesNotBlockChain() + testCompletionFailThrowDoesNotBlockChain() + testRvExecuteBatchThrowDoesNotBlockChain() + testRvCompletionSuccessThrowDoesNotBlockChain() + testRvCompletionFailThrowDoesNotBlockChain() + testCalculateResultFailDoesNotBlockChain() + } + + void testCoalesceMultipleRequests() { + def requestCount = 10 + def completionLatch = new CountDownLatch(requestCount) + def batchExecutionCount = new AtomicInteger(0) + def processedItems = Collections.synchronizedList(new ArrayList()) + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-coalesce" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batchExecutionCount.incrementAndGet() + processedItems.addAll(items) + + new Thread({ + try { + TimeUnit.MILLISECONDS.sleep(100) + } catch (InterruptedException ignored) { + } + completion.success() + }).start() + } + } + + def signature = "host-1" + (0.. + def token = "done-${idx}" + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert processedItems.size() == requestCount + assert batchExecutionCount.get() < requestCount + assert completedTokens.size() == requestCount + (0.. + assert completedTokens.contains("done-${idx}") + } + } + + void testDifferentSignaturesNotCoalesced() { + def signaturesCount = 3 + def requestsPerSignature = 5 + def totalRequests = signaturesCount * requestsPerSignature + def completionLatch = new CountDownLatch(totalRequests) + def batchExecutionCount = new AtomicInteger(0) + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-multi-sig" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batchExecutionCount.incrementAndGet() + completion.success() + } + } + + (0.. + def signature = "host-${sig}" + (0.. + def item = "${signature}-item-${idx}" + def token = "done-${item}" + queue.submit(signature, item, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert batchExecutionCount.get() >= signaturesCount + assert completedTokens.size() == totalRequests + (0.. + def signature = "host-${sig}" + (0.. + assert completedTokens.contains("done-${signature}-item-${idx}") + } + } + } + + void testBatchFailureNotifiesAllRequests() { + def requestCount = 5 + def completionLatch = new CountDownLatch(requestCount) + def failureCount = new AtomicInteger(0) + def testError = org.zstack.core.Platform.operr(org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004, "test error") + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-failure" + } + + @Override + protected void executeBatch(List items, Completion completion) { + completion.fail(testError) + } + } + + def signature = "host-fail" + (0.. + def token = "fail-${idx}" + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failureCount.incrementAndGet() + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert failureCount.get() == requestCount + assert completedTokens.size() == requestCount + (0.. + assert completedTokens.contains("fail-${idx}") + } + } + + void testBatchThrowExceptionNotifiesAllRequests() { + def requestCount = 5 + def completionLatch = new CountDownLatch(requestCount) + def failureCount = new AtomicInteger(0) + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new FailCoalesceQueue() + + def signature = "host-throw" + (0.. + def token = "throw-${idx}" + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failureCount.incrementAndGet() + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert failureCount.get() == requestCount + assert completedTokens.size() == requestCount + (0.. + assert completedTokens.contains("throw-${idx}") + } + } + + + void testReturnValueCompletion() { + def requestCount = 5 + def completionLatch = new CountDownLatch(requestCount) + def receivedResults = Collections.synchronizedMap(new LinkedHashMap()) + def mismatches = Collections.synchronizedList(new ArrayList()) + def batchResult = "batch-success" + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-return-value" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success(batchResult) + } + + @Override + protected String calculateResult(Integer item, String r) { + return "${r}-item-${item}" + } + } + + def signature = "host-result" + (0.. + queue.submit(signature, idx, new ReturnValueCompletion(null) { + @Override + void success(String result) { + def expected = String.format("%s-item-%s", batchResult, idx) + if (result != expected) { + mismatches.add(String.format("item-%s=%s", idx, result)) + } + receivedResults.put(idx, result) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert receivedResults.size() == requestCount + assert mismatches.isEmpty() + (0.. + def expected = String.format("%s-item-%s", batchResult, idx) + assert receivedResults.get(idx) == expected + } + } + + void testResultCalculationFailure() { + def completionLatch = new CountDownLatch(2) + def successCount = new AtomicInteger(0) + def failCount = new AtomicInteger(0) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-calc-fail" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success(null) + } + + @Override + protected String calculateResult(Integer item, Void batchResult) { + if (item == 0) { + throw new RuntimeException("Calculation failed for item 0 (on purpose)") + } + return "success" + } + } + + def signature = "host-calc" + queue.submit(signature, 0, new ReturnValueCompletion(null) { + @Override + void success(String ret) { + successCount.incrementAndGet() + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failCount.incrementAndGet() + completionLatch.countDown() + } + }) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String ret) { + successCount.incrementAndGet() + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failCount.incrementAndGet() + completionLatch.countDown() + } + }) + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert successCount.get() == 1 + assert failCount.get() == 1 + } + + void testSequentialBatches() { + def firstBatchStart = new CountDownLatch(1) + def firstBatchContinue = new CountDownLatch(1) + def secondBatchStart = new CountDownLatch(1) + def secondBatchContinue = new CountDownLatch(1) + def allComplete = new CountDownLatch(6) + def batches = Collections.synchronizedList(new ArrayList>()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-sequential" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batches.add(new ArrayList<>(items)) + + if (batches.size() == 1) { + firstBatchStart.countDown() + try { + firstBatchContinue.await(5, TimeUnit.SECONDS) + } catch (InterruptedException ignored) { + } + } else if (batches.size() == 2) { + secondBatchStart.countDown() + try { + secondBatchContinue.await(5, TimeUnit.SECONDS) + } catch (InterruptedException ignored) { + } + } + + completion.success() + } + } + + def signature = "host-seq" + queue.submit(signature, 0, new Completion(null) { + @Override + void success() { + allComplete.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + allComplete.countDown() + } + }) + + assert firstBatchStart.await(5, TimeUnit.SECONDS) + + (1..<4).each { idx -> + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + allComplete.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + allComplete.countDown() + } + }) + } + + // release first batch so chain.next() fires and second batch can start + firstBatchContinue.countDown() + assert secondBatchStart.await(5, TimeUnit.SECONDS) + + // submit more items while second batch is blocked on secondBatchContinue + (4..<6).each { idx -> + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + allComplete.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + allComplete.countDown() + } + }) + } + + secondBatchContinue.countDown() + assert allComplete.await(10, TimeUnit.SECONDS) + assert batches.size() == 3 + assert batches.get(0) == [0] + assert batches.get(1).containsAll([1, 2, 3]) + assert batches.get(2).containsAll([4, 5]) + } + + void testHighVolumeNoLossAcrossBatches() { + def requestCount = 300 + def completionLatch = new CountDownLatch(requestCount) + def processedItems = Collections.synchronizedSet(new LinkedHashSet()) + def batchCount = new AtomicInteger(0) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-high-volume" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batchCount.incrementAndGet() + processedItems.addAll(items) + + new Thread({ + try { + TimeUnit.MILLISECONDS.sleep(3) + } catch (InterruptedException ignored) { + } + completion.success() + }).start() + } + } + + def signature = "host-high-volume" + (0.. + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(6, TimeUnit.SECONDS) + assert processedItems.size() == requestCount + assert batchCount.get() >= 1 + } + + void testCompletionSuccessThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-success-throw" + } + + @Override + protected void executeBatch(List items, Completion completion) { + completion.success() + } + } + + def signature = "host-throw-success" + + // first request: Java Completion that throws on success() — AJ should catch it + queue.submit(signature, 0, new ThrowOnSuccessCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + // second request on same signature: must succeed if chain is not stuck + queue.submit(signature, 1, new Completion(null) { + @Override + void success() { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after completion.success() threw exception" + } + + void testCompletionFailThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-fail-throw" + } + + @Override + protected void executeBatch(List items, Completion completion) { + completion.fail(org.zstack.core.Platform.operr( + org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004, + "intentional batch failure")) + } + } + + def signature = "host-throw-fail" + + // first request: Java Completion that throws on fail() — AJ should catch it + queue.submit(signature, 0, new ThrowOnFailCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + // second request on same signature: must succeed if chain is not stuck + queue.submit(signature, 1, new Completion(null) { + @Override + void success() { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after completion.fail() threw exception" + } + + void testRvExecuteBatchThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new FailReturnValueCoalesceQueue() + + def signature = "host-rv-throw" + queue.submit(signature, 0, new ThrowOnFailReturnValueCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after RV executeBatch threw exception" + } + + void testRvCompletionSuccessThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-rv-success-throw" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success("ok") + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + return batchResult + } + } + + def signature = "host-rv-success-throw" + queue.submit(signature, 0, new ThrowOnSuccessReturnValueCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after RV completion.success() threw exception" + } + + void testRvCompletionFailThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-rv-fail-throw" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.fail(org.zstack.core.Platform.operr( + org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004, + "intentional rv batch failure")) + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + return null + } + } + + def signature = "host-rv-fail-throw" + queue.submit(signature, 0, new ThrowOnFailReturnValueCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after RV completion.fail() threw exception" + } + + void testCalculateResultFailDoesNotBlockChain() { + def firstLatch = new CountDownLatch(2) + def normalLatch = new CountDownLatch(1) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-calc-fail-chain" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success("ok") + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + if (item == 0) { + throw new RuntimeException("intentional calculateResult failure") + } + return batchResult + } + } + + def signature = "host-calc-fail-chain" + + // item 0 will throw in calculateResult, item 1 should still succeed + (0..1).each { idx -> + queue.submit(signature, idx, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + firstLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + firstLatch.countDown() + } + }) + } + + assert firstLatch.await(5, TimeUnit.SECONDS) + + // subsequent request must work — chain not stuck + queue.submit(signature, 2, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after calculateResult threw exception" + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy index 6e7d69c10eb..f3b9239e879 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy @@ -1,6 +1,7 @@ package org.zstack.test.integration.networkservice.provider.flat.dhcp import org.springframework.http.HttpEntity +import org.zstack.core.thread.ThreadFacade import org.zstack.header.network.service.NetworkServiceType import org.zstack.network.securitygroup.SecurityGroupConstant import org.zstack.network.service.eip.EipConstant @@ -9,6 +10,9 @@ import org.zstack.network.service.flat.FlatNetworkServiceConstant import org.zstack.network.service.userdata.UserdataConstant import org.zstack.network.service.virtualrouter.vyos.VyosConstants import org.zstack.sdk.HostInventory +import org.zstack.sdk.ImageInventory +import org.zstack.sdk.InstanceOfferingInventory +import org.zstack.sdk.L3NetworkInventory import org.zstack.sdk.VirtualRouterVmInventory import org.zstack.sdk.VmInstanceInventory import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest @@ -17,6 +21,10 @@ import org.zstack.testlib.SubCase import org.zstack.utils.data.SizeUnit import org.zstack.utils.gson.JSONObjectUtil +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { EnvSpec env @Override @@ -147,12 +155,14 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { void test() { env.create { checkDhcpWork() + testBatchStartVmApplyDhcp() } } void checkDhcpWork(){ def host = queryHost {}[0] as HostInventory def vm = env.inventoryByName("vm") as VmInstanceInventory + def vmItemTokens = new LinkedHashSet() setVmHostname { uuid = vm.uuid @@ -164,6 +174,11 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e1 -> cmd = JSONObjectUtil.toObject(e1.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) called += 1 + cmd.dhcpInfos.each { info -> + info.dhcp.each { dhcp -> + vmItemTokens.add(String.format("%s-%s-%s", dhcp.ip, dhcp.netmask, dhcp.gateway)) + } + } return rsp } @@ -176,12 +191,17 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { assert called == 1 assert cmd.dhcpInfos.size() == 1 assert cmd.dhcpInfos.get(0).dhcp.get(0).hostname == "test-name" + def vmNic = vm.vmNics.get(0) + def expectedToken = String.format("%s-%s-%s", vmNic.ip, vmNic.netmask, vmNic.gateway) + assert vmItemTokens.contains(expectedToken) called = 0 cmd = null + vmItemTokens.clear() reconnectHost { uuid=host.uuid } assert called == 1 assert cmd.dhcpInfos.get(0).dhcp.get(0).hostname == "test-name" + assert vmItemTokens.contains(expectedToken) def vr = queryVirtualRouterVm {}[0] as VirtualRouterVmInventory assert vr != null @@ -192,6 +212,129 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { assert called == 1 } + void testBatchStartVmApplyDhcp() { + L3NetworkInventory l3 = env.inventoryByName("l3-1") as L3NetworkInventory + ImageInventory image = env.inventoryByName("image") as ImageInventory + InstanceOfferingInventory offering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory + + def vmCount = 4 + def vms = new ArrayList() + def hostnameByIp = new LinkedHashMap() + (0.. + def hname = "batch-${idx}" + VmInstanceInventory inv = createVmInstance { + name = "batch-vm-${idx}" + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + instanceOfferingUuid = offering.uuid + } as VmInstanceInventory + setVmHostname { + uuid = inv.uuid + hostname = hname + } + hostnameByIp.put(inv.vmNics.get(0).ip, hname) + vms.add(inv) + } + + vms.each { vmInv -> + stopVmInstance { + uuid = vmInv.uuid + } + } + + def batchCmds = Collections.synchronizedList(new ArrayList()) + def firstBatchArrived = new CountDownLatch(1) + def releaseFirstBatch = new CountDownLatch(1) + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e1 -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e1.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + batchCmds.add(cmd) + if (batchCmds.size() == 1) { + firstBatchArrived.countDown() + releaseFirstBatch.await(10, TimeUnit.SECONDS) + } + return rsp + } + + VmInstanceInventory blocker = vms.remove(0) + new Thread({ + startVmInstance { + uuid = blocker.uuid + } + }).start() + assert firstBatchArrived.await(10, TimeUnit.SECONDS) + + CountDownLatch doneLatch = new CountDownLatch(vms.size()) + vms.each { vmInv -> + new Thread({ + try { + startVmInstance { + uuid = vmInv.uuid + } + } finally { + doneLatch.countDown() + } + }).start() + } + + ThreadFacade thdf = bean(ThreadFacade.class) + retryInSecs { + assert thdf.getChainTaskInfo(String.format("coalesce-queue-flat-dhcp-apply-%s", vms[0].hostUuid)).pendingTask.size() == 3 + } + + releaseFirstBatch.countDown() + assert doneLatch.await(2, TimeUnit.MINUTES) + + retryInSecs(5) { + assert batchCmds.size() == 2 + } + retryInSecs(2) { + assert batchCmds.size() == 2 + } + + Closure> toTokenSet = { FlatDhcpBackend.BatchApplyDhcpCmd batch -> + def tokens = new LinkedHashSet() + batch.dhcpInfos.each { info -> + info.dhcp.each { dhcp -> + tokens.add(String.format("%s-%s-%s", dhcp.ip, dhcp.netmask, dhcp.gateway)) + } + } + return tokens + } + + Closure> toHostnameMap = { FlatDhcpBackend.BatchApplyDhcpCmd batch -> + def hostnames = new LinkedHashMap() + batch.dhcpInfos.each { info -> + info.dhcp.each { dhcp -> + hostnames.put(dhcp.ip, dhcp.hostname) + } + } + return hostnames + } + + def firstBatchTokens = toTokenSet(batchCmds.get(0)) + def secondBatchTokens = toTokenSet(batchCmds.get(1)) + def firstBatchHostnames = toHostnameMap(batchCmds.get(0)) + def secondBatchHostnames = toHostnameMap(batchCmds.get(1)) + + def blockerNic = blocker.vmNics.get(0) + def blockerToken = String.format("%s-%s-%s", blockerNic.ip, blockerNic.netmask, blockerNic.gateway) + assert firstBatchTokens.size() == 1 + assert firstBatchTokens.contains(blockerToken) + assert firstBatchHostnames.size() == 1 + assert firstBatchHostnames.get(blockerNic.ip) == hostnameByIp.get(blockerNic.ip) + + def expectedTokens = new LinkedHashSet() + def expectedHostnames = new LinkedHashMap() + vms.each { vmInv -> + def nic = vmInv.vmNics.get(0) + expectedTokens.add(String.format("%s-%s-%s", nic.ip, nic.netmask, nic.gateway)) + expectedHostnames.put(nic.ip, hostnameByIp.get(nic.ip)) + } + assert secondBatchTokens.containsAll(expectedTokens) + assert secondBatchTokens.size() == expectedTokens.size() + assert secondBatchHostnames == expectedHostnames + } + @Override void clean() { env.delete() diff --git a/testlib/pom.xml b/testlib/pom.xml index 4d1688012a6..d1927c6d4fd 100644 --- a/testlib/pom.xml +++ b/testlib/pom.xml @@ -247,6 +247,39 @@ + + org.codehaus.mojo + aspectj-maven-plugin + ${aspectj.plugin.version} + + + + compile + test-compile + + + + + ${project.java.version} + ${project.java.version} + ${project.java.version} + true + + + org.springframework + spring-aspects + + + org.zstack + core + + + org.zstack + header + + + + diff --git a/testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java b/testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java new file mode 100644 index 00000000000..7627a34e1ca --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java @@ -0,0 +1,22 @@ +package org.zstack.testlib.core; + +import org.zstack.core.thread.CoalesceQueue; +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.OperationFailureException; + +import java.util.List; + +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004; + +public class FailCoalesceQueue extends CoalesceQueue { + @Override + protected String getName() { + return "test-failure"; + } + + @Override + protected void executeBatch(List items, Completion completion) { + throw new OperationFailureException(operr(ORG_ZSTACK_CORE_THREAD_10004, "test error")); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java b/testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java new file mode 100644 index 00000000000..2c99bd2ee3b --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java @@ -0,0 +1,27 @@ +package org.zstack.testlib.core; + +import org.zstack.core.thread.ReturnValueCoalesceQueue; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.OperationFailureException; + +import java.util.List; + +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004; + +public class FailReturnValueCoalesceQueue extends ReturnValueCoalesceQueue { + @Override + protected String getName() { + return "test-rv-failure"; + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + throw new OperationFailureException(operr(ORG_ZSTACK_CORE_THREAD_10004, "test rv error")); + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + return null; + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java new file mode 100644 index 00000000000..6ce51356b16 --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A Completion that throws RuntimeException on fail(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnFailCompletion extends Completion { + private final CountDownLatch latch; + + public ThrowOnFailCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success() { + latch.countDown(); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + throw new RuntimeException("intentional throw in fail()"); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java new file mode 100644 index 00000000000..25a1326b7dc --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A ReturnValueCompletion that throws RuntimeException on fail(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnFailReturnValueCompletion extends ReturnValueCompletion { + private final CountDownLatch latch; + + public ThrowOnFailReturnValueCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success(String returnValue) { + latch.countDown(); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + throw new RuntimeException("intentional throw in ReturnValueCompletion.fail()"); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java new file mode 100644 index 00000000000..5fa6ccdcdb6 --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A Completion that throws RuntimeException on success(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnSuccessCompletion extends Completion { + private final CountDownLatch latch; + + public ThrowOnSuccessCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success() { + latch.countDown(); + throw new RuntimeException("intentional throw in success()"); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java new file mode 100644 index 00000000000..71cd5ed8a19 --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A ReturnValueCompletion that throws RuntimeException on success(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnSuccessReturnValueCompletion extends ReturnValueCompletion { + private final CountDownLatch latch; + + public ThrowOnSuccessReturnValueCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success(String returnValue) { + latch.countDown(); + throw new RuntimeException("intentional throw in ReturnValueCompletion.success()"); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + } +} 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 3c773613b2f..dde389d818d 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -13247,6 +13247,10 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_CORE_THREAD_10002 = "ORG_ZSTACK_CORE_THREAD_10002"; + public static final String ORG_ZSTACK_CORE_THREAD_10003 = "ORG_ZSTACK_CORE_THREAD_10003"; + + public static final String ORG_ZSTACK_CORE_THREAD_10004 = "ORG_ZSTACK_CORE_THREAD_10004"; + public static final String ORG_ZSTACK_POLICYROUTE_10000 = "ORG_ZSTACK_POLICYROUTE_10000"; public static final String ORG_ZSTACK_POLICYROUTE_10001 = "ORG_ZSTACK_POLICYROUTE_10001"; From 659c2960a27247756bdc90fe9d35987b128a82f6 Mon Sep 17 00:00:00 2001 From: "yingzhe.hu" Date: Mon, 13 Apr 2026 14:52:05 +0800 Subject: [PATCH 70/77] [kvm]: use SSH cert check + ansible deploy instead of kvmagent HTTP for TLS cert update Resolves: ZSTAC-83696 Change-Id: I4a5f404e51f12487b61bdf8f990bbc490cafeeee --- .../java/org/zstack/kvm/KVMAgentCommands.java | 17 -- .../main/java/org/zstack/kvm/KVMConstant.java | 2 +- .../src/main/java/org/zstack/kvm/KVMHost.java | 188 +++++++++++------- .../vm/migrate/LibvirtTlsMigrateCase.groovy | 32 +++ 4 files changed, 145 insertions(+), 94 deletions(-) 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 244d146d5e3..90e576cad7f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -455,23 +455,6 @@ public void setFailedInterfaceNames(List failedInterfaceNames) { public static class HostFactCmd extends AgentCommand { } - public static class UpdateTlsCertCmd extends AgentCommand implements Serializable { - private String caCert; - @NoLogging - private String caKey; - private String certIps; - - public String getCaCert() { return caCert; } - public void setCaCert(String caCert) { this.caCert = caCert; } - public String getCaKey() { return caKey; } - public void setCaKey(String caKey) { this.caKey = caKey; } - public String getCertIps() { return certIps; } - public void setCertIps(String certIps) { this.certIps = certIps; } - } - - public static class UpdateTlsCertResponse extends AgentResponse { - } - public static class HostFactResponse extends AgentResponse { @GrayVersion(value = "5.0.0") private String osDistribution; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 5281d2f5707..303a12bc6fc 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -81,7 +81,7 @@ public interface KVMConstant { String KVM_DELETE_CONSOLE_FIREWALL_PATH = "/vm/console/deletefirewall"; String KVM_UPDATE_HOST_OS_PATH = "/host/updateos"; String KVM_HOST_UPDATE_DEPENDENCY_PATH = "/host/updatedependency"; - String KVM_UPDATE_TLS_CERT_PATH = "/host/updatetlscert"; + String HOST_SHUTDOWN = "/host/shutdown"; String HOST_REBOOT = "/host/reboot"; String HOST_UPDATE_SPICE_CHANNEL_CONFIG_PATH = "/host/updateSpiceChannelConfig"; 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 65dfaf55002..8696757bc92 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -129,6 +129,20 @@ public class KVMHost extends HostBase implements Host { protected static OperationChecker allowedOperations = new OperationChecker(true); protected static OperationChecker skipOperations = new OperationChecker(true); + public static Set parseSanIps(String sanOutput) { + Set sanIps = new HashSet<>(); + if (sanOutput == null || sanOutput.isEmpty()) { + return sanIps; + } + for (String line : sanOutput.split(",|\n")) { + String trimmed = line.trim(); + if (trimmed.startsWith("IP Address:")) { + sanIps.add(trimmed.substring("IP Address:".length()).trim()); + } + } + return sanIps; + } + @Autowired @Qualifier("KVMHostFactory") protected KVMHostFactory factory; @@ -196,7 +210,6 @@ public class KVMHost extends HostBase implements Host { private String checkSnapshotPath; private String mergeSnapshotPath; private String hostFactPath; - private String updateTlsCertPath; private String hostCheckFilePath; private String attachIsoPath; private String detachIsoPath; @@ -330,10 +343,6 @@ public KVMHost(KVMHostVO self, KVMHostContext context) { ub.path(KVMConstant.KVM_HOST_FACT_PATH); hostFactPath = ub.build().toString(); - ub = UriComponentsBuilder.fromHttpUrl(baseUrl); - ub.path(KVMConstant.KVM_UPDATE_TLS_CERT_PATH); - updateTlsCertPath = ub.build().toString(); - ub = UriComponentsBuilder.fromHttpUrl(baseUrl); ub.path(KVMConstant.KVM_HOST_CHECK_FILE_PATH); hostCheckFilePath = ub.build().toString(); @@ -5702,6 +5711,84 @@ public void run(FlowTrigger trigger, Map data) { } }); + flow(new NoRollbackFlow() { + String __name__ = "check-tls-certs-if-needed"; + + @Override + public boolean skip(Map data) { + return CoreGlobalProperty.UNIT_TEST_ON + || !KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) + || !rcf.getResourceConfigValue( + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE, + self.getUuid(), Boolean.class); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + String managementIp = getSelf().getManagementIp(); + + // 1. Get all IPs on the host via SSH + SshShell sshShell = new SshShell(); + sshShell.setHostname(managementIp); + sshShell.setUsername(getSelf().getUsername()); + sshShell.setPassword(getSelf().getPassword()); + sshShell.setPort(getSelf().getPort()); + + SshResult ipResult = sshShell.runCommand( + "ip -4 -o addr show scope global | sed -n \"s/.* inet \\([0-9.]\\+\\).*/\\1/p\""); + if (ipResult.isSshFailure() || ipResult.getReturnCode() != 0) { + logger.warn(String.format("Failed to get host IPs via SSH for TLS cert check on host[uuid:%s]: %s", + self.getUuid(), ipResult.getExitErrorMessage())); + trigger.next(); + return; + } + + // 2. Build IP list: managementIp + extra IPs (exclude managementIp and MN VIP) + List allIps = new ArrayList<>(); + allIps.add(managementIp); + String[] hostIps = ipResult.getStdout().trim().split("\n"); + for (String ip : hostIps) { + String trimmed = ip.trim(); + if (!trimmed.isEmpty() && !trimmed.equals(managementIp) + && !trimmed.equals(CoreGlobalProperty.MN_VIP) + && !trimmed.equals("127.0.0.1") + && !allIps.contains(trimmed)) { + allIps.add(trimmed); + } + } + + String certIpList = String.join(",", allIps); + + // 3. Check existing cert SAN via SSH + SshResult sanResult = sshShell.runCommand( + "openssl x509 -in /etc/pki/libvirt/servercert.pem -noout -ext subjectAltName 2>/dev/null"); + + boolean needDeploy = false; + if (sanResult.isSshFailure() || sanResult.getReturnCode() != 0 + || sanResult.getStdout() == null || sanResult.getStdout().trim().isEmpty()) { + // cert doesn't exist or can't be read + logger.info(String.format("TLS cert not found or unreadable on host[uuid:%s], need deploy", self.getUuid())); + needDeploy = true; + } else { + Set sanIps = parseSanIps(sanResult.getStdout()); + for (String ip : allIps) { + if (!sanIps.contains(ip)) { + logger.info(String.format("TLS cert SAN missing IP %s on host[uuid:%s], need deploy", ip, self.getUuid())); + needDeploy = true; + break; + } + } + } + + if (needDeploy) { + data.put("NEED_DEPLOY_TLS_CERT", true); + } + data.put("TLS_CERT_IPS", certIpList); + + trigger.next(); + } + }); + flow(new NoRollbackFlow() { String __name__ = "apply-ansible-playbook"; @@ -5836,14 +5923,27 @@ public void run(final FlowTrigger trigger, Map data) { deployArguments.setSkipPackages(info.getSkipPackages()); deployArguments.setUpdatePackages(String.valueOf(CoreGlobalProperty.UPDATE_PKG_WHEN_CONNECT)); - // Build TLS cert IP list: management IP + extra IPs (migration network etc.) - String managementIp = getSelf().getManagementIp(); - String extraIps = HostSystemTags.EXTRA_IPS.getTokenByResourceUuid( - self.getUuid(), HostSystemTags.EXTRA_IPS_TOKEN); - if (extraIps != null && !extraIps.isEmpty()) { - deployArguments.setTlsCertIps(managementIp + "," + extraIps); + // Build TLS cert IP list: prefer SSH-detected IPs from check-tls-certs flow + String tlsCertIpsFromData = (String) data.get("TLS_CERT_IPS"); + if (tlsCertIpsFromData != null) { + deployArguments.setTlsCertIps(tlsCertIpsFromData); } else { - deployArguments.setTlsCertIps(managementIp); + // Fallback: management IP + extra IPs from system tag + String managementIp = getSelf().getManagementIp(); + String extraIps = HostSystemTags.EXTRA_IPS.getTokenByResourceUuid( + self.getUuid(), HostSystemTags.EXTRA_IPS_TOKEN); + if (extraIps != null && !extraIps.isEmpty()) { + deployArguments.setTlsCertIps(managementIp + "," + extraIps); + } else { + deployArguments.setTlsCertIps(managementIp); + } + } + + // Force ansible deploy when TLS cert needs update (detected by check-tls-certs flow) + Boolean needDeployTlsCert = (Boolean) data.get("NEED_DEPLOY_TLS_CERT"); + if (Boolean.TRUE.equals(needDeployTlsCert)) { + runner.setForceRun(true); + deployArguments.setRestartLibvirtd("true"); } if (deployArguments.isForceRun()) { @@ -6031,70 +6131,6 @@ public void fail(ErrorCode errorCode) { flow(createCollectHostFactsFlow(info)); - flow(new NoRollbackFlow() { - String __name__ = "update-tls-certs-if-needed"; - - @Override - public boolean skip(Map data) { - return CoreGlobalProperty.UNIT_TEST_ON - || !KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) - || !KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.value(Boolean.class); - } - - @Override - public void run(FlowTrigger trigger, Map data) { - String managementIp = getSelf().getManagementIp(); - String extraIps = HostSystemTags.EXTRA_IPS.getTokenByResourceUuid( - self.getUuid(), HostSystemTags.EXTRA_IPS_TOKEN); - - List allIps = new ArrayList<>(); - allIps.add(managementIp); - if (extraIps != null && !extraIps.isEmpty()) { - for (String ip : extraIps.split(",")) { - String trimmed = ip.trim(); - if (!trimmed.isEmpty() && !allIps.contains(trimmed)) { - allIps.add(trimmed); - } - } - } - - String certIps = String.join(",", allIps); - - String caCert = new JsonLabel().get("libvirtTLSCA", String.class); - String caKey = new JsonLabel().get("libvirtTLSPrivateKey", String.class); - if (caCert == null || caKey == null) { - logger.warn("TLS CA cert/key not found in database, skipping cert update"); - trigger.next(); - return; - } - - UpdateTlsCertCmd cmd = new UpdateTlsCertCmd(); - cmd.setCaCert(caCert); - cmd.setCaKey(caKey); - cmd.setCertIps(certIps); - - new Http<>(updateTlsCertPath, cmd, UpdateTlsCertResponse.class) - .call(new ReturnValueCompletion(trigger) { - @Override - public void success(UpdateTlsCertResponse ret) { - if (!ret.isSuccess()) { - logger.warn(String.format("Failed to update TLS certs on host[uuid:%s]: %s", - self.getUuid(), ret.getError())); - } - // cert update failure should not block reconnect - trigger.next(); - } - - @Override - public void fail(ErrorCode errorCode) { - logger.warn(String.format("Failed to update TLS certs on host[uuid:%s]: %s", - self.getUuid(), errorCode)); - // cert update failure should not block reconnect - trigger.next(); - } - }); - } - }); if (info.isNewAdded()) { flow(new NoRollbackFlow() { diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy index d4a30cef78e..53bc0c46071 100644 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy @@ -4,6 +4,7 @@ import org.springframework.http.HttpEntity import org.zstack.kvm.KVMAgentCommands import org.zstack.kvm.KVMConstant import org.zstack.kvm.KVMGlobalConfig +import org.zstack.kvm.KVMHost import org.zstack.sdk.HostInventory import org.zstack.sdk.UpdateGlobalConfigAction import org.zstack.sdk.VmInstanceInventory @@ -18,6 +19,10 @@ import org.zstack.utils.gson.JSONObjectUtil * Verify that the libvirt TLS configuration (ZSTAC-81343) is correctly * propagated in the MigrateVmCmd sent to kvmagent. * + * TLS certificate deployment is now handled by SSH-based detection + + * ansible deploy (ZSTAC-83696), which skips in unit tests. Only + * migration TLS flag propagation is tested here. + * * Key logic under test (KVMHost.java): * cmd.setUseTls(LIBVIRT_TLS_ENABLED && RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE) * cmd.setSrcHostManagementIp(srcHostMnIp) @@ -126,6 +131,7 @@ class LibvirtTlsMigrateCase extends SubCase { @Override void test() { env.create { + testSanIpParsing() testMigrateWithTlsEnabled() testMigrateWithTlsDisabled() testMigrateWithRestartLibvirtdDisabled() @@ -264,4 +270,30 @@ class LibvirtTlsMigrateCase extends SubCase { value = "true" } } + + void testSanIpParsing() { + // typical openssl SAN output + def sanOutput = " IP Address:10.0.0.10, IP Address:192.168.1.1, DNS:host.example.com\n" + + def ips = KVMHost.parseSanIps(sanOutput) + assert ips.contains("10.0.0.10") + assert ips.contains("192.168.1.1") + assert ips.size() == 2 : "should only contain 2 IPs, got ${ips}" + + // prefix false-positive: 10.0.0.1 must NOT match when only 10.0.0.10 is in SAN + assert !ips.contains("10.0.0.1") : "10.0.0.1 should not match 10.0.0.10" + assert !ips.contains("192.168.1") : "partial IP should not match" + + // null / empty input + assert KVMHost.parseSanIps(null).isEmpty() + assert KVMHost.parseSanIps("").isEmpty() + + // multiline format + def multiline = "X509v3 Subject Alternative Name:\n IP Address:10.0.0.1\n IP Address:10.0.0.10\n" + def mlIps = KVMHost.parseSanIps(multiline) + assert mlIps.contains("10.0.0.1") + assert mlIps.contains("10.0.0.10") + assert mlIps.size() == 2 + assert !mlIps.contains("10.0.0") : "prefix should not match" + } } From bcb77ba168c7f4be70a7c4cca63468140c55e242 Mon Sep 17 00:00:00 2001 From: "yaohua.wu" Date: Tue, 14 Apr 2026 18:30:59 +0800 Subject: [PATCH 71/77] [kvm]: normalize osVersion when matching DVD metadata Strip a leading V/v from osVersion before building the HostOsCategoryVO lookup key, so that distributions that expose VERSION_ID="V10" (notably Kylin Linux Advanced Server) join correctly with DVD metadata that records the same release as plain "10". 1. Why is this change necessary? KvmHypervisorInfoHelper.collectExpectedHypervisorInfoForHosts builds the lookup key as " " and compares it against HostOsCategoryVO.osReleaseVersion. On Kylin V10 hosts KVMHostVO.osVersion is persisted as "V10" straight from /etc/os-release VERSION_ID, while the DVD script /opt/zstack-dvd/x86_64/ky10sp3/Extra/virtualizer-info.sh hard-codes platform.version:10. The two sides build "kylin V10" vs "kylin 10", the join silently returns nothing and matchTargetVersion stays null, leaving every Kylin host in matchState=Unknown even when its qemu-kvm version matches the DVD exactly. Was masked before by the alarm filter only looking at Unmatched. 2. How does it address the problem? Add KvmHypervisorInfoHelper.normalizeOsVersion that strips a leading V/v only when the next character is a digit, so "V10" -> "10" but "Vrouter" stays "Vrouter". Apply the same normalization on both sides: when lookup builds the osReleaseVersion key, and when HypervisorMetadataCollectorImpl writes a fresh metadata row from the DVD script output. With both sides normalized the join succeeds and the existing matchState pipeline reports Matched/Unmatched. 3. Are there any side effects? None on existing CentOS/Rocky/Helix hosts whose osVersion is already a plain numeric string. Existing metadata is rebuilt with the normalized form on the next refreshMetadata call (i.e. MN restart) so no manual data migration is needed. # Summary of changes (by module): - kvm: add normalizeOsVersion helper and apply it both in collectExpectedHypervisorInfoForHosts (lookup) and in HypervisorMetadataCollectorImpl.collectHypervisorMetadata (metadata write). Related: ZSTAC-83682 Change-Id: I4c160f416490e91a08df0acdc5594effb23160e4 --- .../HypervisorMetadataCollectorImpl.java | 3 ++- .../hypervisor/KvmHypervisorInfoHelper.java | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java index 468025b01bd..7118fe89bb4 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java @@ -131,7 +131,8 @@ private boolean collectHypervisorMetadata(HypervisorMetadataDefinition definitio Object platformDistName = properties.get(KEY_PLATFORM_DIST_NAME); Object platformVersion = properties.get(KEY_PLATFORM_VERSION); if (platformDistName != null && platformVersion != null) { - definition.setOsReleaseVersion(String.format("%s %s", platformDistName, platformVersion)); + definition.setOsReleaseVersion(String.format("%s %s", + platformDistName, KvmHypervisorInfoHelper.normalizeOsVersion(platformVersion.toString()))); } else { return false; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java index 4c0b3613024..9ae81bb114c 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java @@ -53,7 +53,7 @@ public static Map collectExpectedHypervisorInfoForHost } HostOperationSystem os = hostOsMap.get(hostUuid); - String osReleaseVersion = String.format("%s %s", os.distribution, os.version); + String osReleaseVersion = String.format("%s %s", os.distribution, normalizeOsVersion(os.version)); Pair key = new Pair<>(architecture, osReleaseVersion); HostOsCategoryVO vo = caches.get(key); @@ -85,6 +85,30 @@ public static Map collectExpectedHypervisorInfoForHost return results; } + /** + * Strip a leading {@code V} or {@code v} from an OS version string when the + * next character is a digit. Some distributions (notably Kylin Linux Advanced + * Server) expose {@code VERSION_ID="V10"} via {@code /etc/os-release}, while + * the matching DVD metadata script outputs the same release as a plain + * {@code 10}. Without normalization the two sides build different + * {@code osReleaseVersion} keys (e.g. {@code "kylin V10"} vs {@code "kylin 10"}) + * and the metadata join silently returns no rows, leaving + * {@code matchTargetVersion} null and the host stuck in {@code Unknown}. + * See ZSTAC-83682. + */ + public static String normalizeOsVersion(String version) { + if (version == null) { + return null; + } + String trimmed = version.trim(); + if (trimmed.length() > 1 + && (trimmed.charAt(0) == 'V' || trimmed.charAt(0) == 'v') + && Character.isDigit(trimmed.charAt(1))) { + return trimmed.substring(1); + } + return trimmed; + } + public static HypervisorVersionState isQemuVersionMatched(String v1, String v2) { if (v1 == null || v2 == null) { return HypervisorVersionState.Unknown; From 22f714fcdadb721ddbd20a0c8fa958618ed68457 Mon Sep 17 00:00:00 2001 From: J M Date: Tue, 14 Apr 2026 23:41:08 +0800 Subject: [PATCH 72/77] [thread]: remove redundant try-catch in CoalesceQueue and fix null backup Remove try-catch around executeBatch since AJ AsyncBackupAspect already wraps Completion/ReturnValueCompletion pointcuts. The try-catch caused double handleFailure when executeBatch threw after completion callback. Fix ReturnValueCoalesceQueue passing null backup to batchCompletion, which caused AJ to re-throw instead of swallowing exceptions. Resolves: ZSTAC-83039 Change-Id: I3b565ba68fa601b33adb4d6b15d0ab4248b5a4da --- .../zstack/core/thread/AbstractCoalesceQueue.java | 14 ++++++-------- .../core/thread/ReturnValueCoalesceQueue.java | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java index 7a97e08b490..579a327160d 100644 --- a/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java +++ b/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java @@ -162,14 +162,12 @@ public void run(SyncTaskChain chain) { // Execute batch with the direct completion object List items = requests.stream().map(req -> req.item).collect(Collectors.toList()); - try { - executeBatch(items, batchCompletion); - } catch (Throwable t) { - logger.warn(String.format("[%s] executeBatch threw exception for signature[%s]", - name, syncSignature), t); - handleFailure(syncSignature, requests, - operr(ORG_ZSTACK_CORE_THREAD_10004, "executeBatch threw exception: %s", t.getMessage()), chain); - } + + /** *(.., AbstractCompletion, ..) is not AsyncSafeAspect's pointcut, but it will call + * executeBatch(.., Completion/ReturnValueCompletion) which is pointcut, + * so we do not need try-catch here. + */ + executeBatch(items, batchCompletion); } @Override diff --git a/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java index 346824647fd..bf52cc93f31 100644 --- a/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java +++ b/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java @@ -28,7 +28,7 @@ protected final void executeBatch(List items, AbstractCompletion batchComplet @Override protected final AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain) { - return new ReturnValueCompletion(null) { + return new ReturnValueCompletion(chain) { @Override public void success(R batchResult) { handleSuccess(syncSignature, requests, batchResult, chain); From 19b35463dcd7d6f9000748b94129ad3891336e19 Mon Sep 17 00:00:00 2001 From: "zhong.xian" Date: Fri, 20 Mar 2026 08:46:12 +0800 Subject: [PATCH 73/77] [sdk]: add shareType field to GPU query APIs Resolves: ZSTAC-82599 Change-Id: I7576776a706d6571756e727165776163736e676b --- sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java | 8 ++++++++ .../main/java/org/zstack/sdk/MdevDeviceSpecInventory.java | 8 ++++++++ sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java | 8 ++++++++ .../main/java/org/zstack/sdk/PciDeviceSpecInventory.java | 8 ++++++++ 4 files changed, 32 insertions(+) diff --git a/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java index e5f8c6e593a..1d8b418e2be 100644 --- a/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java @@ -127,4 +127,12 @@ public java.lang.String getVendor() { return this.vendor; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java b/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java index 4bb5364a115..bdb948d4ef0 100644 --- a/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java @@ -93,4 +93,12 @@ public java.lang.Integer getMaxAvailableDevicesPerHost() { return this.maxAvailableDevicesPerHost; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java index b3d7728f91e..e5cc89cce7d 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java @@ -259,4 +259,12 @@ public java.util.List getMdevSpecRefs() { return this.mdevSpecRefs; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java index 6bead4ce0d9..3f2c33ca2de 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java @@ -165,4 +165,12 @@ public java.lang.Integer getMaxAvailableDevicesPerHost() { return this.maxAvailableDevicesPerHost; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } From 99ae62e227574cf278bafa750a334397259dfefd Mon Sep 17 00:00:00 2001 From: lianghy Date: Wed, 15 Apr 2026 11:40:23 +0800 Subject: [PATCH 74/77] [conf]: bump version to 5.5.16 DBImpact Resolves/Related: ZSTAC-84259 Change-Id: I6f67716c7a616d706b7376637766747678766e6a --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 6d937c2fb57..8fc74fe766b 100755 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ MAJOR=5 MINOR=5 -UPDATE=12 +UPDATE=16 From 1f69b478fdb31878c75ac2c0925b3bfd3cbec916 Mon Sep 17 00:00:00 2001 From: "zhong.xian" Date: Thu, 26 Mar 2026 15:24:13 +0800 Subject: [PATCH 75/77] [kvm]: user vm mount model Add virtiofs-based model mounting capability for user VMs: - Add VmModelMountVO database table and schema with hostUuid tracking - Add AttachVirtiofsCmd/DetachVirtiofsCmd for KVM agent - Add MountModelCenterCmd with storageUrl field - Add SDK actions for mount/unmount/query operations - Add error codes 10138-10149 Resolves: ZSTAC-83157 Change-Id: I746679736f7a7176646e646d797969766f697a76 --- .../global-error-zh_CN.json | 14 ++- .../main/java/org/zstack/kvm/KVMConstant.java | 5 + sdk/src/main/java/SourceClassMap.java | 4 + .../sdk/MountModelToVmInstanceAction.java | 107 ++++++++++++++++++ .../sdk/MountModelToVmInstanceResult.java | 14 +++ .../zstack/sdk/QueryVmModelMountAction.java | 75 ++++++++++++ .../zstack/sdk/QueryVmModelMountResult.java | 22 ++++ .../sdk/UnmountModelFromVmInstanceAction.java | 101 +++++++++++++++++ .../sdk/UnmountModelFromVmInstanceResult.java | 7 ++ .../org/zstack/sdk/VmModelMountInventory.java | 87 ++++++++++++++ .../org/zstack/sdk/VmModelMountStatus.java | 7 ++ .../java/org/zstack/testlib/ApiHelper.groovy | 83 ++++++++++++++ .../CloudOperationsErrorCode.java | 15 +++ 13 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index be35880ac65..4b4f1efae5e 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -4720,5 +4720,17 @@ "ORG_ZSTACK_VPCFIREWALL_10035": "无法添加防火墙规则[%d]因为%s", "ORG_ZSTACK_DGPU_10010": "未找到可用的 dGPU AddOn License,请为 dGPU 产品申请并上传对应授权。", "ORG_ZSTACK_DGPU_10011": "dGPU 产品的 AddOn License 已过期,请及时续期。", - "ORG_ZSTACK_DGPU_10012": "dGPU 授权 GPU 数量不足。License 允许 %d 个 GPU,当前已使用 %d 个,其他节点共享使用 %d 个,本次还需要 %d 个。" + "ORG_ZSTACK_DGPU_10012": "dGPU 授权 GPU 数量不足。License 允许 %d 个 GPU,当前已使用 %d 个,其他节点共享使用 %d 个,本次还需要 %d 个。", + "ORG_ZSTACK_AI_10138": "虚拟机[uuid:%s]未找到", + "ORG_ZSTACK_AI_10139": "模型[uuid:%s]未找到", + "ORG_ZSTACK_AI_10140": "虚拟机[uuid:%s]必须处于运行状态才能挂载模型,当前状态: %s", + "ORG_ZSTACK_AI_10141": "虚拟机[uuid:%s]未运行在任何主机上", + "ORG_ZSTACK_AI_10142": "模型[uuid:%s]属于账户[%s]但虚拟机[uuid:%s]属于账户[%s],无法跨账户挂载模型", + "ORG_ZSTACK_AI_10143": "模型中心[uuid:%s]未找到", + "ORG_ZSTACK_AI_10144": "挂载路径[%s]已被虚拟机[uuid:%s]上的另一个挂载[uuid:%s]使用", + "ORG_ZSTACK_AI_10145": "挂载路径[%s]不允许,不能挂载到系统目录", + "ORG_ZSTACK_AI_10146": "模型安装路径[%s]无效,预期格式: file:///root/bentoml/models/", + "ORG_ZSTACK_AI_10147": "挂载记录[uuid:%s]未找到", + "ORG_ZSTACK_AI_10148": "挂载记录[uuid:%s]未处于已挂载状态,当前状态: %s", + "ORG_ZSTACK_AI_10149": "挂载模型到虚拟机失败: %s" } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 303a12bc6fc..314ff983470 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -97,6 +97,11 @@ public interface KVMConstant { String CLEAN_FIRMWARE_FLASH = "/clean/firmware/flash"; String FSTRIM_VM_PATH = "/vm/fstrim"; + // ZSTAC-83157: virtiofs model mount paths + String KVM_VIRTIOFS_ATTACH_PATH = "/virtiofs/attach"; + String KVM_VIRTIOFS_DETACH_PATH = "/virtiofs/detach"; + String KVM_MODEL_CENTER_MOUNT_PATH = "/modelcenter/mount"; + String ISO_TO = "kvm.isoto"; String ANSIBLE_PLAYBOOK_NAME = "kvm.py"; String ANSIBLE_MODULE_PATH = "ansible/kvm"; diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 1206558cff1..6decef8aaec 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -25,6 +25,8 @@ public class SourceClassMap { put("org.zstack.ai.entity.ModelServiceRefInventory", "org.zstack.sdk.ModelServiceRefInventory"); put("org.zstack.ai.entity.ModelServiceTemplateInventory", "org.zstack.sdk.ModelServiceTemplateInventory"); put("org.zstack.ai.entity.TrainedModelRecordInventory", "org.zstack.sdk.TrainedModelRecordInventory"); + put("org.zstack.ai.entity.VmModelMountInventory", "org.zstack.sdk.VmModelMountInventory"); + put("org.zstack.ai.entity.VmModelMountStatus", "org.zstack.sdk.VmModelMountStatus"); put("org.zstack.ai.message.ArchitectureImageMapping", "org.zstack.sdk.ArchitectureImageMapping"); put("org.zstack.ai.message.MaaSUsage", "org.zstack.sdk.MaaSUsage"); put("org.zstack.ai.message.ModelCenterServiceInventory", "org.zstack.sdk.ModelCenterServiceInventory"); @@ -1565,6 +1567,8 @@ public class SourceClassMap { put("org.zstack.sdk.VmInstancePciDeviceSpecRefInventory", "org.zstack.pciDevice.specification.pci.VmInstancePciDeviceSpecRefInventory"); put("org.zstack.sdk.VmMemoryBillingInventory", "org.zstack.billing.generator.vm.memory.VmMemoryBillingInventory"); put("org.zstack.sdk.VmMemorySpendingDetails", "org.zstack.billing.spendingcalculator.vm.VmMemorySpendingDetails"); + put("org.zstack.sdk.VmModelMountInventory", "org.zstack.ai.entity.VmModelMountInventory"); + put("org.zstack.sdk.VmModelMountStatus", "org.zstack.ai.entity.VmModelMountStatus"); put("org.zstack.sdk.VmNicBandwidthSpendingDetails", "org.zstack.billing.spendingcalculator.vmnic.VmNicBandwidthSpendingDetails"); put("org.zstack.sdk.VmNicInventory", "org.zstack.header.vm.VmNicInventory"); put("org.zstack.sdk.VmNicSecurityGroupRefInventory", "org.zstack.network.securitygroup.VmNicSecurityGroupRefInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java new file mode 100644 index 00000000000..9dfca7a35d1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class MountModelToVmInstanceAction 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.MountModelToVmInstanceResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String modelUuid; + + @Param(required = false, maxLength = 512, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String mountPath; + + @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.MountModelToVmInstanceResult value = res.getResult(org.zstack.sdk.MountModelToVmInstanceResult.class); + ret.value = value == null ? new org.zstack.sdk.MountModelToVmInstanceResult() : 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 = "/vm-model-mounts"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java new file mode 100644 index 00000000000..0feb080e656 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmModelMountInventory; + +public class MountModelToVmInstanceResult { + public VmModelMountInventory inventory; + public void setInventory(VmModelMountInventory inventory) { + this.inventory = inventory; + } + public VmModelMountInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java new file mode 100644 index 00000000000..3205a750d34 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryVmModelMountAction extends QueryAction { + + 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.QueryVmModelMountResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryVmModelMountResult value = res.getResult(org.zstack.sdk.QueryVmModelMountResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryVmModelMountResult() : 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 = "GET"; + info.path = "/vm-model-mounts"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java new file mode 100644 index 00000000000..de488a4a8a0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryVmModelMountResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java new file mode 100644 index 00000000000..677cd311888 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UnmountModelFromVmInstanceAction 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.UnmountModelFromVmInstanceResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @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.UnmountModelFromVmInstanceResult value = res.getResult(org.zstack.sdk.UnmountModelFromVmInstanceResult.class); + ret.value = value == null ? new org.zstack.sdk.UnmountModelFromVmInstanceResult() : 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 = "DELETE"; + info.path = "/vm-model-mounts/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java new file mode 100644 index 00000000000..736cdec965f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class UnmountModelFromVmInstanceResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java b/sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java new file mode 100644 index 00000000000..e328b9292b4 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java @@ -0,0 +1,87 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmModelMountStatus; + +public class VmModelMountInventory { + + 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 vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.lang.String modelUuid; + public void setModelUuid(java.lang.String modelUuid) { + this.modelUuid = modelUuid; + } + public java.lang.String getModelUuid() { + return this.modelUuid; + } + + public java.lang.String modelName; + public void setModelName(java.lang.String modelName) { + this.modelName = modelName; + } + public java.lang.String getModelName() { + return this.modelName; + } + + public java.lang.String mountPath; + public void setMountPath(java.lang.String mountPath) { + this.mountPath = mountPath; + } + public java.lang.String getMountPath() { + return this.mountPath; + } + + public java.lang.String sourcePath; + public void setSourcePath(java.lang.String sourcePath) { + this.sourcePath = sourcePath; + } + public java.lang.String getSourcePath() { + return this.sourcePath; + } + + public VmModelMountStatus status; + public void setStatus(VmModelMountStatus status) { + this.status = status; + } + public VmModelMountStatus getStatus() { + return this.status; + } + + public java.lang.String accountUuid; + public void setAccountUuid(java.lang.String accountUuid) { + this.accountUuid = accountUuid; + } + public java.lang.String getAccountUuid() { + return this.accountUuid; + } + + 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/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java b/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java new file mode 100644 index 00000000000..f4f42f5e8b0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + +public enum VmModelMountStatus { + Mounting, + Mounted, + Unmounting, +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 5a9b5138a3d..db88ce60bae 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -27745,6 +27745,33 @@ abstract class ApiHelper { } + def mountModelToVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.MountModelToVmInstanceAction.class) Closure c) { + def a = new org.zstack.sdk.MountModelToVmInstanceAction() + 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 mountVmInstanceRecoveryPoint(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.MountVmInstanceRecoveryPointAction.class) Closure c) { def a = new org.zstack.sdk.MountVmInstanceRecoveryPointAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -36211,6 +36238,35 @@ abstract class ApiHelper { } + def queryVmModelMount(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryVmModelMountAction.class) Closure c) { + def a = new org.zstack.sdk.QueryVmModelMountAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + 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 queryVmNic(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryVmNicAction.class) Closure c) { def a = new org.zstack.sdk.QueryVmNicAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -42673,6 +42729,33 @@ abstract class ApiHelper { } + def unmountModelFromVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UnmountModelFromVmInstanceAction.class) Closure c) { + def a = new org.zstack.sdk.UnmountModelFromVmInstanceAction() + 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 unmountVmInstanceRecoveryPoint(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UnmountVmInstanceRecoveryPointAction.class) Closure c) { def a = new org.zstack.sdk.UnmountVmInstanceRecoveryPointAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 47e32c7cf90..068d1f50755 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -14904,6 +14904,21 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10142 = "ORG_ZSTACK_AI_10142"; public static final String ORG_ZSTACK_AI_10143 = "ORG_ZSTACK_AI_10143"; + + public static final String ORG_ZSTACK_AI_10144 = "ORG_ZSTACK_AI_10144"; + + public static final String ORG_ZSTACK_AI_10145 = "ORG_ZSTACK_AI_10145"; + + public static final String ORG_ZSTACK_AI_10146 = "ORG_ZSTACK_AI_10146"; + + public static final String ORG_ZSTACK_AI_10147 = "ORG_ZSTACK_AI_10147"; + + public static final String ORG_ZSTACK_AI_10148 = "ORG_ZSTACK_AI_10148"; + + public static final String ORG_ZSTACK_AI_10149 = "ORG_ZSTACK_AI_10149"; + + public static final String ORG_ZSTACK_AI_10157 = "ORG_ZSTACK_AI_10157"; + public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10001 = "ORG_ZSTACK_CORE_CLOUDBUS_10001"; From d913f8480429e6db73be2d83330117b877ca46a3 Mon Sep 17 00:00:00 2001 From: "zhong.xian" Date: Sun, 29 Mar 2026 15:45:48 +0800 Subject: [PATCH 76/77] [conf]: add errorcode and sql Resolves: ZSTAC-83157 Change-Id: I62696a6d667468766a6575656763707374757277 --- conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json | 3 ++- sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java | 1 - .../zstack/utils/clouderrorcode/CloudOperationsErrorCode.java | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 4b4f1efae5e..ff1fda1ebcd 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -4732,5 +4732,6 @@ "ORG_ZSTACK_AI_10146": "模型安装路径[%s]无效,预期格式: file:///root/bentoml/models/", "ORG_ZSTACK_AI_10147": "挂载记录[uuid:%s]未找到", "ORG_ZSTACK_AI_10148": "挂载记录[uuid:%s]未处于已挂载状态,当前状态: %s", - "ORG_ZSTACK_AI_10149": "挂载模型到虚拟机失败: %s" + "ORG_ZSTACK_AI_10149": "挂载模型到虚拟机失败: %s", + "ORG_ZSTACK_AI_10150": "模型[uuid:%s, 名称:%s]已挂载到虚拟机[uuid:%s],挂载路径[%s]。每个模型在每个虚拟机上只能挂载一次" } diff --git a/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java b/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java index f4f42f5e8b0..67222f08987 100644 --- a/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java +++ b/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java @@ -1,7 +1,6 @@ package org.zstack.sdk; public enum VmModelMountStatus { - Mounting, Mounted, Unmounting, } 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 068d1f50755..7dd2e42f08d 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -14916,7 +14916,9 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10148 = "ORG_ZSTACK_AI_10148"; public static final String ORG_ZSTACK_AI_10149 = "ORG_ZSTACK_AI_10149"; - + + public static final String ORG_ZSTACK_AI_10150 = "ORG_ZSTACK_AI_10150"; + public static final String ORG_ZSTACK_AI_10157 = "ORG_ZSTACK_AI_10157"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; From ade3fb7f1e4cfa23984331f979f1cd3b5f79f66a Mon Sep 17 00:00:00 2001 From: "zhong.xian" Date: Tue, 31 Mar 2026 14:29:18 +0800 Subject: [PATCH 77/77] [conf]: Add ORG_ZSTACK_AI_10158 error code" Resolves: ZSTAC-83157 Change-Id: I756c7073707a6468676c70696a7a746870767777 --- conf/db/upgrade/V5.5.16__schema.sql | 21 +++++++++++++++++++ .../global-error-zh_CN.json | 14 ++++++------- conf/springConfigXml/VolumeManager.xml | 2 +- .../CloudOperationsErrorCode.java | 1 + 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/conf/db/upgrade/V5.5.16__schema.sql b/conf/db/upgrade/V5.5.16__schema.sql index 0d8412840ce..8bb7cbbf9d7 100644 --- a/conf/db/upgrade/V5.5.16__schema.sql +++ b/conf/db/upgrade/V5.5.16__schema.sql @@ -344,3 +344,24 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmInstanceDGpuStrategyVO` ( FOREIGN KEY (`gpuDeviceUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- ZSTAC-83157: VM model mount table (virtiofs model mount to user VMs) +CREATE TABLE IF NOT EXISTS `zstack`.`VmModelMountVO` ( + `uuid` VARCHAR(32) NOT NULL, + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `modelUuid` VARCHAR(32) NOT NULL, + `modelName` VARCHAR(256) DEFAULT NULL, + `mountPath` VARCHAR(512) NOT NULL, + `sourcePath` VARCHAR(1024) NOT NULL, + `status` VARCHAR(32) NOT NULL, + `accountUuid` VARCHAR(32) DEFAULT NULL, + `createDate` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + `lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`uuid`), + UNIQUE KEY `uk_vm_mountpath` (`vmInstanceUuid`, `mountPath`(255)), + UNIQUE KEY `uk_vm_model` (`vmInstanceUuid`, `modelUuid`), + CONSTRAINT `fk_vm_model_mount_vm` + FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_model_mount_model` + FOREIGN KEY (`modelUuid`) REFERENCES `zstack`.`ModelVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index ff1fda1ebcd..826e57aa36c 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -4723,15 +4723,15 @@ "ORG_ZSTACK_DGPU_10012": "dGPU 授权 GPU 数量不足。License 允许 %d 个 GPU,当前已使用 %d 个,其他节点共享使用 %d 个,本次还需要 %d 个。", "ORG_ZSTACK_AI_10138": "虚拟机[uuid:%s]未找到", "ORG_ZSTACK_AI_10139": "模型[uuid:%s]未找到", - "ORG_ZSTACK_AI_10140": "虚拟机[uuid:%s]必须处于运行状态才能挂载模型,当前状态: %s", - "ORG_ZSTACK_AI_10141": "虚拟机[uuid:%s]未运行在任何主机上", - "ORG_ZSTACK_AI_10142": "模型[uuid:%s]属于账户[%s]但虚拟机[uuid:%s]属于账户[%s],无法跨账户挂载模型", - "ORG_ZSTACK_AI_10143": "模型中心[uuid:%s]未找到", - "ORG_ZSTACK_AI_10144": "挂载路径[%s]已被虚拟机[uuid:%s]上的另一个挂载[uuid:%s]使用", + "ORG_ZSTACK_AI_10140": "虚拟机「%s」(UUID: %s) 必须处于运行状态才能挂载模型,当前状态: %s", + "ORG_ZSTACK_AI_10141": "虚拟机「%s」(UUID: %s) 未运行在任何主机上", + "ORG_ZSTACK_AI_10142": "模型「%s」(账户: %s) 与虚拟机「%s」(账户: %s) 属于不同账户,无法挂载。\n请确保模型和虚拟机属于同一账户。", + "ORG_ZSTACK_AI_10143": "模型「%s」(UUID: %s) 关联的模型中心(UUID: %s) 未找到。\n请检查模型中心是否已被删除。", + "ORG_ZSTACK_AI_10144": "挂载失败:路径 %s 已被该虚拟机上的另一个挂载占用。\n冲突挂载模型:%s\n冲突挂载UUID:%s\n所属虚拟机:%s\n所属虚拟机UUID:%s\n请先卸载该路径上的已有挂载,或使用其他挂载路径。", "ORG_ZSTACK_AI_10145": "挂载路径[%s]不允许,不能挂载到系统目录", "ORG_ZSTACK_AI_10146": "模型安装路径[%s]无效,预期格式: file:///root/bentoml/models/", "ORG_ZSTACK_AI_10147": "挂载记录[uuid:%s]未找到", - "ORG_ZSTACK_AI_10148": "挂载记录[uuid:%s]未处于已挂载状态,当前状态: %s", + "ORG_ZSTACK_AI_10148": "挂载记录「%s」(UUID: %s) 未处于已挂载状态,当前状态: %s", "ORG_ZSTACK_AI_10149": "挂载模型到虚拟机失败: %s", - "ORG_ZSTACK_AI_10150": "模型[uuid:%s, 名称:%s]已挂载到虚拟机[uuid:%s],挂载路径[%s]。每个模型在每个虚拟机上只能挂载一次" + "ORG_ZSTACK_AI_10150": "模型「%s」(UUID: %s) 已挂载到虚拟机「%s」(UUID: %s),路径: %s。\n每个模型在每个虚拟机上只能挂载一次。" } diff --git a/conf/springConfigXml/VolumeManager.xml b/conf/springConfigXml/VolumeManager.xml index 977134a8873..55a7fbad61c 100755 --- a/conf/springConfigXml/VolumeManager.xml +++ b/conf/springConfigXml/VolumeManager.xml @@ -49,7 +49,7 @@ - + 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 7dd2e42f08d..4a0eaf20428 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -14920,6 +14920,7 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10150 = "ORG_ZSTACK_AI_10150"; public static final String ORG_ZSTACK_AI_10157 = "ORG_ZSTACK_AI_10157"; + public static final String ORG_ZSTACK_AI_10158 = "ORG_ZSTACK_AI_10158"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000";