From 6c19c4b39690e57b685e3595974413ac9f286515 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 1 May 2026 15:14:52 +0800 Subject: [PATCH] [compute]: hard delete host file if created state vm destroyed Resolves: ZSV-11845 Related: ZSV-11310 Change-Id: I776f676a797473756a6f6166756c6a786f717869 --- .../VmHostBackupFileCascadeExtension.java | 75 ++++++++++++++++++- .../devices/VmHostFileCascadeExtension.java | 75 ++++++++++++++++++- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java index 0c0821696a7..d22b9725ffc 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java @@ -16,7 +16,10 @@ import org.zstack.header.vm.VmDeletionStruct; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceDeletionPolicyManager.VmInstanceDeletionPolicy; +import org.zstack.header.vm.VmInstanceInventory; +import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.vm.additions.VmHostBackupFileDeletionMsg; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostBackupFileVO_; @@ -24,7 +27,10 @@ import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.zstack.core.Platform.operr; import static org.zstack.utils.CollectionDSL.list; @@ -44,8 +50,9 @@ public class VmHostBackupFileCascadeExtension extends AbstractAsyncCascadeExtens public void asyncCascade(CascadeAction action, Completion completion) { if (action.isActionCode(CascadeConstant.DELETION_CHECK_CODE)) { handleDeletionCheck(action, completion); - } else if (action.isActionCode(CascadeConstant.DELETION_DELETE_CODE, CascadeConstant.DELETION_FORCE_DELETE_CODE) - || action.isActionCode(CascadeConstant.VM_INSTANCE_EXPUNGE_CODE)) { + } else if (action.isActionCode(CascadeConstant.VM_INSTANCE_EXPUNGE_CODE)) { + handleDeletion(action, completion); + } else if (action.isActionCode(CascadeConstant.DELETION_DELETE_CODE, CascadeConstant.DELETION_FORCE_DELETE_CODE)) { if (shouldDeferVmAssociatedDeletion(action)) { completion.success(); return; @@ -65,6 +72,13 @@ private boolean shouldDeferVmAssociatedDeletion(CascadeAction action) { if (!VmInstanceVO.class.getSimpleName().equals(action.getParentIssuer())) { return false; } + if (hasCreatedVmInDeletionContext(action)) { + logger.info(String.format( + "VmHostBackupFileCascadeExtension: skip deferring backup-file deletion for Created VM(s): %s; " + + "destroy uses DBOnly hard-delete without expunge, backup rows must be removed in this cascade", + formatCreatedVmUuidsFromContext(action))); + return false; + } Object raw = action.getParentIssuerContext(); if (!(raw instanceof List)) { return false; @@ -82,6 +96,37 @@ private boolean shouldDeferVmAssociatedDeletion(CascadeAction action) { return false; } + private boolean hasCreatedVmInDeletionContext(CascadeAction action) { + Object raw = action.getParentIssuerContext(); + if (!(raw instanceof List)) { + return false; + } + for (Object o : (List) raw) { + if (!(o instanceof VmDeletionStruct)) { + continue; + } + VmInstanceInventory inv = ((VmDeletionStruct) o).getInventory(); + if (inv != null && VmInstanceState.Created.toString().equals(inv.getState())) { + return true; + } + } + return false; + } + + private String formatCreatedVmUuidsFromContext(CascadeAction action) { + Object raw = action.getParentIssuerContext(); + if (!(raw instanceof List)) { + return "[]"; + } + return ((List) raw).stream() + .filter(VmDeletionStruct.class::isInstance) + .map(VmDeletionStruct.class::cast) + .map(VmDeletionStruct::getInventory) + .filter(inv -> inv != null && VmInstanceState.Created.toString().equals(inv.getState())) + .map(VmInstanceInventory::getUuid) + .collect(Collectors.joining(", ")); + } + private List voFromAction(CascadeAction action) { if (VmInstanceVO.class.getSimpleName().equals(action.getParentIssuer())) { List vmDeletionStructs = action.getParentIssuerContext(); @@ -114,11 +159,22 @@ private void handleDeletion(CascadeAction action, Completion completion) { return; } + Set createdVmUuidsAsResource = findVmUuidsInCreatedState( + voList.stream().map(VmHostBackupFileVO::getResourceUuid).collect(Collectors.toSet())); + if (!createdVmUuidsAsResource.isEmpty()) { + logger.info(String.format( + "VmHostBackupFileCascadeExtension: deleting VmHostBackupFile row(s) tied to Created VM(s) %s with forceDelete=true " + + "(same as expunge path; avoids leftovers when VM row is hard-deleted)", + String.join(", ", createdVmUuidsAsResource))); + } + new While<>(voList).each((vo, whileCompletion) -> { VmHostBackupFileDeletionMsg msg = new VmHostBackupFileDeletionMsg(); msg.setUuid(vo.getUuid()); - msg.setForceDelete(action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE) - || CascadeConstant.VM_INSTANCE_EXPUNGE_CODE.equals(action.getActionCode())); + boolean force = action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE) + || CascadeConstant.VM_INSTANCE_EXPUNGE_CODE.equals(action.getActionCode()) + || createdVmUuidsAsResource.contains(vo.getResourceUuid()); + msg.setForceDelete(force); bus.makeLocalServiceId(msg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); bus.send(msg, new CloudBusCallBack(whileCompletion) { @Override @@ -146,6 +202,17 @@ public void done(ErrorCodeList errorCodeList) { }); } + private Set findVmUuidsInCreatedState(Set candidateVmUuids) { + if (candidateVmUuids.isEmpty()) { + return new HashSet<>(); + } + return new HashSet<>(Q.New(VmInstanceVO.class) + .in(VmInstanceVO_.uuid, candidateVmUuids) + .eq(VmInstanceVO_.state, VmInstanceState.Created) + .select(VmInstanceVO_.uuid) + .listValues()); + } + private void handleDeletionCleanup(CascadeAction action, Completion completion) { completion.success(); } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java index 555f775de35..26983d7d6a2 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java @@ -16,7 +16,10 @@ import org.zstack.header.vm.VmDeletionStruct; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceDeletionPolicyManager.VmInstanceDeletionPolicy; +import org.zstack.header.vm.VmInstanceInventory; +import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.vm.additions.VmHostFileDeletionMsg; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; @@ -24,7 +27,10 @@ import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.zstack.core.Platform.operr; import static org.zstack.utils.CollectionDSL.list; @@ -44,8 +50,9 @@ public class VmHostFileCascadeExtension extends AbstractAsyncCascadeExtension { public void asyncCascade(CascadeAction action, Completion completion) { if (action.isActionCode(CascadeConstant.DELETION_CHECK_CODE)) { handleDeletionCheck(action, completion); - } else if (action.isActionCode(CascadeConstant.DELETION_DELETE_CODE, CascadeConstant.DELETION_FORCE_DELETE_CODE) - || action.isActionCode(CascadeConstant.VM_INSTANCE_EXPUNGE_CODE)) { + } else if (action.isActionCode(CascadeConstant.VM_INSTANCE_EXPUNGE_CODE)) { + handleDeletion(action, completion); + } else if (action.isActionCode(CascadeConstant.DELETION_DELETE_CODE, CascadeConstant.DELETION_FORCE_DELETE_CODE)) { if (shouldDeferVmAssociatedDeletion(action)) { completion.success(); return; @@ -65,6 +72,13 @@ private boolean shouldDeferVmAssociatedDeletion(CascadeAction action) { if (!VmInstanceVO.class.getSimpleName().equals(action.getParentIssuer())) { return false; } + if (hasCreatedVmInDeletionContext(action)) { + logger.info(String.format( + "VmHostFileCascadeExtension: skip deferring host-file deletion for Created VM(s): %s; " + + "destroy uses DBOnly hard-delete without expunge, host files must be removed in this cascade", + formatCreatedVmUuidsFromContext(action))); + return false; + } Object raw = action.getParentIssuerContext(); if (!(raw instanceof List)) { return false; @@ -82,6 +96,37 @@ private boolean shouldDeferVmAssociatedDeletion(CascadeAction action) { return false; } + private boolean hasCreatedVmInDeletionContext(CascadeAction action) { + Object raw = action.getParentIssuerContext(); + if (!(raw instanceof List)) { + return false; + } + for (Object o : (List) raw) { + if (!(o instanceof VmDeletionStruct)) { + continue; + } + VmInstanceInventory inv = ((VmDeletionStruct) o).getInventory(); + if (inv != null && VmInstanceState.Created.toString().equals(inv.getState())) { + return true; + } + } + return false; + } + + private String formatCreatedVmUuidsFromContext(CascadeAction action) { + Object raw = action.getParentIssuerContext(); + if (!(raw instanceof List)) { + return "[]"; + } + return ((List) raw).stream() + .filter(VmDeletionStruct.class::isInstance) + .map(VmDeletionStruct.class::cast) + .map(VmDeletionStruct::getInventory) + .filter(inv -> inv != null && VmInstanceState.Created.toString().equals(inv.getState())) + .map(VmInstanceInventory::getUuid) + .collect(Collectors.joining(", ")); + } + private List voFromAction(CascadeAction action) { if (VmInstanceVO.class.getSimpleName().equals(action.getParentIssuer())) { List vmDeletionStructs = action.getParentIssuerContext(); @@ -111,11 +156,22 @@ private void handleDeletion(CascadeAction action, Completion completion) { return; } + Set createdVmUuids = findVmUuidsInCreatedState( + voList.stream().map(VmHostFileVO::getVmInstanceUuid).collect(Collectors.toSet())); + if (!createdVmUuids.isEmpty()) { + logger.info(String.format( + "VmHostFileCascadeExtension: deleting VmHostFile row(s) for Created VM(s) %s with forceDelete=true " + + "(same as expunge path; avoids leftovers when VM row is hard-deleted)", + String.join(", ", createdVmUuids))); + } + new While<>(voList).each((vo, whileCompletion) -> { VmHostFileDeletionMsg msg = new VmHostFileDeletionMsg(); msg.setUuid(vo.getUuid()); - msg.setForceDelete(action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE) - || CascadeConstant.VM_INSTANCE_EXPUNGE_CODE.equals(action.getActionCode())); + boolean force = action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE) + || CascadeConstant.VM_INSTANCE_EXPUNGE_CODE.equals(action.getActionCode()) + || createdVmUuids.contains(vo.getVmInstanceUuid()); + msg.setForceDelete(force); bus.makeLocalServiceId(msg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); bus.send(msg, new CloudBusCallBack(whileCompletion) { @Override @@ -143,6 +199,17 @@ public void done(ErrorCodeList errorCodeList) { }); } + private Set findVmUuidsInCreatedState(Set vmUuids) { + if (vmUuids.isEmpty()) { + return new HashSet<>(); + } + return new HashSet<>(Q.New(VmInstanceVO.class) + .in(VmInstanceVO_.uuid, vmUuids) + .eq(VmInstanceVO_.state, VmInstanceState.Created) + .select(VmInstanceVO_.uuid) + .listValues()); + } + private void handleDeletionCleanup(CascadeAction action, Completion completion) { completion.success(); }