Fix backup after adding a volume

This commit is contained in:
Abhisar Sinha 2026-05-11 18:28:10 +05:30
parent 77d7d43a4f
commit d3798e1251
2 changed files with 114 additions and 11 deletions

View File

@ -19,12 +19,18 @@ package com.cloud.hypervisor.kvm.resource.wrapper;
import java.io.File;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.Map;
import org.apache.cloudstack.backup.StartBackupAnswer;
import org.apache.cloudstack.backup.StartBackupCommand;
import org.apache.cloudstack.utils.qemu.QemuCommand;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.json.JSONArray;
import org.json.JSONObject;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
@ -150,31 +156,47 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper<StartBackup
return xml.toString();
}
private String createBackupXml(StartBackupCommand cmd, String fromCheckpointId, String socket, LibvirtComputingResource resource) {
private String createBackupXml(StartBackupCommand cmd, String fromCheckpointId, String socket, LibvirtComputingResource resource) throws LibvirtException {
StringBuilder xml = new StringBuilder();
xml.append("<domainbackup mode=\"pull\">\n");
if (StringUtils.isNotBlank(fromCheckpointId)) {
xml.append(" <incremental>").append(fromCheckpointId).append("</incremental>\n");
}
xml.append(String.format(" <server transport=\"unix\" socket=\"/tmp/imagetransfer/%s.sock\"/>\n", socket));
xml.append(" <disks>\n");
Map<String, String> diskPathUuidMap = cmd.getDiskPathUuidMap();
Map<String, String> diskPathLabelMap = resource.getDiskPathLabelMap(cmd.getVmName());
Map<String, Boolean> diskPathHasFromCheckpointMap = new HashMap<>();
if (StringUtils.isNotBlank(fromCheckpointId)) {
Domain vm = null;
try {
vm = resource.getDomain(resource.getLibvirtUtilitiesHelper().getConnection(), cmd.getVmName());
if (vm != null) {
diskPathHasFromCheckpointMap = getVmDiskPathHasFromCheckpointMap(vm, fromCheckpointId);
} else {
logger.warn("Failed to get domain for VM [{}] while evaluating export bitmap [{}]. Falling back to full Backup",
cmd.getVmName(), fromCheckpointId);
}
} finally {
if (vm != null) {
vm.free();
}
}
}
for (Map.Entry<String, String> entry : diskPathLabelMap.entrySet()) {
if (!diskPathUuidMap.containsKey(entry.getKey())) {
String diskPath = entry.getKey();
if (!diskPathUuidMap.containsKey(diskPath)) {
continue;
}
String diskName = entry.getValue();
String export = diskPathUuidMap.get(entry.getKey());
String export = diskPathUuidMap.get(diskPath);
String scratchFile = "/var/tmp/scratch-" + export + ".qcow2";
xml.append(" <disk name=\"").append(diskName).append("\" type=\"file\" exportname=\"").append(export);
if (StringUtils.isNotBlank(fromCheckpointId)) {
xml.append("\" exportbitmap=\"").append(fromCheckpointId);
if (StringUtils.isNotBlank(fromCheckpointId) && Boolean.TRUE.equals(diskPathHasFromCheckpointMap.get(diskPath))) {
xml.append("\" backupmode=\"incremental\"")
.append(" incremental=\"").append(fromCheckpointId)
.append("\" exportbitmap=\"").append(fromCheckpointId);
}
xml.append("\">\n");
xml.append(" <scratch file=\"").append(scratchFile).append("\"/>\n");
@ -194,7 +216,6 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper<StartBackup
}
private Answer handleStoppedVmBackup(StartBackupCommand cmd, String toCheckpointId) {
String vmName = cmd.getVmName();
Map<String, String> diskPathUuidMap = cmd.getDiskPathUuidMap();
for (Map.Entry<String, String> entry : diskPathUuidMap.entrySet()) {
String diskPath = entry.getKey();
@ -218,4 +239,43 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper<StartBackup
private long getCheckpointCreateTime() {
return System.currentTimeMillis() / 1000;
}
private Map<String, Boolean> getVmDiskPathHasFromCheckpointMap(Domain vm, String fromCheckpointId) throws LibvirtException {
Map<String, Boolean> diskPathHasFromCheckpointMap = new HashMap<>();
String queryBlock = vm.qemuMonitorCommand(QemuCommand.buildQemuCommand("query-block", null), 0);
JSONObject response = new JSONObject(queryBlock);
JSONArray blocks = response.optJSONArray("return");
if (blocks == null) {
logger.warn("Couldn't get bitmap information for the VM [{}]. Falling back to full Backup", vm.getName());
return diskPathHasFromCheckpointMap;
}
for (int i = 0; i < blocks.length(); i++) {
JSONObject block = blocks.getJSONObject(i);
JSONObject inserted = block.optJSONObject("inserted");
if (inserted == null) {
continue;
}
String file = inserted.optString("file");
if (StringUtils.isBlank(file)) {
continue;
}
JSONArray dirtyBitmaps = inserted.optJSONArray("dirty-bitmaps");
boolean hasFromCheckpointBitmap = false;
if (dirtyBitmaps != null) {
for (int j = 0; j < dirtyBitmaps.length(); j++) {
JSONObject dirtyBitmap = dirtyBitmaps.optJSONObject(j);
if (dirtyBitmap == null) {
continue;
}
String bitmapName = dirtyBitmap.optString("name");
if (fromCheckpointId.equals(bitmapName)) {
hasFromCheckpointBitmap = true;
break;
}
}
}
diskPathHasFromCheckpointMap.put(file, hasFromCheckpointBitmap);
}
return diskPathHasFromCheckpointMap;
}
}

View File

@ -23,6 +23,8 @@ import org.apache.cloudstack.backup.StartNBDServerAnswer;
import org.apache.cloudstack.backup.StartNBDServerCommand;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.json.JSONArray;
import org.json.JSONObject;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
@ -68,6 +70,11 @@ public class LibvirtStartNBDServerCommandWrapper extends CommandWrapper<StartNBD
}
String socketName = "/tmp/imagetransfer/" + socket + ".sock";
String bitmapArg = "";
if (StringUtils.isNotBlank(cmd.getFromCheckpointId())
&& isBitmapPresentOnDisk(volumePath, cmd.getFromCheckpointId())) {
bitmapArg = "-B " + cmd.getFromCheckpointId();
}
// --persistent: Don't stop the service when the last client disconnects.
// --shared=NUM: Allow up to NUM clients to share the device (default 1), 0 for unlimited. Number of parallel connections is managed by the image server.
String systemdRunCmd = String.format(
@ -75,7 +82,7 @@ public class LibvirtStartNBDServerCommandWrapper extends CommandWrapper<StartNBD
unitName,
exportName,
socketName,
cmd.getFromCheckpointId() != null ? "-B " + cmd.getFromCheckpointId() : "",
bitmapArg,
cmd.getDirection().equals("download") ? "--read-only" : "",
volumePath
);
@ -125,4 +132,40 @@ public class LibvirtStartNBDServerCommandWrapper extends CommandWrapper<StartNBD
return new StartNBDServerAnswer(cmd, true, "qemu-nbd service started for upload",
transferId, transferUrl);
}
private boolean isBitmapPresentOnDisk(String volumePath, String fromCheckpointId) {
String qemuImgInfo = Script.runBashScriptIgnoreExitValue(
String.format("qemu-img info --output=json %s", volumePath), 0);
if (StringUtils.isBlank(qemuImgInfo)) {
logger.warn("Unable to read qemu-img info output for disk path [{}].", volumePath);
return false;
}
try {
JSONObject info = new JSONObject(qemuImgInfo);
JSONObject formatSpecific = info.optJSONObject("format-specific");
if (formatSpecific == null) {
return false;
}
JSONObject formatData = formatSpecific.optJSONObject("data");
if (formatData == null) {
return false;
}
JSONArray bitmaps = formatData.optJSONArray("bitmaps");
if (bitmaps == null) {
return false;
}
for (int i = 0; i < bitmaps.length(); i++) {
JSONObject bitmap = bitmaps.optJSONObject(i);
if (bitmap == null) {
continue;
}
if (fromCheckpointId.equals(bitmap.optString("name"))) {
return true;
}
}
} catch (Exception e) {
logger.warn("Failed to parse qemu-img info output for disk path [{}].", volumePath, e);
}
return false;
}
}