From 8f43cbef783516e100b3257c4b724b31d76c4612 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Mon, 25 May 2026 17:38:17 +0800 Subject: [PATCH 1/3] [storage]: return group tree in APIQueryVolumeSnapshotTree Add groupTrees field to APIQueryVolumeSnapshotTreeReply, built from VolumeSnapshotGroupVO topology with vote-majority parent resolution. Extract build logic into VolumeSnapshotGroupTreeBuilder. Resolves: ZSV-9792 Change-Id: I71616f6f6f6c637a6167637863727575746f616c --- .../APIQueryVolumeSnapshotTreeReply.java | 10 + ...eryVolumeSnapshotTreeReplyDoc_zh_cn.groovy | 9 + .../VolumeSnapshotGroupTreeInventory.java | 107 +++++++ ...SnapshotGroupTreeInventoryDoc_zh_cn.groovy | 79 +++++ .../VolumeSnapshotGroupTreeRefInventory.java | 60 ++++ ...pshotGroupTreeRefInventoryDoc_zh_cn.groovy | 47 +++ .../snapshot/VolumeSnapshotManagerImpl.java | 36 ++- .../snapshot/VolumeSnapshotTreeBase.java | 53 ++-- .../group/VolumeSnapshotGroupTreeBuilder.java | 283 ++++++++++++++++++ 9 files changed, 664 insertions(+), 20 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy create mode 100644 storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java index 522ec92d29b..c759485391b 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java @@ -2,6 +2,7 @@ import org.zstack.header.query.APIQueryReply; import org.zstack.header.rest.RestResponse; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory; import org.zstack.header.volume.VolumeType; import java.util.List; @@ -11,6 +12,7 @@ @RestResponse(allTo = "inventories") public class APIQueryVolumeSnapshotTreeReply extends APIQueryReply { private List inventories; + private List groupTrees; public List getInventories() { return inventories; @@ -19,6 +21,14 @@ public List getInventories() { public void setInventories(List inventories) { this.inventories = inventories; } + + public List getGroupTrees() { + return groupTrees; + } + + public void setGroupTrees(List groupTrees) { + this.groupTrees = groupTrees; + } public static APIQueryVolumeSnapshotTreeReply __example__() { APIQueryVolumeSnapshotTreeReply reply = new APIQueryVolumeSnapshotTreeReply(); diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy index e0ea093436e..7b070b9a77b 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy @@ -1,6 +1,7 @@ package org.zstack.header.storage.snapshot import org.zstack.header.errorcode.ErrorCode +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory doc { @@ -28,4 +29,12 @@ doc { since "0.6" clz VolumeSnapshotTreeInventory.class } + ref { + name "groupTrees" + path "org.zstack.header.storage.snapshot.APIQueryVolumeSnapshotTreeReply.groupTrees" + desc "虚拟机快照组树清单(仅当查询条件包含 volumeUuid eq <根云盘UUID> 时返回)" + type "List" + since "5.0" + clz VolumeSnapshotGroupTreeInventory.class + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java new file mode 100644 index 00000000000..ca23b47de0c --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java @@ -0,0 +1,107 @@ +package org.zstack.header.storage.snapshot.group; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class VolumeSnapshotGroupTreeInventory { + private String uuid; + private String name; + private String description; + private String vmInstanceUuid; + private Timestamp createDate; + private Timestamp lastOpDate; + private boolean current; + private boolean incomplete; + private String parentGroupUuid; + private List children = new ArrayList<>(); + private List refs = new ArrayList<>(); + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + 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; + } + + public boolean isCurrent() { + return current; + } + + public void setCurrent(boolean current) { + this.current = current; + } + + public boolean isIncomplete() { + return incomplete; + } + + public void setIncomplete(boolean incomplete) { + this.incomplete = incomplete; + } + + public String getParentGroupUuid() { + return parentGroupUuid; + } + + public void setParentGroupUuid(String parentGroupUuid) { + this.parentGroupUuid = parentGroupUuid; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public List getRefs() { + return refs; + } + + public void setRefs(List refs) { + this.refs = refs; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..bd2ab05c69d --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy @@ -0,0 +1,79 @@ +package org.zstack.header.storage.snapshot.group + +import java.sql.Timestamp + +doc { + + title "虚拟机快照组树清单" + + field { + name "uuid" + desc "快照组UUID" + type "String" + since "5.0" + } + field { + name "name" + desc "快照组名称" + type "String" + since "5.0" + } + field { + name "description" + desc "快照组描述" + type "String" + since "5.0" + } + field { + name "vmInstanceUuid" + desc "虚拟机UUID" + type "String" + since "5.0" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.0" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.0" + } + field { + name "current" + desc "是否是当前快照组(虚拟机维度)" + type "boolean" + since "5.0" + } + field { + name "incomplete" + desc "是否为残缺快照组(部分盘的快照已被删除)" + type "boolean" + since "5.0" + } + field { + name "parentGroupUuid" + desc "父快照组UUID" + type "String" + since "5.0" + } + ref { + name "children" + path "org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory.children" + desc "子快照组列表" + type "List" + since "5.0" + clz VolumeSnapshotGroupTreeInventory.class + } + ref { + name "refs" + path "org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory.refs" + desc "快照组成员盘列表" + type "List" + since "5.0" + clz VolumeSnapshotGroupTreeRefInventory.class + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java new file mode 100644 index 00000000000..47ccdbf7c43 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java @@ -0,0 +1,60 @@ +package org.zstack.header.storage.snapshot.group; + +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; + +public class VolumeSnapshotGroupTreeRefInventory { + private String volumeUuid; + private String volumeName; + private String volumeType; + private String volumeSnapshotUuid; + private boolean snapshotDeleted; + private VolumeSnapshotInventory snapshot; + + public String getVolumeUuid() { + return volumeUuid; + } + + public void setVolumeUuid(String volumeUuid) { + this.volumeUuid = volumeUuid; + } + + public String getVolumeName() { + return volumeName; + } + + public void setVolumeName(String volumeName) { + this.volumeName = volumeName; + } + + public String getVolumeType() { + return volumeType; + } + + public void setVolumeType(String volumeType) { + this.volumeType = volumeType; + } + + public String getVolumeSnapshotUuid() { + return volumeSnapshotUuid; + } + + public void setVolumeSnapshotUuid(String volumeSnapshotUuid) { + this.volumeSnapshotUuid = volumeSnapshotUuid; + } + + public boolean isSnapshotDeleted() { + return snapshotDeleted; + } + + public void setSnapshotDeleted(boolean snapshotDeleted) { + this.snapshotDeleted = snapshotDeleted; + } + + public VolumeSnapshotInventory getSnapshot() { + return snapshot; + } + + public void setSnapshot(VolumeSnapshotInventory snapshot) { + this.snapshot = snapshot; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..43ea8bc700f --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy @@ -0,0 +1,47 @@ +package org.zstack.header.storage.snapshot.group + +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory + +doc { + + title "虚拟机快照组成员盘清单" + + field { + name "volumeUuid" + desc "云盘UUID" + type "String" + since "5.0" + } + field { + name "volumeName" + desc "云盘名称" + type "String" + since "5.0" + } + field { + name "volumeType" + desc "云盘类型(Root/Data)" + type "String" + since "5.0" + } + field { + name "volumeSnapshotUuid" + desc "对应的云盘快照UUID" + type "String" + since "5.0" + } + field { + name "snapshotDeleted" + desc "对应的云盘快照是否已被删除" + type "boolean" + since "5.0" + } + ref { + name "snapshot" + path "org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeRefInventory.snapshot" + desc "对应的云盘快照清单(被删除时为null,无访问权限时仅返回uuid)" + type "VolumeSnapshotInventory" + since "5.0" + clz VolumeSnapshotInventory.class + } +} diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java index 1f936ca6141..bc9e8bb5e08 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java @@ -27,6 +27,8 @@ import org.zstack.header.identity.*; import org.zstack.header.identity.quota.QuotaMessageHandler; import org.zstack.header.message.*; +import org.zstack.header.query.QueryCondition; +import org.zstack.header.query.QueryOp; import org.zstack.header.storage.backup.CleanUpVmBackupExtensionPoint; import org.zstack.header.storage.primary.*; import org.zstack.header.storage.primary.VolumeSnapshotCapability.VolumeSnapshotArrangementType; @@ -41,6 +43,7 @@ import org.zstack.storage.snapshot.group.MemorySnapshotGroupReferenceFactory; import org.zstack.storage.snapshot.group.VolumeSnapshotGroupBase; import org.zstack.storage.snapshot.group.VolumeSnapshotGroupChecker; +import org.zstack.storage.snapshot.group.VolumeSnapshotGroupTreeBuilder; import org.zstack.storage.snapshot.reference.VolumeSnapshotReferenceTreeBase; import org.zstack.storage.snapshot.reference.VolumeSnapshotReferenceUtils; import org.zstack.storage.volume.FireSnapShotCanonicalEvent; @@ -1338,11 +1341,14 @@ public List getReplyMessageClassForMarshalExtensionPoint() { @Override public void marshalReplyMessageBeforeSending(Message replyOrEvent, NeedReplyMessage msg) { if (replyOrEvent instanceof APIQueryVolumeSnapshotTreeReply) { - marshal(((APIMessage) msg).getSession(), (APIQueryVolumeSnapshotTreeReply) replyOrEvent); + APIQueryVolumeSnapshotTreeReply reply = (APIQueryVolumeSnapshotTreeReply) replyOrEvent; + APIQueryVolumeSnapshotTreeMsg apiMsg = (APIQueryVolumeSnapshotTreeMsg) msg; + marshalVolumeTrees(apiMsg.getSession(), reply); + marshalGroupTrees(apiMsg, reply); } } - private void marshal(SessionInventory session, APIQueryVolumeSnapshotTreeReply reply) { + private void marshalVolumeTrees(SessionInventory session, APIQueryVolumeSnapshotTreeReply reply) { if (reply.getInventories() == null) { // this is for count return; @@ -1357,6 +1363,32 @@ private void marshal(SessionInventory session, APIQueryVolumeSnapshotTreeReply r } } + private final VolumeSnapshotGroupTreeBuilder groupTreeBuilder = new VolumeSnapshotGroupTreeBuilder(); + + private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolumeSnapshotTreeReply reply) { + if (reply.getInventories() == null) { + return; + } + + String volumeUuid = groupTreeBuilder.extractVolumeUuidCondition(msg); + if (volumeUuid == null) { + return; + } + + boolean authorized = reply.getInventories().stream() + .anyMatch(inv -> volumeUuid.equals(inv.getVolumeUuid())); + if (!authorized) { + return; + } + + String vmInstanceUuid = groupTreeBuilder.findRootVmUuid(volumeUuid); + if (vmInstanceUuid == null) { + return; + } + + reply.setGroupTrees(groupTreeBuilder.buildForVm(vmInstanceUuid)); + } + @SuppressWarnings("unchecked") private Set querySnapshotUuids(String treeUuid, SessionInventory session) { String zql = String.format("query volumesnapshot.uuid where treeUuid = '%s'", treeUuid); diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java index beb8b044d1a..86cf38ea9fa 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java @@ -1433,9 +1433,10 @@ private void ungroupAfterDeleteSingleSnapshot(VolumeSnapshotInventory volumeSnap .eq(VolumeSnapshotGroupRefVO_.snapshotDeleted, false).count(); if (count == 0) { cleanVmHostBackupFilesForGroup(list(volumeSnapshotInv.getGroupUuid())); + logger.debug(String.format("snapshot group[uuid:%s] all refs deleted, disbanding", + volumeSnapshotInv.getGroupUuid())); + vidm.deleteArchiveVmInstanceResourceMetadataGroup(volumeSnapshotInv.getGroupUuid()); dbf.removeByPrimaryKey(volumeSnapshotInv.getGroupUuid(), VolumeSnapshotGroupVO.class); - logger.debug(String.format("snapshot group[uuid:%s] all volume snapshot has been deleted, " + - "delete snapshot group", volumeSnapshotInv.getGroupUuid())); } } @@ -2142,27 +2143,43 @@ protected Boolean scripts() { return cleanup; } - // The logic for cleaning up snapshot groups when deleting a snapshot chain + // Mark deleted refs and disband a group only after all its refs are deleted. private void ungroupAfterDeleted(List snapshots) { - List uuids = snapshots.stream().map(VolumeSnapshotInventory::getUuid).collect(Collectors.toList()); - SQL.New(VolumeSnapshotGroupRefVO.class).in(VolumeSnapshotGroupRefVO_.volumeSnapshotUuid, uuids) - .set(VolumeSnapshotGroupRefVO_.snapshotDeleted, true).update(); - if (currentRoot.getVolumeType().equals(VolumeType.Root.toString())) { - List groupUuids = new ArrayList<>(); - for (VolumeSnapshotInventory snapshot : snapshots) { - String groupUuid = snapshot.getGroupUuid(); - if (groupUuid != null) { - logger.debug(String.format("root volume snapshot[uuid:%s, name:%s] has been deleted, " + - "ungroup snapshot group[uuid:%s]", snapshot.getUuid(), snapshot.getName(), groupUuid)); - groupUuids.add(groupUuid); - } + List uuids = snapshots.stream() + .map(VolumeSnapshotInventory::getUuid) + .collect(Collectors.toList()); + SQL.New(VolumeSnapshotGroupRefVO.class) + .in(VolumeSnapshotGroupRefVO_.volumeSnapshotUuid, uuids) + .set(VolumeSnapshotGroupRefVO_.snapshotDeleted, true) + .update(); + + Set touchedGroupUuids = snapshots.stream() + .map(VolumeSnapshotInventory::getGroupUuid) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (touchedGroupUuids.isEmpty()) { + return; + } + List groupsToDelete = new ArrayList<>(); + for (String groupUuid : touchedGroupUuids) { + long remaining = Q.New(VolumeSnapshotGroupRefVO.class) + .eq(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuid) + .eq(VolumeSnapshotGroupRefVO_.snapshotDeleted, false) + .count(); + if (remaining == 0) { + logger.debug(String.format("snapshot group[uuid:%s] all refs deleted, disbanding", groupUuid)); + groupsToDelete.add(groupUuid); } + } - groupUuids.forEach(groupUuid -> vidm.deleteArchiveVmInstanceResourceMetadataGroup(groupUuid)); - cleanVmHostBackupFilesForGroup(groupUuids); - dbf.removeByPrimaryKeys(groupUuids, VolumeSnapshotGroupVO.class); + if (groupsToDelete.isEmpty()) { + return; } + + groupsToDelete.forEach(vidm::deleteArchiveVmInstanceResourceMetadataGroup); + cleanVmHostBackupFilesForGroup(groupsToDelete); + dbf.removeByPrimaryKeys(groupsToDelete, VolumeSnapshotGroupVO.class); } private void cleanVmHostBackupFilesForGroup(List groupUuids) { diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java new file mode 100644 index 00000000000..fccd0d78234 --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java @@ -0,0 +1,283 @@ +package org.zstack.storage.snapshot.group; + +import org.zstack.core.db.Q; +import org.zstack.header.query.QueryCondition; +import org.zstack.header.query.QueryOp; +import org.zstack.header.storage.snapshot.APIQueryVolumeSnapshotTreeMsg; +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeRefInventory; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.volume.VolumeType; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class VolumeSnapshotGroupTreeBuilder { + private static final CLogger logger = Utils.getLogger(VolumeSnapshotGroupTreeBuilder.class); + + public List buildForVm(String vmInstanceUuid) { + List groupVOs = Q.New(VolumeSnapshotGroupVO.class) + .eq(VolumeSnapshotGroupVO_.vmInstanceUuid, vmInstanceUuid) + .list(); + if (groupVOs.isEmpty()) { + return Collections.emptyList(); + } + + List groupUuids = groupVOs.stream() + .map(VolumeSnapshotGroupVO::getUuid) + .collect(Collectors.toList()); + + List refs = Q.New(VolumeSnapshotGroupRefVO.class) + .in(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuids) + .list(); + + Map snapVOs = loadLiveSnapshotVOs(refs); + Map parentMap = buildParentMap(snapVOs); + Map snapToGroup = buildSnapToGroupMap(refs); + + Map> refsByGroup = refs.stream() + .collect(Collectors.groupingBy(VolumeSnapshotGroupRefVO::getVolumeSnapshotGroupUuid)); + + Map groupNodeMap = + buildGroupNodes(groupVOs, refsByGroup, snapVOs); + + Map groupCreateDate = groupVOs.stream() + .collect(HashMap::new, + (m, g) -> m.put(g.getUuid(), g.getCreateDate()), + HashMap::putAll); + + linkParents(groupNodeMap, refsByGroup, parentMap, snapToGroup, groupCreateDate); + + return assembleForest(groupNodeMap); + } + + public String extractVolumeUuidCondition(APIQueryVolumeSnapshotTreeMsg msg) { + List conditions = msg.getConditions(); + if (conditions == null) { + return null; + } + String found = null; + for (QueryCondition c : conditions) { + if (!"volumeUuid".equals(c.getName())) { + continue; + } + if (!QueryOp.EQ.toString().equals(c.getOp())) { + continue; + } + if (found != null && !found.equals(c.getValue())) { + logger.warn(String.format("multiple conflicting volumeUuid eq conditions in APIQueryVolumeSnapshotTreeMsg: %s vs %s; taking the first", + found, c.getValue())); + return found; + } + found = c.getValue(); + } + return found; + } + + public String findRootVmUuid(String volumeUuid) { + return Q.New(VolumeVO.class) + .select(VolumeVO_.vmInstanceUuid) + .eq(VolumeVO_.uuid, volumeUuid) + .eq(VolumeVO_.type, VolumeType.Root) + .findValue(); + } + + private Map loadLiveSnapshotVOs(List refs) { + List liveSnapUuids = refs.stream() + .filter(r -> !r.isSnapshotDeleted()) + .map(VolumeSnapshotGroupRefVO::getVolumeSnapshotUuid) + .collect(Collectors.toList()); + if (liveSnapUuids.isEmpty()) { + return Collections.emptyMap(); + } + List svos = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.uuid, liveSnapUuids) + .list(); + return svos.stream().collect(Collectors.toMap(VolumeSnapshotVO::getUuid, v -> v)); + } + + private Map buildParentMap(Map snapVOs) { + Map m = new HashMap<>(); + for (VolumeSnapshotVO v : snapVOs.values()) { + m.put(v.getUuid(), v.getParentUuid()); + } + return m; + } + + private Map buildSnapToGroupMap(List refs) { + Map m = new HashMap<>(); + for (VolumeSnapshotGroupRefVO r : refs) { + m.put(r.getVolumeSnapshotUuid(), r.getVolumeSnapshotGroupUuid()); + } + return m; + } + + private Map buildGroupNodes( + List groupVOs, + Map> refsByGroup, + Map snapVOs) { + Map groupNodeMap = new HashMap<>(); + for (VolumeSnapshotGroupVO g : groupVOs) { + VolumeSnapshotGroupTreeInventory node = new VolumeSnapshotGroupTreeInventory(); + node.setUuid(g.getUuid()); + node.setName(g.getName()); + node.setDescription(g.getDescription()); + node.setVmInstanceUuid(g.getVmInstanceUuid()); + node.setCreateDate(g.getCreateDate()); + node.setLastOpDate(g.getLastOpDate()); + + List groupRefs = refsByGroup.getOrDefault(g.getUuid(), Collections.emptyList()); + long deletedCount = groupRefs.stream().filter(VolumeSnapshotGroupRefVO::isSnapshotDeleted).count(); + int total = groupRefs.size(); + node.setIncomplete(deletedCount > 0 && deletedCount < total); + + node.setRefs(buildRefInventories(groupRefs, snapVOs)); + groupNodeMap.put(g.getUuid(), node); + } + return groupNodeMap; + } + + private List buildRefInventories( + List groupRefs, + Map snapVOs) { + List refInvs = new ArrayList<>(); + for (VolumeSnapshotGroupRefVO r : groupRefs) { + VolumeSnapshotGroupTreeRefInventory refInv = new VolumeSnapshotGroupTreeRefInventory(); + refInv.setVolumeUuid(r.getVolumeUuid()); + refInv.setVolumeName(r.getVolumeName()); + refInv.setVolumeType(r.getVolumeType()); + refInv.setVolumeSnapshotUuid(r.getVolumeSnapshotUuid()); + refInv.setSnapshotDeleted(r.isSnapshotDeleted()); + if (r.isSnapshotDeleted()) { + refInv.setSnapshot(null); + } else { + VolumeSnapshotVO svo = snapVOs.get(r.getVolumeSnapshotUuid()); + refInv.setSnapshot(svo == null ? null : VolumeSnapshotInventory.valueOf(svo)); + } + refInvs.add(refInv); + } + return refInvs; + } + + private void linkParents(Map groupNodeMap, + Map> refsByGroup, + Map parentMap, + Map snapToGroup, + Map groupCreateDate) { + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + String parentGroupUuid = resolveParentGroupUuid(node.getUuid(), + refsByGroup.getOrDefault(node.getUuid(), Collections.emptyList()), + parentMap, snapToGroup, groupCreateDate); + if (parentGroupUuid != null && groupNodeMap.containsKey(parentGroupUuid)) { + node.setParentGroupUuid(parentGroupUuid); + } + } + } + + private String resolveParentGroupUuid(String selfGroupUuid, + List selfRefs, + Map parentMap, + Map snapToGroup, + Map groupCreateDate) { + Map votes = new HashMap<>(); + for (VolumeSnapshotGroupRefVO r : selfRefs) { + if (r.isSnapshotDeleted()) { + continue; + } + String cur = parentMap.get(r.getVolumeSnapshotUuid()); + Set visited = new HashSet<>(); + visited.add(r.getVolumeSnapshotUuid()); + while (cur != null && !visited.contains(cur)) { + visited.add(cur); + String g = snapToGroup.get(cur); + if (g != null && !g.equals(selfGroupUuid)) { + votes.merge(g, 1, Integer::sum); + break; + } + cur = parentMap.get(cur); + } + } + if (votes.isEmpty()) { + return null; + } + + List> ranked = new ArrayList<>(votes.entrySet()); + ranked.sort((a, b) -> { + int cmp = Integer.compare(b.getValue(), a.getValue()); + if (cmp != 0) { + return cmp; + } + Date da = groupCreateDate.get(a.getKey()); + Date db = groupCreateDate.get(b.getKey()); + cmp = Comparator.nullsLast(Date::compareTo).compare(da, db); + if (cmp != 0) { + return cmp; + } + return a.getKey().compareTo(b.getKey()); + }); + + String winner = ranked.get(0).getKey(); + if (ranked.size() > 1 && ranked.get(1).getValue().equals(ranked.get(0).getValue())) { + logger.warn(String.format("group[uuid:%s] has tied parentGroup votes: %s; picked %s by (createDate asc, uuid asc)", + selfGroupUuid, votes, winner)); + } + return winner; + } + + private List assembleForest(Map groupNodeMap) { + List forest = new ArrayList<>(); + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + if (node.getParentGroupUuid() == null) { + forest.add(node); + } else { + groupNodeMap.get(node.getParentGroupUuid()).getChildren().add(node); + } + } + + Comparator byCreateDateAsc = + Comparator.comparing(VolumeSnapshotGroupTreeInventory::getCreateDate, + Comparator.nullsFirst(Comparator.naturalOrder())); + forest.sort(byCreateDateAsc); + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + node.getChildren().sort(byCreateDateAsc); + } + + markCurrent(groupNodeMap); + return forest; + } + + private void markCurrent(Map groupNodeMap) { + VolumeSnapshotGroupTreeInventory newest = null; + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + if (newest == null) { + newest = node; + continue; + } + if (node.getCreateDate() != null && (newest.getCreateDate() == null + || node.getCreateDate().after(newest.getCreateDate()))) { + newest = node; + } + } + if (newest != null) { + newest.setCurrent(true); + } + } +} From cb342d7026531483460849e50e303b1cd0230745 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Fri, 29 May 2026 15:23:45 +0800 Subject: [PATCH 2/3] [storage]: fix parent chain & cross-volume leak in group tree Two P1 fixes from review: 1. parent map now includes deleted snapshots so chain traversal can cross the deleted point and reach ancestor group. 2. when query is scoped to a single volume, filter refs and groupVOs by the visible volume set in the reply to avoid leaking snapshots of sibling volumes the caller cannot see. Resolves: ZSV-9792 Change-Id: I582aa250977fcaf9f5a2f0368d5891518949e32b --- .../snapshot/VolumeSnapshotManagerImpl.java | 9 ++--- .../group/VolumeSnapshotGroupTreeBuilder.java | 35 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java index bc9e8bb5e08..2f3e1285576 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java @@ -1375,9 +1375,10 @@ private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolume return; } - boolean authorized = reply.getInventories().stream() - .anyMatch(inv -> volumeUuid.equals(inv.getVolumeUuid())); - if (!authorized) { + Set visibleVolumeUuids = reply.getInventories().stream() + .map(VolumeSnapshotTreeInventory::getVolumeUuid) + .collect(Collectors.toSet()); + if (!visibleVolumeUuids.contains(volumeUuid)) { return; } @@ -1386,7 +1387,7 @@ private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolume return; } - reply.setGroupTrees(groupTreeBuilder.buildForVm(vmInstanceUuid)); + reply.setGroupTrees(groupTreeBuilder.buildForVm(vmInstanceUuid, visibleVolumeUuids)); } @SuppressWarnings("unchecked") diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java index fccd0d78234..90223235672 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java @@ -34,6 +34,10 @@ public class VolumeSnapshotGroupTreeBuilder { private static final CLogger logger = Utils.getLogger(VolumeSnapshotGroupTreeBuilder.class); public List buildForVm(String vmInstanceUuid) { + return buildForVm(vmInstanceUuid, null); + } + + public List buildForVm(String vmInstanceUuid, Set visibleVolumeUuids) { List groupVOs = Q.New(VolumeSnapshotGroupVO.class) .eq(VolumeSnapshotGroupVO_.vmInstanceUuid, vmInstanceUuid) .list(); @@ -49,15 +53,30 @@ public List buildForVm(String vmInstanceUuid) .in(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuids) .list(); - Map snapVOs = loadLiveSnapshotVOs(refs); - Map parentMap = buildParentMap(snapVOs); + if (visibleVolumeUuids != null) { + refs = refs.stream() + .filter(r -> visibleVolumeUuids.contains(r.getVolumeUuid())) + .collect(Collectors.toList()); + if (refs.isEmpty()) { + return Collections.emptyList(); + } + Set visibleGroupUuids = refs.stream() + .map(VolumeSnapshotGroupRefVO::getVolumeSnapshotGroupUuid) + .collect(Collectors.toSet()); + groupVOs = groupVOs.stream() + .filter(g -> visibleGroupUuids.contains(g.getUuid())) + .collect(Collectors.toList()); + } + + Map liveSnapVOs = loadSnapshotVOs(refs, true); + Map parentMap = buildParentMap(loadSnapshotVOs(refs, false)); Map snapToGroup = buildSnapToGroupMap(refs); Map> refsByGroup = refs.stream() .collect(Collectors.groupingBy(VolumeSnapshotGroupRefVO::getVolumeSnapshotGroupUuid)); Map groupNodeMap = - buildGroupNodes(groupVOs, refsByGroup, snapVOs); + buildGroupNodes(groupVOs, refsByGroup, liveSnapVOs); Map groupCreateDate = groupVOs.stream() .collect(HashMap::new, @@ -100,16 +119,16 @@ public String findRootVmUuid(String volumeUuid) { .findValue(); } - private Map loadLiveSnapshotVOs(List refs) { - List liveSnapUuids = refs.stream() - .filter(r -> !r.isSnapshotDeleted()) + private Map loadSnapshotVOs(List refs, boolean liveOnly) { + List snapUuids = refs.stream() + .filter(r -> !liveOnly || !r.isSnapshotDeleted()) .map(VolumeSnapshotGroupRefVO::getVolumeSnapshotUuid) .collect(Collectors.toList()); - if (liveSnapUuids.isEmpty()) { + if (snapUuids.isEmpty()) { return Collections.emptyMap(); } List svos = Q.New(VolumeSnapshotVO.class) - .in(VolumeSnapshotVO_.uuid, liveSnapUuids) + .in(VolumeSnapshotVO_.uuid, snapUuids) .list(); return svos.stream().collect(Collectors.toMap(VolumeSnapshotVO::getUuid, v -> v)); } From c91dbf15c1c2e55c86e93de397a24ed42e2317a2 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Fri, 29 May 2026 17:06:13 +0800 Subject: [PATCH 3/3] [storage]: drop ref slice that broke incomplete [R2] Initial fix sliced refs by visibleVolumeUuids, which broke the incomplete computation (a single-deleted group looked fully-deleted when only the matching volume's ref survived the filter). Drop the ref-level slice: cross-account leak is already prevented by the inventories.anyMatch(volumeUuid) check above, and refs of a single VM share the VM owner anyway, so returning full per-VM group structure is the intended semantic. Resolves: ZSV-9792 Change-Id: I5d58d358c1ac8146ef505e120d491f4e347fb108 --- .../snapshot/VolumeSnapshotManagerImpl.java | 9 ++++----- .../group/VolumeSnapshotGroupTreeBuilder.java | 15 --------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java index 2f3e1285576..d0e2fcf96f5 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java @@ -1375,10 +1375,9 @@ private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolume return; } - Set visibleVolumeUuids = reply.getInventories().stream() - .map(VolumeSnapshotTreeInventory::getVolumeUuid) - .collect(Collectors.toSet()); - if (!visibleVolumeUuids.contains(volumeUuid)) { + boolean visible = reply.getInventories().stream() + .anyMatch(inv -> volumeUuid.equals(inv.getVolumeUuid())); + if (!visible) { return; } @@ -1387,7 +1386,7 @@ private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolume return; } - reply.setGroupTrees(groupTreeBuilder.buildForVm(vmInstanceUuid, visibleVolumeUuids)); + reply.setGroupTrees(groupTreeBuilder.buildForVm(vmInstanceUuid)); } @SuppressWarnings("unchecked") diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java index 90223235672..d269d72cc98 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java @@ -53,21 +53,6 @@ public List buildForVm(String vmInstanceUuid, .in(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuids) .list(); - if (visibleVolumeUuids != null) { - refs = refs.stream() - .filter(r -> visibleVolumeUuids.contains(r.getVolumeUuid())) - .collect(Collectors.toList()); - if (refs.isEmpty()) { - return Collections.emptyList(); - } - Set visibleGroupUuids = refs.stream() - .map(VolumeSnapshotGroupRefVO::getVolumeSnapshotGroupUuid) - .collect(Collectors.toSet()); - groupVOs = groupVOs.stream() - .filter(g -> visibleGroupUuids.contains(g.getUuid())) - .collect(Collectors.toList()); - } - Map liveSnapVOs = loadSnapshotVOs(refs, true); Map parentMap = buildParentMap(loadSnapshotVOs(refs, false)); Map snapToGroup = buildSnapToGroupMap(refs);