Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import com.cloud.agent.api.LogLevel;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TakeBackupCommand extends Command {
private String vmName;
Expand All @@ -35,6 +37,7 @@ public class TakeBackupCommand extends Command {
private Boolean quiesce;
@LogLevel(LogLevel.Log4jLevel.Off)
private String mountOptions;
private Map<String, String> details = new HashMap<>();

public TakeBackupCommand(String vmName, String backupPath) {
super();
Expand Down Expand Up @@ -106,6 +109,18 @@ public void setQuiesce(Boolean quiesce) {
this.quiesce = quiesce;
}

public Map<String, String> getDetails() {
return details;
}

public void setDetails(Map<String, String> details) {
this.details = details;
}

public void addDetail(String key, String value) {
this.details.put(key, value);
}

@Override
public boolean executeInSequence() {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,46 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
true,
BackupFrameworkEnabled.key());

ConfigKey<Boolean> NASBackupCompressionEnabled = new ConfigKey<>("Advanced", Boolean.class,
"nas.backup.compression.enabled",
"false",
"Enable qcow2 compression for NAS backup files.",
true,
ConfigKey.Scope.Zone,
BackupFrameworkEnabled.key());

ConfigKey<Boolean> NASBackupEncryptionEnabled = new ConfigKey<>("Advanced", Boolean.class,
"nas.backup.encryption.enabled",
"false",
"Enable LUKS encryption for NAS backup files.",
true,
ConfigKey.Scope.Zone,
BackupFrameworkEnabled.key());

ConfigKey<String> NASBackupEncryptionPassphrase = new ConfigKey<>("Secure", String.class,
"nas.backup.encryption.passphrase",
"",
"Passphrase for LUKS encryption of NAS backup files. Required when encryption is enabled.",
true,
ConfigKey.Scope.Zone,
BackupFrameworkEnabled.key());

ConfigKey<Integer> NASBackupBandwidthLimitMbps = new ConfigKey<>("Advanced", Integer.class,
"nas.backup.bandwidth.limit.mbps",
"0",
"Bandwidth limit in MiB/s for backup operations (0 = unlimited).",
true,
ConfigKey.Scope.Zone,
BackupFrameworkEnabled.key());

ConfigKey<Boolean> NASBackupIntegrityCheckEnabled = new ConfigKey<>("Advanced", Boolean.class,
"nas.backup.integrity.check",
"false",
"Run qemu-img check on backup files after creation to verify integrity.",
true,
ConfigKey.Scope.Zone,
BackupFrameworkEnabled.key());

@Inject
private BackupDao backupDao;

Expand Down Expand Up @@ -205,6 +245,26 @@ public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesce
command.setMountOptions(backupRepository.getMountOptions());
command.setQuiesce(quiesceVM);

// Pass optional backup enhancement settings from zone-scoped configs
Long zoneId = vm.getDataCenterId();
if (Boolean.TRUE.equals(NASBackupCompressionEnabled.valueIn(zoneId))) {
command.addDetail("compression", "true");
}
if (Boolean.TRUE.equals(NASBackupEncryptionEnabled.valueIn(zoneId))) {
command.addDetail("encryption", "true");
String passphrase = NASBackupEncryptionPassphrase.valueIn(zoneId);
if (passphrase != null && !passphrase.isEmpty()) {
command.addDetail("encryption_passphrase", passphrase);
}
}
Integer bandwidthLimit = NASBackupBandwidthLimitMbps.valueIn(zoneId);
if (bandwidthLimit != null && bandwidthLimit > 0) {
command.addDetail("bandwidth_limit", String.valueOf(bandwidthLimit));
}
if (Boolean.TRUE.equals(NASBackupIntegrityCheckEnabled.valueIn(zoneId))) {
command.addDetail("integrity_check", "true");
}

if (VirtualMachine.State.Stopped.equals(vm.getState())) {
List<VolumeVO> vmVolumes = volumeDao.findByInstance(vm.getId());
vmVolumes.sort(Comparator.comparing(Volume::getDeviceId));
Expand Down Expand Up @@ -594,7 +654,12 @@ public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[]{
NASBackupRestoreMountTimeout
NASBackupRestoreMountTimeout,
NASBackupCompressionEnabled,
NASBackupEncryptionEnabled,
NASBackupEncryptionPassphrase,
NASBackupBandwidthLimitMbps,
NASBackupIntegrityCheckEnabled
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
import org.apache.cloudstack.backup.TakeBackupCommand;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@ResourceWrapper(handles = TakeBackupCommand.class)
Expand Down Expand Up @@ -68,21 +72,59 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir
}
}

List<String> cmdArgs = new ArrayList<>();
cmdArgs.add(libvirtComputingResource.getNasBackupPath());
cmdArgs.add("-o"); cmdArgs.add("backup");
cmdArgs.add("-v"); cmdArgs.add(vmName);
cmdArgs.add("-t"); cmdArgs.add(backupRepoType);
cmdArgs.add("-s"); cmdArgs.add(backupRepoAddress);
cmdArgs.add("-m"); cmdArgs.add(Objects.nonNull(mountOptions) ? mountOptions : "");
cmdArgs.add("-p"); cmdArgs.add(backupPath);
cmdArgs.add("-q"); cmdArgs.add(command.getQuiesce() != null && command.getQuiesce() ? "true" : "false");
cmdArgs.add("-d"); cmdArgs.add(diskPaths.isEmpty() ? "" : String.join(",", diskPaths));

// Append optional enhancement flags from management server config
File passphraseFile = null;
Map<String, String> details = command.getDetails();
if (details != null) {
if ("true".equals(details.get("compression"))) {
cmdArgs.add("-c");
}
if ("true".equals(details.get("encryption"))) {
String passphrase = details.get("encryption_passphrase");
if (passphrase != null && !passphrase.isEmpty()) {
try {
passphraseFile = File.createTempFile("cs-backup-enc-", ".key");
passphraseFile.deleteOnExit();
try (FileWriter fw = new FileWriter(passphraseFile)) {
fw.write(passphrase);
}
cmdArgs.add("-e"); cmdArgs.add(passphraseFile.getAbsolutePath());
} catch (IOException e) {
logger.error("Failed to create encryption passphrase file", e);
return new BackupAnswer(command, false, "Failed to create encryption passphrase file: " + e.getMessage());
}
}
}
String bwLimit = details.get("bandwidth_limit");
if (bwLimit != null && !"0".equals(bwLimit)) {
cmdArgs.add("-b"); cmdArgs.add(bwLimit);
}
if ("true".equals(details.get("integrity_check"))) {
cmdArgs.add("--verify");
}
}

List<String[]> commands = new ArrayList<>();
commands.add(new String[]{
libvirtComputingResource.getNasBackupPath(),
"-o", "backup",
"-v", vmName,
"-t", backupRepoType,
"-s", backupRepoAddress,
"-m", Objects.nonNull(mountOptions) ? mountOptions : "",
"-p", backupPath,
"-q", command.getQuiesce() != null && command.getQuiesce() ? "true" : "false",
"-d", diskPaths.isEmpty() ? "" : String.join(",", diskPaths)
});
commands.add(cmdArgs.toArray(new String[0]));

Pair<Integer, String> result = Script.executePipedCommands(commands, libvirtComputingResource.getCmdsTimeout());

// Clean up passphrase file after backup completes
if (passphraseFile != null && passphraseFile.exists()) {
passphraseFile.delete();
}

if (result.first() != 0) {
logger.debug("Failed to take VM backup: " + result.second());
BackupAnswer answer = new BackupAnswer(command, false, result.second().trim());
Expand Down
113 changes: 111 additions & 2 deletions scripts/vm/hypervisor/kvm/nasbackup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ MOUNT_OPTS=""
BACKUP_DIR=""
DISK_PATHS=""
QUIESCE=""
COMPRESS=""
BANDWIDTH=""
ENCRYPT_PASSFILE=""
VERIFY=""
logFile="/var/log/cloudstack/agent/agent.log"

EXIT_CLEANUP_FAILED=20
Expand Down Expand Up @@ -87,6 +91,52 @@ sanity_checks() {
log -ne "Environment Sanity Checks successfully passed"
}

encrypt_backup() {
local backup_dir="$1"
if [[ -z "$ENCRYPT_PASSFILE" ]]; then
return
fi
if [[ ! -f "$ENCRYPT_PASSFILE" ]]; then
echo "Encryption passphrase file not found: $ENCRYPT_PASSFILE"
exit 1
fi
log -ne "Encrypting backup files with LUKS"
for img in "$backup_dir"/*.qcow2; do
[[ -f "$img" ]] || continue
local tmp_img="${img}.luks"
if qemu-img convert -O qcow2 \
--object "secret,id=sec0,file=$ENCRYPT_PASSFILE" \
-o "encrypt.format=luks,encrypt.key-secret=sec0" \
"$img" "$tmp_img" 2>&1 | tee -a "$logFile"; then
mv "$tmp_img" "$img"
log -ne "Encrypted: $img"
else
echo "Encryption failed for $img"
rm -f "$tmp_img"
exit 1
fi
done
}

verify_backup() {
local backup_dir="$1"
local failed=0
for img in "$backup_dir"/*.qcow2; do
[[ -f "$img" ]] || continue
if ! qemu-img check "$img" > /dev/null 2>&1; then
echo "Backup verification failed for $img"
log -ne "Backup verification FAILED: $img"
failed=1
else
log -ne "Backup verification passed: $img"
fi
done
if [[ $failed -ne 0 ]]; then
echo "One or more backup files failed verification"
exit 1
fi
}

### Operation methods ###

backup_running_vm() {
Expand Down Expand Up @@ -128,6 +178,14 @@ backup_running_vm() {
exit 1
fi

# Throttle backup bandwidth if requested (MiB/s per disk)
if [[ -n "$BANDWIDTH" ]]; then
for disk in $(virsh -c qemu:///system domblklist $VM --details 2>/dev/null | awk '/disk/{print$3}'); do
virsh -c qemu:///system blockjob $VM $disk --bandwidth "${BANDWIDTH}" 2>/dev/null || true
done
log -ne "Backup bandwidth limited to ${BANDWIDTH} MiB/s per disk for $VM"
fi

# Backup domain information
virsh -c qemu:///system dumpxml $VM > $dest/domain-config.xml 2>/dev/null
virsh -c qemu:///system dominfo $VM > $dest/dominfo.xml 2>/dev/null
Expand All @@ -147,8 +205,32 @@ backup_running_vm() {
done

rm -f $dest/backup.xml

# Compress backup files if requested
if [[ "$COMPRESS" == "true" ]]; then
log -ne "Compressing backup files for $VM"
for img in "$dest"/*.qcow2; do
[[ -f "$img" ]] || continue
local tmp_img="${img}.tmp"
if qemu-img convert -c -O qcow2 "$img" "$tmp_img" 2>&1 | tee -a "$logFile"; then
mv "$tmp_img" "$img"
else
log -ne "Warning: compression failed for $img, keeping uncompressed"
rm -f "$tmp_img"
fi
done
fi

# Encrypt backup files if requested
encrypt_backup "$dest"

sync

# Verify backup integrity if requested
if [[ "$VERIFY" == "true" ]]; then
verify_backup "$dest"
fi

# Print statistics
virsh -c qemu:///system domjobinfo $VM --completed
du -sb $dest | cut -f1
Expand All @@ -174,14 +256,23 @@ backup_stopped_vm() {
volUuid="${disk##*/}"
fi
output="$dest/$name.$volUuid.qcow2"
if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then
if ! ionice -c 3 qemu-img convert $([[ "$COMPRESS" == "true" ]] && echo "-c") $([[ -n "$BANDWIDTH" ]] && echo "-r" "${BANDWIDTH}M") -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then
echo "qemu-img convert failed for $disk $output"
cleanup
fi
name="datadisk"
done

# Encrypt backup files if requested
encrypt_backup "$dest"

sync

# Verify backup integrity if requested
if [[ "$VERIFY" == "true" ]]; then
verify_backup "$dest"
fi

ls -l --numeric-uid-gid $dest | awk '{print $5}'
}

Expand Down Expand Up @@ -233,7 +324,7 @@ cleanup() {

function usage {
echo ""
echo "Usage: $0 -o <operation> -v|--vm <domain name> -t <storage type> -s <storage address> -m <mount options> -p <backup path> -d <disks path> -q|--quiesce <true|false>"
echo "Usage: $0 -o <operation> -v|--vm <domain name> -t <storage type> -s <storage address> -m <mount options> -p <backup path> -d <disks path> -q|--quiesce <true|false> [-c] [-b <MiB/s>] [-e <passphrase file>] [--verify]"
echo ""
exit 1
}
Expand Down Expand Up @@ -280,6 +371,24 @@ while [[ $# -gt 0 ]]; do
shift
shift
;;
-c|--compress)
COMPRESS="true"
shift
;;
-b|--bandwidth)
BANDWIDTH="$2"
shift
shift
;;
-e|--encrypt)
ENCRYPT_PASSFILE="$2"
shift
shift
;;
--verify)
VERIFY="true"
shift
;;
-h|--help)
usage
shift
Expand Down
Loading