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


##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -206,6 +246,9 @@ 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
+        applyBackupEnhancementDetails(command, vm.getDataCenterId());

Review Comment:
   Blocking: `createBackupObject()` on line 242 has already persisted a 
`BackupVO` in `BackingUp` state by the time this runs, so when encryption is 
enabled without a passphrase the `CloudRuntimeException` thrown from 
`applyBackupEnhancementDetails` leaves an orphaned backup row stuck in 
`BackingUp` forever.
   
   Every other failure path in `takeBackup` calls 
`backupDao.remove(backupVO.getId())` before throwing. Since the validation 
needs nothing from the backup object, moving this call above line 242 fixes it 
cleanly.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -87,6 +91,75 @@ 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"
+    return 1
+  fi
+  log -ne "Encrypting backup files with LUKS"
+  # Preserve compression if it was requested upstream — otherwise the
+  # encrypt-step re-convert produces an uncompressed (but encrypted) qcow2,
+  # silently discarding the compression work done earlier.
+  local compress_flag=""
+  if [[ "$COMPRESS" == "true" ]]; then
+    compress_flag="-c"
+  fi
+  for img in "$backup_dir"/*.qcow2; do
+    [[ -f "$img" ]] || continue
+    local tmp_img="${img}.luks"
+    if qemu-img convert $compress_flag -O qcow2 \

Review Comment:
   Blocking: qcow2 cannot compress and encrypt at the same time, so enabling 
both `nas.backup.compression.enabled` and `nas.backup.encryption.enabled` fails 
every backup and then deletes it.
   
   Reproduced with qemu-img 9.2.3:
   
   ```
   $ qemu-img convert -c -O qcow2 --object secret,id=sec0,file=pass.key \
       -o encrypt.format=luks,encrypt.key-secret=sec0 plain.qcow2 out.qcow2
   qemu-img: Compression and encryption not supported at the same time
   ```
   
   With both settings on, `encrypt_backup` returns 1, the caller runs 
`cleanup`, and the whole backup directory is removed. This hits both the 
running-VM and stopped-VM paths.
   
   The comment above `compress_flag` says it exists so compression is not 
silently discarded, but the result is a hard failure instead. Could you either 
reject the combination up front in `applyBackupEnhancementDetails` (clearest, 
the admin gets told why) or drop `-c` here and document that encryption wins? 
Either way it would be good to add a test for the two together, since neither 
the unit tests nor the test-plan checklist covers that combination today.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -254,14 +366,30 @@ 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:
   Blocking: `qemu-img convert -r` is not available on the minimum QEMU this 
script supports. `sanity_checks()` only requires QEMU >= 4.2.0, but `-r 
rate_limit` was added much later (it is present in 9.2, absent in 4.2). On a 
4.2/5.x host (RHEL 8, Ubuntu 20.04) enabling `nas.backup.bandwidth.limit.mbps` 
would make every stopped-VM backup fail with `invalid option -- 'r'`.
   
   Could you check the exact QEMU version that introduced it and either probe 
for support with a graceful fallback, or raise the minimum version check when 
bandwidth limiting is requested?



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -254,14 +366,30 @@ 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:
   `ionice -c 3` is applied unconditionally here, so it also affects backups 
where none of the four new settings are enabled. That is a silent default 
behaviour change for existing users. Could it be gated on `$BANDWIDTH` being 
set?
   
   Worth noting too that `-c 3` (idle) only has an effect under CFQ/BFQ. With 
mq-deadline or none, the default for NVMe on current kernels, it is a no-op, so 
it may not buy much even when the feature is on.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -87,6 +91,75 @@ 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"
+    return 1
+  fi
+  log -ne "Encrypting backup files with LUKS"
+  # Preserve compression if it was requested upstream — otherwise the
+  # encrypt-step re-convert produces an uncompressed (but encrypted) qcow2,
+  # silently discarding the compression work done earlier.
+  local compress_flag=""
+  if [[ "$COMPRESS" == "true" ]]; then
+    compress_flag="-c"
+  fi
+  for img in "$backup_dir"/*.qcow2; do
+    [[ -f "$img" ]] || continue
+    local tmp_img="${img}.luks"
+    if qemu-img convert $compress_flag -O qcow2 \
+        --object "secret,id=sec0,file=$ENCRYPT_PASSFILE" \
+        -o "encrypt.format=luks,encrypt.key-secret=sec0" \
+        "$img" "$tmp_img" >> "$logFile" 2>&1; then
+      mv "$tmp_img" "$img"
+      log -ne "Encrypted: $img"
+    else
+      echo "Encryption failed for $img"
+      rm -f "$tmp_img"
+      return 1
+    fi
+  done
+}
+
+verify_backup() {
+  local backup_dir="$1"
+  local failed=0
+  # If encryption was applied to this backup, qemu-img check has to open the
+  # qcow2 with the same LUKS secret — otherwise every verification call fails
+  # with a "Could not open" error and --verify is unusable on encrypted
+  # backups.
+  local check_secret=()
+  if [[ -n "$ENCRYPT_PASSFILE" && -f "$ENCRYPT_PASSFILE" ]]; then
+    check_secret=(--object "secret,id=sec0,file=$ENCRYPT_PASSFILE")
+  fi
+  for img in "$backup_dir"/*.qcow2; do
+    [[ -f "$img" ]] || continue
+    local check_ok=0
+    if [[ ${#check_secret[@]} -gt 0 ]]; then
+      qemu-img check "${check_secret[@]}" --image-opts \
+        "driver=qcow2,file.filename=$img,encrypt.key-secret=sec0" \
+        > /dev/null 2>&1 && check_ok=1
+    else
+      qemu-img check "$img" > /dev/null 2>&1 && check_ok=1
+    fi
+    if [[ $check_ok -eq 1 ]]; then
+      log -ne "Backup verification passed: $img"
+    else
+      echo "Backup verification failed for $img"
+      log -ne "Backup verification FAILED: $img"

Review Comment:
   Only exit code 0 is accepted, and a non-zero result makes the caller run 
`cleanup`, deleting the entire backup.
   
   `qemu-img check` uses 2 for a corrupt image and 3 for leaked clusters. Leaks 
are benign, so as written a backup with leaked clusters is declared failed and 
destroyed. Could you confirm the exit code semantics on your side and treat 3 
as a warning rather than a failure?
   
   Separately, deleting the backup on a verification failure removes the 
evidence needed to diagnose it. Leaving it in place and reporting the failure 
would probably be more useful for an integrity check.



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -176,6 +249,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"

Review Comment:
   Two things here.
   
   1. stderr is sent to `/dev/null` and the failure is swallowed by `|| true`, 
but line 257 then logs that the limit was applied regardless. If the call 
fails, the admin sees a log line claiming throttling is active while nothing is 
throttled. Could you capture the exit status and log the actual outcome?
   
   2. I am not sure libvirt accepts `blockjob --bandwidth` against a push-mode 
backup job. Backups are reported through `domjobinfo` rather than `blockjob`, 
so the lookup may not resolve. Could you confirm on your target libvirt version 
that the bandwidth is actually applied, rather than the command erroring out 
silently?
   
   Minor: `awk '/disk/{print$3}'` matches any line containing "disk", including 
a cdrom row whose source path happens to contain it. The existing code a few 
lines below uses `awk '$2=="disk"'`, which would be more robust and consistent.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java:
##########
@@ -257,31 +266,66 @@ private String getBackupPath(String mountDirectory, 
String backupPath, String ba
         return bkpPath;
     }
 
-    private boolean checkBackupFileImage(String backupPath) {
-        int exitValue = 
Script.runSimpleBashScriptForExitValue(String.format("qemu-img check %s", 
backupPath));
-        return exitValue == 0;
+    private boolean checkBackupFileImage(String backupPath, File keyFile) {
+        if (!isEncryptedImage(backupPath)) {
+            int exitValue = 
Script.runSimpleBashScriptForExitValue(String.format("qemu-img check %s", 
backupPath));
+            return exitValue == 0;
+        }
+        List<String> cmd = new ArrayList<>(List.of("qemu-img", "check"));
+        cmd.addAll(encryptedSourceArgs(backupPath, keyFile));
+        return Script.executeCommandForExitValue(cmd.toArray(new String[0])) 
== 0;
+    }
+
+    /**
+     * True when qemu reports the backup qcow2 as encrypted (LUKS, produced by 
nasbackup.sh {@code -e}).
+     * Reading the header needs no secret, so this works before any passphrase 
is involved.
+     */
+    private boolean isEncryptedImage(String backupPath) {
+        String info = Script.executeCommand("qemu-img", "info", 
"--output=json", backupPath);
+        return info != null && info.replaceAll("\\s", 
"").contains("\"encrypted\":true");
+    }

Review Comment:
   This fails open. If `Script.executeCommand` returns null (qemu-img missing, 
path unreadable, timeout) the method returns `false`, and 
`replaceVolumeWithBackup` then falls through to `rsync`, copying an encrypted 
qcow2 verbatim onto the volume. The restore reports success but the volume is 
unbootable.
   
   Could you distinguish "not encrypted" from "could not determine" and fail 
loudly on the latter?



-- 
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