-
Notifications
You must be signed in to change notification settings - Fork 1.3k
NAS backup: compression, encryption, bandwidth throttle, integrity check #12898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.22
Are you sure you want to change the base?
Changes from 1 commit
2c40fdd
05076db
4884980
3d65f34
c11e0b8
45ce015
2150959
8a84d8e
0effda2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
|
DaanHoogland marked this conversation as resolved.
Outdated
|
||
| } | ||
| 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)); | ||
|
|
@@ -594,7 +654,12 @@ public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) { | |
| @Override | ||
| public ConfigKey<?>[] getConfigKeys() { | ||
| return new ConfigKey[]{ | ||
| NASBackupRestoreMountTimeout | ||
| NASBackupRestoreMountTimeout, | ||
| NASBackupCompressionEnabled, | ||
| NASBackupEncryptionEnabled, | ||
| NASBackupEncryptionPassphrase, | ||
| NASBackupBandwidthLimitMbps, | ||
| NASBackupIntegrityCheckEnabled | ||
| }; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||
|
|
@@ -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"); | ||||||||||||
|
DaanHoogland marked this conversation as resolved.
Outdated
|
||||||||||||
| 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()); | ||||||||||||
| } | ||||||||||||
|
||||||||||||
| } | |
| } | |
| } else { | |
| logger.error("Encryption requested for backup but no encryption passphrase was provided"); | |
| return new BackupAnswer(command, false, "Encryption requested but encryption_passphrase is missing or empty"); |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||
|
|
@@ -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 | ||||||||
|
Comment on lines
+99
to
+102
|
||||||||
| 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" | ||||||||
|
DaanHoogland marked this conversation as resolved.
Outdated
|
||||||||
| fi | ||||||||
| done | ||||||||
| if [[ $failed -ne 0 ]]; then | ||||||||
| echo "One or more backup files failed verification" | ||||||||
| exit 1 | ||||||||
| fi | ||||||||
|
Comment on lines
+157
to
+160
|
||||||||
| } | ||||||||
|
|
||||||||
| ### Operation methods ### | ||||||||
|
|
||||||||
| backup_running_vm() { | ||||||||
|
|
@@ -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 | ||||||||
|
|
@@ -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 | ||||||||
|
|
@@ -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 | ||||||||
|
DaanHoogland marked this conversation as resolved.
Outdated
|
||||||||
| echo "qemu-img convert failed for $disk $output" | ||||||||
| cleanup | ||||||||
|
||||||||
| cleanup | |
| cleanup | |
| return 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
detailsmay carry sensitive values (e.g., an encryption passphrase). CloudStack’s Gson logging usesLoggingExclusionStrategywith@LogLevelto exclude fields, so leaving this unannotated can leak secrets in debug logs. Annotatedetailswith@LogLevel(Off)(or avoid putting secrets in this map).