Implement backend for delete vm checkpoint

This commit is contained in:
Abhisar Sinha 2026-04-07 05:16:00 +05:30 committed by Abhishek Kumar
parent b84ff6b99a
commit dc480e07d3
4 changed files with 210 additions and 3 deletions

View File

@ -0,0 +1,60 @@
//Licensed to the Apache Software Foundation (ASF) under one
//or more contributor license agreements. See the NOTICE file
//distributed with this work for additional information
//regarding copyright ownership. The ASF licenses this file
//to you under the Apache License, Version 2.0 (the
//"License"); you may not use this file except in compliance
//the License. You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing,
//software distributed under the License is distributed on an
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
//KIND, either express or implied. See the License for the
//specific language governing permissions and limitations
//under the License.
package org.apache.cloudstack.backup;
import java.util.Map;
import com.cloud.agent.api.Command;
public class DeleteVmCheckpointCommand extends Command {
private String vmName;
private String checkpointId;
private Map<String, String> diskPathUuidMap;
private boolean stoppedVM;
public DeleteVmCheckpointCommand() {
}
public DeleteVmCheckpointCommand(String vmName, String checkpointId, Map<String, String> diskPathUuidMap, boolean stoppedVM) {
this.vmName = vmName;
this.checkpointId = checkpointId;
this.diskPathUuidMap = diskPathUuidMap;
this.stoppedVM = stoppedVM;
}
public String getVmName() {
return vmName;
}
public String getCheckpointId() {
return checkpointId;
}
public Map<String, String> getDiskPathUuidMap() {
return diskPathUuidMap;
}
public boolean isStoppedVM() {
return stoppedVM;
}
@Override
public boolean executeInSequence() {
return true;
}
}

View File

@ -0,0 +1,80 @@
//Licensed to the Apache Software Foundation (ASF) under one
//or more contributor license agreements. See the NOTICE file
//distributed with this work for additional information
//regarding copyright ownership. The ASF licenses this file
//to you under the Apache License, Version 2.0 (the
//"License"); you may not use this file except in compliance
//the License. You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing,
//software distributed under the License is distributed on an
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
//KIND, either express or implied. See the License for the
//specific language governing permissions and limitations
//under the License.
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.util.Map;
import org.apache.cloudstack.backup.DeleteVmCheckpointCommand;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = DeleteVmCheckpointCommand.class)
public class LibvirtDeleteVmCheckpointCommandWrapper extends CommandWrapper<DeleteVmCheckpointCommand, Answer, LibvirtComputingResource> {
@Override
public Answer execute(DeleteVmCheckpointCommand cmd, LibvirtComputingResource resource) {
if (cmd.isStoppedVM()) {
return deleteBitmapsOnDisks(cmd);
}
return deleteDomainCheckpoint(cmd);
}
private Answer deleteDomainCheckpoint(DeleteVmCheckpointCommand cmd) {
String vmName = cmd.getVmName();
String checkpointId = cmd.getCheckpointId();
String virshCmd = String.format("virsh checkpoint-delete %s %s", vmName, checkpointId);
Script script = new Script("/bin/bash");
script.add("-c");
script.add(virshCmd);
String result = script.execute();
if (result != null) {
return new Answer(cmd, false, "Failed to delete checkpoint: " + result);
}
return new Answer(cmd, true, "Checkpoint deleted");
}
/**
* Stopped VM: persistent bitmaps on disk images ({@code qemu-img bitmap --remove}), matching {@link LibvirtStartBackupCommandWrapper} bitmap --add.
*/
private Answer deleteBitmapsOnDisks(DeleteVmCheckpointCommand cmd) {
String checkpointId = cmd.getCheckpointId();
Map<String, String> diskPathUuidMap = cmd.getDiskPathUuidMap();
if (diskPathUuidMap == null || diskPathUuidMap.isEmpty()) {
return new Answer(cmd, false, "No disks provided for bitmap removal");
}
for (Map.Entry<String, String> entry : diskPathUuidMap.entrySet()) {
String diskPath = entry.getKey();
Script script = new Script("sudo");
script.add("qemu-img");
script.add("bitmap");
script.add("--remove");
script.add(diskPath);
script.add(checkpointId);
String result = script.execute();
if (result != null) {
return new Answer(cmd, false,
"Failed to remove bitmap " + checkpointId + " from disk " + diskPath + ": " + result);
}
}
return new Answer(cmd, true, "Checkpoint bitmap removed from disks");
}
}

View File

