From 6b4521b38a688cf6072d6fc849171022af1e1490 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 12 Feb 2026 12:02:19 +0800 Subject: [PATCH 1/7] [vm]: use max of virtual and actual size for root disk when no disk offering Resolves: ZSTAC-74683 Change-Id: Id0339ed0221e92e506f60745cde972cc3ee6d9ae --- header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java index 7007c592aea..99ee2173b98 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -847,7 +847,9 @@ public void setBootMode(String bootMode) { public long getRootDiskAllocateSize() { if (rootDiskOffering == null) { - return this.getImageSpec().getInventory().getSize(); + long virtualSize = this.getImageSpec().getInventory().getSize(); + long actualSize = this.getImageSpec().getInventory().getActualSize(); + return Math.max(virtualSize, actualSize); } return rootDiskOffering.getDiskSize(); } From 3b5bda3b76aef968a911d18e35b3b30bd0cab803 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 12 Feb 2026 13:52:13 +0800 Subject: [PATCH 2/7] [zbs]: enable tryNext and 30s timeout for getActiveClients MDS call When anti-split-brain check selects a disconnected MDS node, the HTTP call now times out after 30s instead of 5+ minutes, and automatically retries the next available MDS via tryNext mechanism. Resolves: ZSTAC-80595 Change-Id: I1be80f1b70cad1606eb38d1f0078c8f2781e6941 --- .../org/zstack/storage/zbs/ZbsStorageController.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java index db06239acb3..276ab367ba1 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java @@ -179,7 +179,10 @@ public List getActiveClients(String installPath, String prot if (VolumeProtocol.CBD.toString().equals(protocol)) { GetVolumeClientsCmd cmd = new GetVolumeClientsCmd(); cmd.setPath(installPath); - GetVolumeClientsRsp rsp = syncHttpCall(GET_VOLUME_CLIENTS_PATH, cmd, GetVolumeClientsRsp.class); + GetVolumeClientsRsp rsp = new HttpCaller<>(GET_VOLUME_CLIENTS_PATH, cmd, GetVolumeClientsRsp.class, + null, TimeUnit.SECONDS, 30, true) + .setTryNext(true) + .syncCall(); List clients = new ArrayList<>(); if (!rsp.isSuccess()) { @@ -1411,6 +1414,11 @@ public class HttpCaller { private boolean tryNext = false; + HttpCaller setTryNext(boolean tryNext) { + this.tryNext = tryNext; + return this; + } + public HttpCaller(String path, AgentCommand cmd, Class retClass, ReturnValueCompletion callback) { this(path, cmd, retClass, callback, null, 0, false); } From 80df074f8dd1140b278ce0979f2068d5c271d8e5 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 12 Feb 2026 14:22:40 +0800 Subject: [PATCH 3/7] [vm]: add Destroying->Stopped state transition When MN restarts during a destroy operation, the hypervisor may report the VM as Stopped. Without this transition, the state machine throws an exception and the VM stays stuck in Destroying state forever. Resolves: ZSTAC-80620 Change-Id: I037edba70d145a44a88ce0d3573089182fedb162 --- header/src/main/java/org/zstack/header/vm/VmInstanceState.java | 1 + 1 file changed, 1 insertion(+) diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceState.java b/header/src/main/java/org/zstack/header/vm/VmInstanceState.java index 8a755b52fda..49303e23252 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceState.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceState.java @@ -168,6 +168,7 @@ public enum VmInstanceState { new Transaction(VmInstanceStateEvent.destroyed, VmInstanceState.Destroyed), new Transaction(VmInstanceStateEvent.destroying, VmInstanceState.Destroying), new Transaction(VmInstanceStateEvent.running, VmInstanceState.Running), + new Transaction(VmInstanceStateEvent.stopped, VmInstanceState.Stopped), new Transaction(VmInstanceStateEvent.expunging, VmInstanceState.Expunging) ); Destroyed.transactions( From 24d4f3b4870ea72fce77bcf64980d0b70b868502 Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Fri, 13 Feb 2026 13:32:45 +0800 Subject: [PATCH 4/7] [i18n]: improve snapshot error message for unattached volume Resolves: ZSTAC-82153 Change-Id: Ib51c2e21553277416d1a9444be55aca2aa4b2fc4 --- conf/i18n/globalErrorCodeMapping/global-error-en_US.json | 2 +- conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index 32eb4c8f056..715e823d95e 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -3374,7 +3374,7 @@ "ORG_ZSTACK_NETWORK_HUAWEI_IMASTER_10019": "delete token of SDN controller [IP:%s] failed because %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10004": "Cannot execute volume mapping to host flow due to invalid volume ID.%s", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10007": "port forwarding rule [uuid:%s] has not been attached to any virtual machine network interface, cannot detach", - "ORG_ZSTACK_MEVOCO_10088": "cannot take a snapshot for volumes[%s] when volume[uuid: %s] is not attached", + "ORG_ZSTACK_MEVOCO_10088": "cannot create snapshot for volume[uuid:%s] because it is not attached to any VM instance. Please attach the volume to a VM first. Affected volumes: %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10005": "Cannot execute map LUN to host flow due to invalid LUN type: %s", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10008": "port forwarding rule [uuid:%s] has been associated with vm nic [uuid:%s], cannot be reassigned again", "ORG_ZSTACK_MEVOCO_10087": "A Running VM[uuid:%s] has no associated Host UUID.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 84609838ddc..01960e8eb45 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -3374,7 +3374,7 @@ "ORG_ZSTACK_NETWORK_HUAWEI_IMASTER_10019": "删除 SDN 控制器 [IP:%s] 的令牌失败,因为 %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10004": "无法执行映射LUN到主机流程,无效的LUN ID", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10007": "端口转发规则 rule[uuid:%s] 没有绑定到任何 VM 的网卡上,无法解除绑定", - "ORG_ZSTACK_MEVOCO_10088": "无法为挂载状态以外的卷[%s]创建快照", + "ORG_ZSTACK_MEVOCO_10088": "无法为云盘[uuid:%s]创建快照,因为该云盘未挂载到任何云主机。请先将云盘挂载到云主机后再创建快照。相关云盘: %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10005": "无法执行映射LUN到主机流程,无效的LUN类型", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10008": "端口转发规则[uuid:%s]已绑定到VM网卡[uuid:%s],无法再次绑定", "ORG_ZSTACK_MEVOCO_10087": "如何一个运行中的VM[uuid:%s]没有宿主机uuid?", From 800d01db2affca0dea1ea9f8ac9d5676791c0bfa Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Thu, 19 Feb 2026 16:33:17 +0800 Subject: [PATCH 5/7] [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 6/7] [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 7/7] [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;