From c70a9b673733fd4ec0883d2c4155189aca875332 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Wed, 1 Jul 2026 10:54:53 +0800 Subject: [PATCH 1/2] [storage]: keep partial groups APIImpact Resolves: ZSV-9792 Change-Id: I6c6b656d77706671687561696d7669777863646f --- ...reateVolumesSnapshotOverlayInnerReply.java | 19 ++++++++ .../snapshot/CreateVolumesSnapshotReply.java | 19 ++++++++ .../APICreateVolumeSnapshotGroupMsg.java | 3 ++ .../group/VolumeSnapshotGroupBase.java | 6 +++ .../org/zstack/storage/volume/VolumeBase.java | 43 ++++++++++++++----- 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java index cd816f6014c..86a5bc651dd 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java @@ -1,5 +1,6 @@ package org.zstack.header.storage.snapshot; +import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.message.MessageReply; import java.util.List; @@ -10,6 +11,8 @@ public class CreateVolumesSnapshotOverlayInnerReply extends MessageReply { private List inventories; private List hostBackupFileUuidList; + private List failedSnapshotJobs; + private ErrorCode partialError; public List getInventories() { return inventories; @@ -26,4 +29,20 @@ public List getHostBackupFileUuidList() { public void setHostBackupFileUuidList(List hostBackupFileUuidList) { this.hostBackupFileUuidList = hostBackupFileUuidList; } + + public List getFailedSnapshotJobs() { + return failedSnapshotJobs; + } + + public void setFailedSnapshotJobs(List failedSnapshotJobs) { + this.failedSnapshotJobs = failedSnapshotJobs; + } + + public ErrorCode getPartialError() { + return partialError; + } + + public void setPartialError(ErrorCode partialError) { + this.partialError = partialError; + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java index ac510f260b7..a889b83e38f 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java @@ -1,5 +1,6 @@ package org.zstack.header.storage.snapshot; +import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.message.MessageReply; import java.util.List; @@ -10,6 +11,8 @@ public class CreateVolumesSnapshotReply extends MessageReply { private List inventories; private List hostBackupFileUuidList; + private List failedSnapshotJobs; + private ErrorCode partialError; public List getInventories() { return inventories; @@ -26,4 +29,20 @@ public List getHostBackupFileUuidList() { public void setHostBackupFileUuidList(List hostBackupFileUuidList) { this.hostBackupFileUuidList = hostBackupFileUuidList; } + + public List getFailedSnapshotJobs() { + return failedSnapshotJobs; + } + + public void setFailedSnapshotJobs(List failedSnapshotJobs) { + this.failedSnapshotJobs = failedSnapshotJobs; + } + + public ErrorCode getPartialError() { + return partialError; + } + + public void setPartialError(ErrorCode partialError) { + this.partialError = partialError; + } } diff --git a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java index 13434495343..c7d4e87312a 100644 --- a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java @@ -127,6 +127,9 @@ public List multiAudit(APIMessage msg, APIEvent rsp) { List volumeSnapshotRefs = ((APICreateVolumeSnapshotGroupEvent) rsp).getInventory().getVolumeSnapshotRefs(); volumeSnapshotRefs.forEach(it -> { + if (it.isSnapshotDeleted()) { + return; + } res.add(new APIAuditor.Result(it.getVolumeSnapshotUuid(), VolumeSnapshotVO.class)); }); } diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index fcb6907ca9e..e12b6233a62 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -224,6 +224,12 @@ private void handle(DeleteVolumeSnapshotGroupInnerMsg msg) { if (snapshots.size() < self.getSnapshotCount()) { logger.debug(String.format("skip snapshots not belong to origin vm[uuid:%s]", self.getVmInstanceUuid())); } + if (snapshots.isEmpty()) { + vmHostFileManager.cleanVmHostBackupFile(self.getUuid()); + dbf.remove(self); + bus.reply(msg, reply); + return; + } SimpleFlowChain.of("delete-volume-snapshot-group") .then("delete-volume-snapshots", (trigger) -> diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java index 989913e3c1f..811bbebdb1c 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java @@ -3116,6 +3116,7 @@ private void createSnapshotGroup(CreateVolumeSnapshotGroupMessage msg, ReturnVal cmsg.setConsistentType(msg.getConsistentType()); List inventories = new ArrayList<>(); + List failedSnapshotJobs = new ArrayList<>(); List hostBackupFileUuidList = new ArrayList<>(); AtomicReference groupRef = new AtomicReference<>(null); String resourceUuid = msg.getResourceUuid() == null ? getUuid() : msg.getResourceUuid(); @@ -3131,7 +3132,16 @@ public void run(MessageReply reply) { return; } CreateVolumesSnapshotReply r = reply.castReply(); - inventories.addAll(r.getInventories()); + if (!CollectionUtils.isEmpty(r.getInventories())) { + inventories.addAll(r.getInventories()); + } + if (!CollectionUtils.isEmpty(r.getFailedSnapshotJobs())) { + failedSnapshotJobs.addAll(r.getFailedSnapshotJobs()); + } + if (r.getPartialError() != null) { + logger.warn(String.format("created volume snapshot group[uuid:%s] with %s failed snapshot jobs, partial error: %s", + resourceUuid, failedSnapshotJobs.size(), r.getPartialError().getReadableDetails())); + } if (!CollectionUtils.isEmpty(r.getHostBackupFileUuidList())) { hostBackupFileUuidList.addAll(r.getHostBackupFileUuidList()); } @@ -3142,6 +3152,8 @@ public void run(MessageReply reply) { .then(Flow.of("persist-snapshot-group") .handle(trigger -> { List refs = new ArrayList<>(); + Map invByVolumeUuid = inventories.stream() + .collect(Collectors.toMap(VolumeSnapshotInventory::getVolumeUuid, inv -> inv, (oldValue, newValue) -> oldValue)); VolumeSnapshotGroupVO group = new VolumeSnapshotGroupVO(); group.setUuid(resourceUuid); group.setSnapshotCount(cmsg.getVolumeSnapshotJobs().size()); @@ -3149,17 +3161,28 @@ public void run(MessageReply reply) { group.setDescription(msg.getDescription()); group.setVmInstanceUuid(vm.getUuid()); group.setAccountUuid(msg.getSession().getAccountUuid()); - for (VolumeSnapshotInventory inv : inventories) { + for (CreateVolumesSnapshotsJobStruct job : cmsg.getVolumeSnapshotJobs()) { + VolumeInventory vol = vols.get(job.getVolumeUuid()); + VolumeSnapshotInventory inv = invByVolumeUuid.get(job.getVolumeUuid()); VolumeSnapshotGroupRefVO ref = new VolumeSnapshotGroupRefVO(); - ref.setVolumeUuid(inv.getVolumeUuid()); - ref.setVolumeName(vols.get(inv.getVolumeUuid()).getName()); - ref.setVolumeType(inv.getVolumeType()); + ref.setVolumeUuid(job.getVolumeUuid()); + ref.setVolumeName(vol.getName()); + ref.setVolumeType(inv == null ? vol.getType() : inv.getVolumeType()); ref.setVolumeSnapshotGroupUuid(group.getUuid()); - ref.setVolumeSnapshotUuid(inv.getUuid()); - ref.setVolumeSnapshotName(inv.getName()); - ref.setVolumeSnapshotInstallPath(inv.getPrimaryStorageInstallPath()); - ref.setDeviceId(vols.get(inv.getVolumeUuid()).getDeviceId()); - ref.setVolumeLastAttachDate(vols.get(inv.getVolumeUuid()).getLastAttachDate()); + if (inv == null) { + ref.setVolumeSnapshotUuid(job.getVolumeSnapshotStruct() == null ? + job.getResourceUuid() : job.getVolumeSnapshotStruct().getCurrent().getUuid()); + ref.setVolumeSnapshotName(job.getName()); + ref.setVolumeSnapshotInstallPath(job.getVolumeSnapshotStruct() == null ? + null : job.getVolumeSnapshotStruct().getCurrent().getPrimaryStorageInstallPath()); + ref.setSnapshotDeleted(true); + } else { + ref.setVolumeSnapshotUuid(inv.getUuid()); + ref.setVolumeSnapshotName(inv.getName()); + ref.setVolumeSnapshotInstallPath(inv.getPrimaryStorageInstallPath()); + } + ref.setDeviceId(vol.getDeviceId()); + ref.setVolumeLastAttachDate(vol.getLastAttachDate()); refs.add(ref); } From a77e9c469e3c8ecbc4db8d1d0a7a371391b8528b Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Tue, 7 Jul 2026 12:02:05 +0800 Subject: [PATCH 2/2] [storage]: get volume snapshot group tree Root cause: the group tree response was modeled as a query inventory, which can conflict with the normal volume snapshot group query mapping and return the wrong inventory type. Solution: expose the group tree as a dedicated API get message, build the tree in the snapshot service, and keep tree inventories outside the generic query inventory mapping. Impact: this changes the group tree API message type from query to get while preserving the existing REST path and returning only the tree inventory payload. APIImpact Resolves: ZSV-9792 Change-Id: I696867747a666d6d6967686f66696278776b6f63 --- conf/serviceConfig/volumeSnapshot.xml | 4 + .../APIGetVolumeSnapshotGroupTreeMsg.java | 43 +++ ...VolumeSnapshotGroupTreeMsgDoc_zh_cn.groovy | 30 +++ .../APIGetVolumeSnapshotGroupTreeReply.java | 35 +++ ...lumeSnapshotGroupTreeReplyDoc_zh_cn.groovy | 30 +++ .../VolumeSnapshotGroupTreeInventory.java | 131 ++++++++++ ...SnapshotGroupTreeInventoryDoc_zh_cn.groovy | 79 ++++++ .../VolumeSnapshotGroupTreeRefInventory.java | 60 +++++ ...pshotGroupTreeRefInventoryDoc_zh_cn.groovy | 47 ++++ .../snapshot/VolumeSnapshotManagerImpl.java | 29 +++ .../group/VolumeSnapshotGroupTreeBuilder.java | 246 ++++++++++++++++++ 11 files changed, 734 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReplyDoc_zh_cn.groovy 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/conf/serviceConfig/volumeSnapshot.xml b/conf/serviceConfig/volumeSnapshot.xml index 57e2e6c9b7b..d352268b496 100755 --- a/conf/serviceConfig/volumeSnapshot.xml +++ b/conf/serviceConfig/volumeSnapshot.xml @@ -50,6 +50,10 @@ query + + org.zstack.header.storage.snapshot.group.APIGetVolumeSnapshotGroupTreeMsg + + org.zstack.header.vm.devices.APIQueryVmInstanceResourceMetadataArchiveMsg query diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsg.java new file mode 100644 index 00000000000..f8b6f0c767a --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsg.java @@ -0,0 +1,43 @@ +package org.zstack.header.storage.snapshot.group; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.VmInstanceVO; + +@RestRequest( + path = "/volume-snapshots/group/trees", + optionalPaths = {"/volume-snapshots/group/trees/{uuid}"}, + method = HttpMethod.GET, + responseClass = APIGetVolumeSnapshotGroupTreeReply.class +) +public class APIGetVolumeSnapshotGroupTreeMsg extends APISyncCallMessage { + @APIParam(required = false, resourceType = VmInstanceVO.class) + private String uuid; + + @APIParam(required = false, resourceType = VmInstanceVO.class) + private String vmInstanceUuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public static APIGetVolumeSnapshotGroupTreeMsg __example__() { + APIGetVolumeSnapshotGroupTreeMsg msg = new APIGetVolumeSnapshotGroupTreeMsg(); + msg.setVmInstanceUuid(uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..a4ec2a66ebc --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeMsgDoc_zh_cn.groovy @@ -0,0 +1,30 @@ +package org.zstack.header.storage.snapshot.group + +import org.zstack.header.storage.snapshot.group.APIGetVolumeSnapshotGroupTreeReply + +doc { + title "GetVolumeSnapshotGroupTree" + + category "snapshot.volume" + + desc """查询指定云主机的快照组树""" + + rest { + request { + url "GET /v1/volume-snapshots/group/trees" + url "GET /v1/volume-snapshots/group/trees/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIGetVolumeSnapshotGroupTreeMsg.class + + desc """""" + + params APIGetVolumeSnapshotGroupTreeMsg.class + } + + response { + clz APIGetVolumeSnapshotGroupTreeReply.class + } + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReply.java new file mode 100644 index 00000000000..92565039b42 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReply.java @@ -0,0 +1,35 @@ +package org.zstack.header.storage.snapshot.group; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.sql.Timestamp; +import java.util.Collections; +import java.util.List; + +@RestResponse(allTo = "inventories") +public class APIGetVolumeSnapshotGroupTreeReply extends APIReply { + private List inventories; + + public List getInventories() { + return inventories; + } + + public void setInventories(List inventories) { + this.inventories = inventories; + } + + public static APIGetVolumeSnapshotGroupTreeReply __example__() { + VolumeSnapshotGroupTreeInventory inv = new VolumeSnapshotGroupTreeInventory(); + inv.setUuid(uuid()); + inv.setName("group"); + inv.setVmInstanceUuid(uuid()); + inv.setCurrent(true); + inv.setCreateDate(new Timestamp(org.zstack.header.message.DocUtils.date)); + inv.setLastOpDate(new Timestamp(org.zstack.header.message.DocUtils.date)); + + APIGetVolumeSnapshotGroupTreeReply reply = new APIGetVolumeSnapshotGroupTreeReply(); + reply.setInventories(Collections.singletonList(inv)); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReplyDoc_zh_cn.groovy new file mode 100644 index 00000000000..03909eeccbe --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIGetVolumeSnapshotGroupTreeReplyDoc_zh_cn.groovy @@ -0,0 +1,30 @@ +package org.zstack.header.storage.snapshot.group + +import org.zstack.header.errorcode.ErrorCode + +doc { + title "快照组树清单" + + field { + name "success" + desc "" + type "boolean" + since "0.6" + } + ref { + name "error" + path "org.zstack.header.storage.snapshot.group.APIGetVolumeSnapshotGroupTreeReply.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "0.6" + clz ErrorCode.class + } + ref { + name "inventories" + path "org.zstack.header.storage.snapshot.group.APIGetVolumeSnapshotGroupTreeReply.inventories" + desc "指定云主机的快照组树清单" + 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..f80a11a5553 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java @@ -0,0 +1,131 @@ +package org.zstack.header.storage.snapshot.group; + +import org.zstack.header.query.Unqueryable; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class VolumeSnapshotGroupTreeInventory { + private String uuid; + private String name; + private String description; + private String vmInstanceUuid; + private Timestamp createDate; + private Timestamp lastOpDate; + @Unqueryable + private boolean current; + @Unqueryable + private boolean incomplete; + @Unqueryable + private String parentGroupUuid; + @Unqueryable + private List children = new ArrayList<>(); + @Unqueryable + private List refs = new ArrayList<>(); + + public static VolumeSnapshotGroupTreeInventory valueOf(VolumeSnapshotGroupVO vo) { + VolumeSnapshotGroupTreeInventory inv = new VolumeSnapshotGroupTreeInventory(); + inv.setUuid(vo.getUuid()); + inv.setName(vo.getName()); + inv.setDescription(vo.getDescription()); + inv.setVmInstanceUuid(vo.getVmInstanceUuid()); + inv.setCreateDate(vo.getCreateDate()); + inv.setLastOpDate(vo.getLastOpDate()); + return inv; + } + + public static List valueOf(Collection vos) { + return vos.stream().map(VolumeSnapshotGroupTreeInventory::valueOf).collect(Collectors.toList()); + } + + 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..57155d9d890 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java @@ -41,6 +41,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; @@ -484,6 +485,23 @@ private void handle(APICheckVolumeSnapshotGroupAvailabilityMsg msg) { bus.reply(msg, reply); } + private void handle(APIGetVolumeSnapshotGroupTreeMsg msg) { + APIGetVolumeSnapshotGroupTreeReply reply = new APIGetVolumeSnapshotGroupTreeReply(); + List inventories = buildVolumeSnapshotGroupTrees(msg); + reply.setInventories(inventories); + + bus.reply(msg, reply); + } + + private List buildVolumeSnapshotGroupTrees(APIGetVolumeSnapshotGroupTreeMsg msg) { + String vmInstanceUuid = extractVmInstanceUuid(msg); + if (vmInstanceUuid == null) { + return Collections.emptyList(); + } + + return groupTreeBuilder.buildForVm(vmInstanceUuid); + } + /* private void handle(APIGetVolumeSnapshotTreeMsg msg) { APIGetVolumeSnapshotTreeReply reply = new APIGetVolumeSnapshotTreeReply(); @@ -1196,6 +1214,8 @@ private void handleApiMessage(APIMessage msg) { handle((APIBatchDeleteVolumeSnapshotMsg) msg); } else if (msg instanceof APICheckVolumeSnapshotGroupAvailabilityMsg) { handle((APICheckVolumeSnapshotGroupAvailabilityMsg) msg); + } else if (msg instanceof APIGetVolumeSnapshotGroupTreeMsg) { + handle((APIGetVolumeSnapshotGroupTreeMsg) msg); } else if (msg instanceof APIGetMemorySnapshotGroupReferenceMsg) { handle((APIGetMemorySnapshotGroupReferenceMsg) msg); } else if (msg instanceof APICheckMemorySnapshotGroupConflictMsg) { @@ -1365,6 +1385,15 @@ private Set querySnapshotUuids(String treeUuid, SessionInventory session : Collections.emptySet(); } + private final VolumeSnapshotGroupTreeBuilder groupTreeBuilder = new VolumeSnapshotGroupTreeBuilder(); + + private String extractVmInstanceUuid(APIGetVolumeSnapshotGroupTreeMsg msg) { + if (msg.getVmInstanceUuid() != null) { + return msg.getVmInstanceUuid(); + } + return msg.getUuid(); + } + @Override public void volumePreExpunge(VolumeInventory volume) {} 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..cf921599e77 --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java @@ -0,0 +1,246 @@ +package org.zstack.storage.snapshot.group; + +import org.zstack.core.db.Q; +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.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 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, liveSnapVOs); + + 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); + } + + private Map loadSnapshotVOs(List refs, boolean liveOnly) { + List snapUuids = refs.stream() + .filter(r -> !liveOnly || !r.isSnapshotDeleted()) + .map(VolumeSnapshotGroupRefVO::getVolumeSnapshotUuid) + .collect(Collectors.toList()); + if (snapUuids.isEmpty()) { + return Collections.emptyMap(); + } + List svos = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.uuid, snapUuids) + .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); + } + } +}