Skip to content

Commit 2c40fdd

Browse files
committed
NAS backup enhancements: compression, encryption, bandwidth throttle, integrity check
Adds four optional features to NAS backup operations, configurable at zone scope via CloudStack global settings: - Compression (-c): qcow2 internal compression of backup files Config: nas.backup.compression.enabled (default: false) - LUKS Encryption (-e): encrypt backup files at rest using qemu-img Config: nas.backup.encryption.enabled (default: false) Config: nas.backup.encryption.passphrase (Secure category) - Bandwidth Throttle (-b): limit backup I/O bandwidth via virsh blockjob for running VMs or qemu-img -r for stopped VMs Config: nas.backup.bandwidth.limit.mbps (default: 0/unlimited) - Integrity Check (--verify): qemu-img check after backup creation Config: nas.backup.integrity.check (default: false) All features are disabled by default and fully backward compatible. Settings are read from zone-scoped ConfigKeys in NASBackupProvider, passed to the KVM agent via TakeBackupCommand details map, and translated to nasbackup.sh CLI flags in LibvirtTakeBackupCommandWrapper. Changes: - nasbackup.sh: add -c, -b, -e, --verify flags with encrypt_backup() and verify_backup() helper functions - TakeBackupCommand.java: add details map for passing config to agent - NASBackupProvider.java: add 5 ConfigKeys, populate command details - LibvirtTakeBackupCommandWrapper.java: extract details, build CLI args, handle passphrase temp file lifecycle Combines and supersedes PRs #12844, #12846, #12848, #12845
1 parent c1af36f commit 2c40fdd

File tree

4 files changed

+245
-14
lines changed

4 files changed

+245
-14
lines changed

core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import com.cloud.agent.api.LogLevel;
2424
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
2525

26+
import java.util.HashMap;
2627
import java.util.List;
28+
import java.util.Map;
2729

