Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/db/upgrade/V5.5.6__schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
2 changes: 1 addition & 1 deletion conf/i18n/globalErrorCodeMapping/global-error-en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 1 addition & 1 deletion conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class TagPatternInventory {

private TagPatternType type;

private String resourceType;

private Timestamp createDate;

private Timestamp lastOpDate;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}
22 changes: 22 additions & 0 deletions header/src/main/java/org/zstack/header/tag/TagPatternVO.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ public class TagPatternVO extends ResourceVO implements OwnedByAccount {
@Transient
private String accountUuid;

/**
* Limits this tag pattern to a specific resource type (e.g. "ModelVO").
* <p>
* 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.
* <p>
* When filtering tag patterns for a specific resource page, use:
* {@code WHERE resourceType IS NULL OR resourceType = :targetResourceType}
*/
@Column
private String resourceType;

@Column
private Timestamp createDate;

Expand Down Expand Up @@ -106,4 +120,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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class TagPatternVO_ extends ResourceVO_ {
public static volatile SingularAttribute<TagPatternVO, String> description;
public static volatile SingularAttribute<TagPatternVO, String> color;
public static volatile SingularAttribute<TagPatternVO, TagPatternType> type;
public static volatile SingularAttribute<TagPatternVO, String> resourceType;
public static volatile SingularAttribute<TagPatternVO, Timestamp> createDate;
public static volatile SingularAttribute<TagPatternVO, Timestamp> lastOpDate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@ public List<ActiveVolumeClient> 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<ActiveVolumeClient> clients = new ArrayList<>();

if (!rsp.isSuccess()) {
Expand Down Expand Up @@ -1411,6 +1414,11 @@ public class HttpCaller<T extends AgentResponse> {

private boolean tryNext = false;

HttpCaller<T> setTryNext(boolean tryNext) {
this.tryNext = tryNext;
return this;
}

public HttpCaller(String path, AgentCommand cmd, Class<T> retClass, ReturnValueCompletion<T> callback) {
this(path, cmd, retClass, callback, null, 0, false);
}
Expand Down
8 changes: 8 additions & 0 deletions sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TagPatternVO> 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<TagPatternVO> 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<TagPatternVO> 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()
}
}