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/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/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/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/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/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/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);
+ }
+ }
+}
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);
}