2830
public class TakeBackupCommand extends Command {
2931
private String vmName;
@@ -35,6 +37,7 @@ public class TakeBackupCommand extends Command {
3537
private Boolean quiesce;
3638
@LogLevel(LogLevel.Log4jLevel.Off)
3739
private String mountOptions;
40+
private Map<String, String> details = new HashMap<>();
3841

3942
public TakeBackupCommand(String vmName, String backupPath) {
4043
super();
@@ -106,6 +109,18 @@ public void setQuiesce(Boolean quiesce) {
106109
this.quiesce = quiesce;
107110
}
108111

112+
public Map<String, String> getDetails() {
113+
return details;
114+
}
115+
116+
public void setDetails(Map<String, String> details) {
117+
this.details = details;
118+
}
119+
120+
public void addDetail(String key, String value) {
121+
this.details.put(key, value);
122+
}
123+
109124
@Override
110125
public boolean executeInSequence() {
111126
return true;

plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,46 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
8484
true,
8585
BackupFrameworkEnabled.key());
8686

87+
ConfigKey<Boolean> NASBackupCompressionEnabled = new ConfigKey<>("Advanced", Boolean.class,
88+
"nas.backup.compression.enabled",
89+
"false",
90+
"Enable qcow2 compression for NAS backup files.",
91+
true,
92+
ConfigKey.Scope.Zone,
93+
BackupFrameworkEnabled.key());
94+
95+
ConfigKey<Boolean> NASBackupEncryptionEnabled = new ConfigKey<>("Advanced", Boolean.class,
96+
"nas.backup.encryption.enabled",
97+
"false",
98+
"Enable LUKS encryption for NAS backup files.",
99+
true,
100+
ConfigKey.Scope.Zone,
101+
BackupFrameworkEnabled.key());
102+
103+
ConfigKey<String> NASBackupEncryptionPassphrase = new ConfigKey<>("Secure", String.class,
104+
"nas.backup.encryption.passphrase",
105+
"",
106+
"Passphrase for LUKS encryption of NAS backup files. Required when encryption is enabled.",
107+
true,
108+
ConfigKey.Scope.Zone,
109+
BackupFrameworkEnabled.key());
110+
111+
ConfigKey<Integer> NASBackupBandwidthLimitMbps = new ConfigKey<>("Advanced", Integer.class,
112+
"nas.backup.bandwidth.limit.mbps",
113+
"0",
114+
"Bandwidth limit in MiB/s for backup operations (0 = unlimited).",
115+
true,
116+
ConfigKey.Scope.Zone,
117+
BackupFrameworkEnabled.key());
118+
119+
ConfigKey<Boolean> NASBackupIntegrityCheckEnabled = new ConfigKey<>("Advanced", Boolean.class,
120+
"nas.backup.integrity.check",
121+
"false",
122+
"Run qemu-img check on backup files after creation to verify integrity.",
123+
true,
124+
ConfigKey.Scope.Zone,
125+
BackupFrameworkEnabled.key());
126+
87127
@Inject
88128
private BackupDao backupDao;
89129

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

248+
// Pass optional backup enhancement settings from zone-scoped configs
249+
Long zoneId = vm.getDataCenterId();
250+
if (Boolean.TRUE.equals(NASBackupCompressionEnabled.valueIn(zoneId))) {
251+
command.addDetail("compression", "true");
252+
}
253+
if (Boolean.TRUE.equals(NASBackupEncryptionEnabled.valueIn(zoneId))) {
254+
command.addDetail("encryption", "true");
255+
String passphrase = NASBackupEncryptionPassphrase.valueIn(zoneId);
256+
if (passphrase != null && !passphrase.isEmpty()) {
257+
command.addDetail("encryption_passphrase", passphrase);
258+
}
259+
}
260+
Integer bandwidthLimit = NASBackupBandwidthLimitMbps.valueIn(zoneId);
261+
if (bandwidthLimit != null && bandwidthLimit > 0) {
262+
command.addDetail("bandwidth_limit", String.valueOf(bandwidthLimit));
263+
}
264+
if (Boolean.TRUE.equals(NASBackupIntegrityCheckEnabled.valueIn(zoneId))) {
265+
command.addDetail("integrity_check", "true");
266+
}
267+
208268
if (VirtualMachine.State.Stopped.equals(vm.getState())) {
209269
List<VolumeVO> vmVolumes = volumeDao.findByInstance(vm.getId());
210270
vmVolumes.sort(Comparator.comparing(Volume::getDeviceId));
@@ -594,7 +654,12 @@ public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
594654
@Override
595655
public ConfigKey<?>[] getConfigKeys() {
596656
return new ConfigKey[]{
597-
NASBackupRestoreMountTimeout
657+
NASBackupRestoreMountTimeout,
658+
NASBackupCompressionEnabled,
659+
NASBackupEncryptionEnabled,
660+
NASBackupEncryptionPassphrase,
661+
NASBackupBandwidthLimitMbps,
662+
NASBackupIntegrityCheckEnabled
598663
};
599664
}
600665

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@
3434
import org.apache.cloudstack.backup.TakeBackupCommand;
3535
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
3636

37+
import java.io.File;
38+
import java.io.FileWriter;
39+
import java.io.IOException;
3740
import java.util.ArrayList;
3841
import java.util.Arrays;
3942
import java.util.List;
43+
import java.util.Map;
4044
import java.util.Objects;
4145

4246
@ResourceWrapper(handles = TakeBackupCommand.class)
@@ -68,21 +72,59 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir
6872
}
6973
}
7074

75+
List<String> cmdArgs = new ArrayList<>();
76+
cmdArgs.add(libvirtComputingResource.getNasBackupPath());
77+
cmdArgs.add("-o"); cmdArgs.add("backup");
78+
cmdArgs.add("-v"); cmdArgs.add(vmName);
79+
cmdArgs.add("-t"); cmdArgs.add(backupRepoType);
80+
cmdArgs.add("-s"); cmdArgs.add(backupRepoAddress);
81+
cmdArgs.add("-m"); cmdArgs.add(Objects.nonNull(mountOptions) ? mountOptions : "");
82+
cmdArgs.add("-p"); cmdArgs.add(backupPath);
83+
cmdArgs.add("-q"); cmdArgs.add(command.getQuiesce() != null && command.getQuiesce() ? "true" : "false");
84+
cmdArgs.add("-d"); cmdArgs.add(diskPaths.isEmpty() ? "" : String.join(",", diskPaths));
85+
86+
// Append optional enhancement flags from management server config
87+
File passphraseFile = null;
88+
Map<String, String> details = command.getDetails();
89+
if (details != null) {
90+
if ("true".equals(details.get("compression"))) {
91+
cmdArgs.add("-c");
92+
}
93+
if ("true".equals(details.get("encryption"))) {
94+
String passphrase = details.get("encryption_passphrase");
95+
if (passphrase != null && !passphrase.isEmpty()) {
96+
try {
97+
passphraseFile = File.createTempFile("cs-backup-enc-", ".key");
98+
passphraseFile.deleteOnExit();
99+
try (FileWriter fw = new FileWriter(passphraseFile)) {
100+
fw.write(passphrase);
101+
}
102+
cmdArgs.add("-e"); cmdArgs.add(passphraseFile.getAbsolutePath());
103+
} catch (IOException e) {
104+
logger.error("Failed to create encryption passphrase file", e);
105+
return new BackupAnswer(command, false, "Failed to create encryption passphrase file: " + e.getMessage());
106+
}
107+
}
108+
}
109+
String bwLimit = details.get("bandwidth_limit");
110+
if (bwLimit != null && !"0".equals(bwLimit)) {
111+
cmdArgs.add("-b"); cmdArgs.add(bwLimit);
112+
}
113+
if ("true".equals(details.get("integrity_check"))) {
114+
cmdArgs.add("--verify");
115+
}
116+
}
117+
71118
List<String[]> commands = new ArrayList<>();
72-
commands.add(new String[]{
73-
libvirtComputingResource.getNasBackupPath(),
74-
"-o", "backup",
75-
"-v", vmName,
76-
"-t", backupRepoType,
77-
"-s", backupRepoAddress,
78-
"-m", Objects.nonNull(mountOptions) ? mountOptions : "",
79-
"-p", backupPath,
80-
"-q", command.getQuiesce() != null && command.getQuiesce() ? "true" : "false",
81-
"-d", diskPaths.isEmpty() ? "" : String.join(",", diskPaths)
82-
});
119+
commands.add(cmdArgs.toArray(new String[0]));
83120

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

123+
// Clean up passphrase file after backup completes
124+
if (passphraseFile != null && passphraseFile.exists()) {
125+
passphraseFile.delete();
126+
}
127+
86128
if (result.first() != 0) {
87129
logger.debug("Failed to take VM backup: " + result.second());
88130
BackupAnswer answer = new BackupAnswer(command, false, result.second().trim());

scripts/vm/hypervisor/kvm/nasbackup.sh

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ MOUNT_OPTS=""
3232
BACKUP_DIR=""
3333
DISK_PATHS=""
3434
QUIESCE=""
35+
COMPRESS=""
36+
BANDWIDTH=""
37+
ENCRYPT_PASSFILE=""
38+
VERIFY=""
3539
logFile="/var/log/cloudstack/agent/agent.log"
3640

3741
EXIT_CLEANUP_FAILED=20
@@ -87,6 +91,52 @@ sanity_checks() {
8791
log -ne "Environment Sanity Checks successfully passed"
8892
}
8993

94+
encrypt_backup() {
95+
local backup_dir="$1"
96+
if [[ -z "$ENCRYPT_PASSFILE" ]]; then
97+
return
98+
fi
99+
if [[ ! -f "$ENCRYPT_PASSFILE" ]]; then
100+
echo "Encryption passphrase file not found: $ENCRYPT_PASSFILE"
101+
exit 1
102+
fi
103+
log -ne "Encrypting backup files with LUKS"
104+
for img in "$backup_dir"/*.qcow2; do
105+
[[ -f "$img" ]] || continue
106+
local tmp_img="${img}.luks"
107+
if qemu-img convert -O qcow2 \
108+
--object "secret,id=sec0,file=$ENCRYPT_PASSFILE" \
109+
-o "encrypt.format=luks,encrypt.key-secret=sec0" \
110+
"$img" "$tmp_img" 2>&1 | tee -a "$logFile"; then
111+
mv "$tmp_img" "$img"
112+
log -ne "Encrypted: $img"
113+
else
114+
echo "Encryption failed for $img"
115+
rm -f "$tmp_img"
116+
exit 1
117+
fi
118+
done
119+
}
120+
121+
verify_backup() {
122+
local backup_dir="$1"
123+
local failed=0
124+
for img in "$backup_dir"/*.qcow2; do
125+
[[ -f "$img" ]] || continue
126+
if ! qemu-img check "$img" > /dev/null 2>&1; then
127+
echo "Backup verification failed for $img"
128+
log -ne "Backup verification FAILED: $img"
129+
failed=1
130+
else
131+
log -ne "Backup verification passed: $img"
132+
fi
133+
done
134+
if [[ $failed -ne 0 ]]; then
135+
echo "One or more backup files failed verification"
136+
exit 1
137+
fi
138+
}
139+
90140
### Operation methods ###
91141

92142
backup_running_vm() {
@@ -128,6 +178,14 @@ backup_running_vm() {
128178
exit 1
129179
fi
130180

181+
# Throttle backup bandwidth if requested (MiB/s per disk)
182+
if [[ -n "$BANDWIDTH" ]]; then
183+
for disk in $(virsh -c qemu:///system domblklist $VM --details 2>/dev/null | awk '/disk/{print$3}'); do
184+
virsh -c qemu:///system blockjob $VM $disk --bandwidth "${BANDWIDTH}" 2>/dev/null || true
185+
done
186+
log -ne "Backup bandwidth limited to ${BANDWIDTH} MiB/s per disk for $VM"
187+
fi
188+
131189
# Backup domain information
132190
virsh -c qemu:///system dumpxml $VM > $dest/domain-config.xml 2>/dev/null
133191
virsh -c qemu:///system dominfo $VM > $dest/dominfo.xml 2>/dev/null
@@ -147,8 +205,32 @@ backup_running_vm() {
147205
done
148206

149207
rm -f $dest/backup.xml
208+
209+
# Compress backup files if requested
210+
if [[ "$COMPRESS" == "true" ]]; then
211+
log -ne "Compressing backup files for $VM"
212+
for img in "$dest"/*.qcow2; do
213+
[[ -f "$img" ]] || continue
214+
local tmp_img="${img}.tmp"
215+
if qemu-img convert -c -O qcow2 "$img" "$tmp_img" 2>&1 | tee -a "$logFile"; then
216+
mv "$tmp_img" "$img"
217+
else
218+
log -ne "Warning: compression failed for $img, keeping uncompressed"
219+
rm -f "$tmp_img"
220+
fi
221+
done
222+
fi
223+
224+
# Encrypt backup files if requested
225+
encrypt_backup "$dest"
226+
150227
sync
151228

229+
# Verify backup integrity if requested
230+
if [[ "$VERIFY" == "true" ]]; then
231+
verify_backup "$dest"
232+
fi
233+
152234
# Print statistics
153235
virsh -c qemu:///system domjobinfo $VM --completed
154236
du -sb $dest | cut -f1
@@ -174,14 +256,23 @@ backup_stopped_vm() {
174256
volUuid="${disk##*/}"
175257
fi
176258
output="$dest/$name.$volUuid.qcow2"
177-
if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then
259+
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
178260
echo "qemu-img convert failed for $disk $output"
179261
cleanup
180262
fi
181263
name="datadisk"
182264
done
265+
266+
# Encrypt backup files if requested
267+
encrypt_backup "$dest"
268+
183269
sync
184270

271+
# Verify backup integrity if requested
272+
if [[ "$VERIFY" == "true" ]]; then
273+
verify_backup "$dest"
274+
fi
275+
185276
ls -l --numeric-uid-gid $dest | awk '{print $5}'
186277
}
187278

@@ -233,7 +324,7 @@ cleanup() {
233324

234325
function usage {
235326
echo ""
236-
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>"
327+
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]"
237328
echo ""
238329
exit 1
239330
}
@@ -280,6 +371,24 @@ while [[ $# -gt 0 ]]; do
280371
shift
281372
shift
282373
;;
374+
-c|--compress)
375+
COMPRESS="true"
376+
shift
377+
;;
378+
-b|--bandwidth)
379+
BANDWIDTH="$2"
380+
shift
381+
shift
382+
;;
383+
-e|--encrypt)
384+
ENCRYPT_PASSFILE="$2"
385+
shift
386+
shift
387+
;;
388+
--verify)
389+
VERIFY="true"
390+
shift
391+
;;
283392
-h|--help)
284393
usage
285394
shift

0 commit comments

Comments
 (0)