@ -1712,6 +1712,7 @@ public class ServerAdapter extends ManagerBase {
DeleteVmCheckpointCmd cmd = new DeleteVmCheckpointCmd();
ComponentContext.inject(cmd);
cmd.setVmId(vo.getId());
cmd.setCheckpointId(checkpointId);
kvmBackupExportService.deleteVmCheckpoint(cmd);
} catch (Exception e) {
throw new CloudRuntimeException("Failed to delete checkpoint: " + e.getMessage(), e);

View File

@ -76,6 +76,7 @@ import com.cloud.user.User;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceDetailVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.VmDetailConstants;
@ -203,6 +204,15 @@ public class KVMBackupExportServiceImpl extends ManagerBase implements KVMBackup
}
long hostId = backup.getHostId();
VMInstanceDetailVO lastCheckpointId = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.LAST_CHECKPOINT_ID);
if (lastCheckpointId != null) {
try {
sendDeleteCheckpointCommand(vm, lastCheckpointId.getValue());
} catch (CloudRuntimeException e) {
logger.warn("Failed to delete last checkpoint {} for VM {}, proceeding with backup start", lastCheckpointId.getValue(), vmId, e);
}
}
Host host = hostDao.findById(hostId);
Map<String, String> vmDetails = vmInstanceDetailsDao.listDetailsKeyPairs(vmId);
String activeCkpCreateTimeStr = vmDetails.get(VmDetailConstants.ACTIVE_CHECKPOINT_CREATE_TIME);
@ -724,9 +734,39 @@ public class KVMBackupExportServiceImpl extends ManagerBase implements KVMBackup
return responses;
}
private void sendDeleteCheckpointCommand(VMInstanceVO vm, String checkpointId) {
Long hostId = vm.getHostId() != null ? vm.getHostId() : vm.getLastHostId();
Map<String, String> diskPathUuidMap = new HashMap<>();
if (vm.getState() == State.Stopped) {
List<VolumeVO> volumes = volumeDao.findByInstance(vm.getId());
for (Volume vol : volumes) {
diskPathUuidMap.put(getVolumePathForFileBasedBackend(vol), vol.getUuid());
}
}
DeleteVmCheckpointCommand deleteCmd = new DeleteVmCheckpointCommand(
vm.getInstanceName(),
checkpointId,
diskPathUuidMap,
vm.getState() == State.Stopped);
Answer answer;
try {
answer = agentManager.send(hostId, deleteCmd);
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.error("Failed to communicate with agent to delete checkpoint for VM {}", vm.getId(), e);
throw new CloudRuntimeException("Failed to communicate with agent", e);
}
if (answer == null || !answer.getResult()) {
String err = answer != null ? answer.getDetails() : "null answer";
throw new CloudRuntimeException("Failed to delete checkpoint: " + err);
}
}
@Override
public boolean deleteVmCheckpoint(DeleteVmCheckpointCmd cmd) {
// Todo : backend support?
VMInstanceVO vm = vmInstanceDao.findById(cmd.getVmId());
if (vm == null) {
throw new CloudRuntimeException("VM not found: " + cmd.getVmId());
@ -736,12 +776,38 @@ public class KVMBackupExportServiceImpl extends ManagerBase implements KVMBackup
" backup provider. Either set backup.framework.enabled to false or set the Zone level config backup.framework.provider.plugin to \"dummy\".");
}
if (vm.getState() != State.Running && vm.getState() != State.Stopped) {
throw new CloudRuntimeException("VM must be running or stopped to delete checkpoint");
}
long vmId = cmd.getVmId();
vmInstanceDetailsDao.removeDetail(vmId, VmDetailConstants.ACTIVE_CHECKPOINT_ID);
vmInstanceDetailsDao.removeDetail(vmId, VmDetailConstants.ACTIVE_CHECKPOINT_CREATE_TIME);
Map<String, String> details = vmInstanceDetailsDao.listDetailsKeyPairs(vmId);
String activeCheckpointId = details.get(VmDetailConstants.ACTIVE_CHECKPOINT_ID);
if (activeCheckpointId == null || !activeCheckpointId.equals(cmd.getCheckpointId())) {
logger.error("Checkpoint ID {} to delete does not match active checkpoint ID for VM {}", cmd.getCheckpointId(), vmId);
return true;
}
sendDeleteCheckpointCommand(vm, activeCheckpointId);
revertVmCheckpointDetailsAfterActiveDelete(vmId, details);
return true;
}
private void revertVmCheckpointDetailsAfterActiveDelete(long vmId, Map<String, String> detailsBeforeDelete) {
String lastId = detailsBeforeDelete.get(VmDetailConstants.LAST_CHECKPOINT_ID);
String lastTime = detailsBeforeDelete.get(VmDetailConstants.LAST_CHECKPOINT_CREATE_TIME);
if (lastId != null) {
vmInstanceDetailsDao.addDetail(vmId, VmDetailConstants.ACTIVE_CHECKPOINT_ID, lastId, false);
vmInstanceDetailsDao.addDetail(vmId, VmDetailConstants.ACTIVE_CHECKPOINT_CREATE_TIME, lastTime, false);
vmInstanceDetailsDao.removeDetail(vmId, VmDetailConstants.LAST_CHECKPOINT_ID);
vmInstanceDetailsDao.removeDetail(vmId, VmDetailConstants.LAST_CHECKPOINT_CREATE_TIME);
} else {
vmInstanceDetailsDao.removeDetail(vmId, VmDetailConstants.ACTIVE_CHECKPOINT_ID);
vmInstanceDetailsDao.removeDetail(vmId, VmDetailConstants.ACTIVE_CHECKPOINT_CREATE_TIME);
}
}
@Override
public List<Class<?>> getCommands() {
List<Class<?>> cmdList = new ArrayList<>();