Copilot commented on code in PR #12898:
URL: https://github.com/apache/cloudstack/pull/12898#discussion_r3008588977


##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -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");
+        }

Review Comment:
   The new zone-scoped feature flags are translated into `TakeBackupCommand` 
details here, but the existing `NASBackupProviderTest.takeBackupSuccessfully` 
doesn’t assert the details map contents. Add/extend unit tests to verify the 
correct details are added for each config (compression, bandwidth limit, 
integrity check, and encryption+passphrase; and that encryption without 
passphrase fails).



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -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

Review Comment:
   On `qemu-img convert` failure, this calls `cleanup` but then continues 
execution (no `exit`/`return`). If cleanup succeeds, the function proceeds to 
later steps with an unmounted/removed `dest`, which can cause confusing 
follow-on failures and potentially report incorrect results. Exit the script 
(or `return 1`) after `cleanup` here.
   ```suggestion
         cleanup
         return 1
   ```



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java:
##########
@@ -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());
+                    }

Review Comment:
   The temp passphrase file can be left behind if an exception is thrown after 
`createTempFile` succeeds (e.g., `FileWriter` fails) because the catch returns 
without deleting it. Also consider setting strict permissions (0600) and using 
an explicit charset (UTF-8) when writing the passphrase.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -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

Review Comment:
   This redirects stdout to `> "$logFile"`, which truncates 
`/var/log/cloudstack/agent/agent.log` each time a stopped-VM disk conversion 
runs (and for every disk). Use append (`>>`) or pipe through `tee -a` to avoid 
destroying the agent log.
   ```suggestion
       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
   ```



##########
core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java:
##########
@@ -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;

Review Comment:
   `setDetails` assigns the map directly and can set it to null; later 
`addDetail` will NPE. Normalize null to an empty map inside `setDetails` (or 
remove the setter / keep `details` final) to make the command robust.
   ```suggestion
           this.details = details != null ? details : new HashMap<>();
   ```



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java:
##########
@@ -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());
+                    }

Review Comment:
   If `details` indicates encryption is enabled but the passphrase is 
missing/empty, the wrapper silently skips adding `-e` and the backup proceeds 
unencrypted. This should fail the command (or at least log and return an error) 
to avoid reporting a successful encrypted backup when encryption was requested.
   ```suggestion
                       }
                   } 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");
   ```



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -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

Review Comment:
   `verify_backup` exits directly on failure, which skips `cleanup()`/unmount 
in the calling backup paths and can leave the NAS store mounted and temp 
directories behind. Return failure to the caller and perform cleanup/unmount 
before exiting.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -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

Review Comment:
   `encrypt_backup` calls `exit 1` on missing/invalid passphrase file, which 
bypasses `cleanup()`/unmount logic and can leave the NAS mount + temp dir 
behind. Prefer returning a non-zero status and letting callers invoke 
`cleanup()` (or add a trap-based cleanup) so failures don’t leak 
mounts/directories.



##########
core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java:
##########
@@ -35,6 +37,7 @@ public class TakeBackupCommand extends Command {
     private Boolean quiesce;
     @LogLevel(LogLevel.Log4jLevel.Off)
     private String mountOptions;

Review Comment:
   `details` may carry sensitive values (e.g., an encryption passphrase). 
CloudStack’s Gson logging uses `LoggingExclusionStrategy` with `@LogLevel` to 
exclude fields, so leaving this unannotated can leak secrets in debug logs. 
Annotate `details` with `@LogLevel(Off)` (or avoid putting secrets in this map).
   ```suggestion
       private String mountOptions;
       @LogLevel(LogLevel.Log4jLevel.Off)
   ```



##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -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);
+            }

Review Comment:
   When encryption is enabled at zone scope, the code sets the `encryption` 
detail even if the passphrase is missing/empty, which will currently result in 
an unencrypted backup on the agent side. Since the passphrase is required, fail 
fast here with a clear error instead of proceeding (e.g., throw a 
CloudRuntimeException / mark backup failed).
   ```suggestion
               String passphrase = 
NASBackupEncryptionPassphrase.valueIn(zoneId);
               if (passphrase == null || passphrase.trim().isEmpty()) {
                   throw new CloudRuntimeException(String.format(
                           "NAS backup encryption is enabled for zone %d but no 
encryption passphrase is configured",
                           zoneId));
               }
               command.addDetail("encryption", "true");
               command.addDetail("encryption_passphrase", passphrase);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to