Skip to content

NAS backup: compression, encryption, bandwidth throttle, integrity check - #12898

Open
jmsperu wants to merge 12 commits into
apache:4.22from
jmsperu:fix/nasbackup-enhancements-combined
Open

NAS backup: compression, encryption, bandwidth throttle, integrity check#12898
jmsperu wants to merge 12 commits into
apache:4.22from
jmsperu:fix/nasbackup-enhancements-combined

Conversation

@jmsperu

@jmsperu jmsperu commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds four optional, zone-scoped features to NAS backup operations on KVM, all disabled by default:

  • Compression (-c): Uses qcow2 internal compression (qemu-img convert -c) to reduce backup size
  • LUKS Encryption (-e): Encrypts backup files at rest using LUKS via qemu-img convert --object secret
  • Bandwidth Throttle (-b): Limits backup I/O — virsh blockjob --bandwidth for running VMs, qemu-img convert -r + ionice for stopped VMs
  • Integrity Check (--verify): Runs qemu-img check on each backup file after creation

Configuration Keys (Zone scope)

Setting Type Default Description
nas.backup.compression.enabled Boolean false Enable qcow2 compression for backup files
nas.backup.encryption.enabled Boolean false Enable LUKS encryption for backup files
nas.backup.encryption.passphrase String (Secure) "" Passphrase for LUKS encryption
nas.backup.bandwidth.limit.mbps Integer 0 Bandwidth limit in MiB/s (0 = unlimited)
nas.backup.integrity.check Boolean false Run qemu-img check after backup

Architecture

  1. NASBackupProvider reads zone-scoped ConfigKeys and populates a details map on TakeBackupCommand
  2. TakeBackupCommand carries the details map from management server to KVM agent
  3. LibvirtTakeBackupCommandWrapper extracts the details and translates them to nasbackup.sh CLI flags
  4. nasbackup.sh implements the actual compression, encryption, throttling, and verification logic

Files Changed

  • scripts/vm/hypervisor/kvm/nasbackup.sh — new -c, -b, -e, --verify flags with encrypt_backup() and verify_backup() functions
  • core/.../TakeBackupCommand.java — added details map (HashMap) with getter/setter/addDetail
  • plugins/backup/nas/.../NASBackupProvider.java — 5 new ConfigKeys, populate command details in takeBackup()
  • plugins/hypervisors/kvm/.../LibvirtTakeBackupCommandWrapper.java — extract details, build dynamic CLI args, temp passphrase file lifecycle

Notes

Test plan

  • Verify backup works with all four features disabled (default) — no behavioral change
  • Enable nas.backup.compression.enabled at zone scope, take backup, verify qcow2 files are compressed
  • Enable nas.backup.bandwidth.limit.mbps (e.g. 50), take backup of running VM, verify virsh blockjob bandwidth is applied
  • Enable nas.backup.bandwidth.limit.mbps, take backup of stopped VM, verify qemu-img -r rate limit is applied
  • Enable nas.backup.encryption.enabled with passphrase, take backup, verify files are LUKS encrypted (qemu-img info shows encryption)
  • Enable nas.backup.integrity.check, take backup, verify qemu-img check runs and passes
  • Test with multiple features enabled simultaneously (compression + integrity check)
  • Verify restore still works for backups created with compression/encryption
  • Test with RBD storage pools — verify bandwidth throttle applies correctly

… 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 apache#12844, apache#12846, apache#12848, apache#12845
@codecov

codecov Bot commented Mar 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.45251% with 69 lines in your changes missing coverage. Please review.
✅ Project coverage is 17.79%. Comparing base (7ea1dca) to head (7f2c57a).

Files with missing lines Patch % Lines
...ource/wrapper/LibvirtTakeBackupCommandWrapper.java 48.00% 25 Missing and 1 partial ⚠️
...ce/wrapper/LibvirtRestoreBackupCommandWrapper.java 64.28% 15 Missing and 5 partials ⚠️
.../kvm/resource/wrapper/NasBackupPassphraseFile.java 62.50% 6 Missing and 3 partials ⚠️
...rg/apache/cloudstack/backup/TakeBackupCommand.java 40.00% 6 Missing ⚠️
...rg/apache/cloudstack/backup/NASBackupProvider.java 84.84% 2 Missing and 3 partials ⚠️
...apache/cloudstack/backup/RestoreBackupCommand.java 50.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               4.22   #12898    +/-   ##
==========================================
  Coverage     17.79%   17.79%            
- Complexity    15995    16019    +24     
==========================================
  Files          5928     5929     +1     
  Lines        534306   534456   +150     
  Branches      65383    65401    +18     
==========================================
+ Hits          95069    95124    +55     
- Misses       428467   428560    +93     
- Partials      10770    10772     +2     
Flag Coverage Δ
uitests 3.69% <ø> (ø)
unittests 18.88% <61.45%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional, zone-scoped enhancements for KVM NAS backups (compression, LUKS encryption, bandwidth throttling, and post-backup integrity verification) by plumbing config from management server → TakeBackupCommand details → KVM agent wrapper → nasbackup.sh flags.

Changes:

  • Add new CLI flags and implementation in nasbackup.sh for compression (-c), encryption (-e), bandwidth throttling (-b), and verification (--verify).
  • Extend TakeBackupCommand with a details map to carry optional settings to the agent.
  • Add zone-scoped NAS backup ConfigKeys and populate command details; update KVM wrapper to translate details into script args and manage a temporary passphrase file.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
scripts/vm/hypervisor/kvm/nasbackup.sh Implements compression/encryption/throttle/verify logic and argument parsing for NAS backup operations.
core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java Adds a details map to carry optional backup feature settings from management to agent.
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java Introduces zone-scoped ConfigKeys and passes enabled settings into TakeBackupCommand details.
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java Builds dynamic nasbackup.sh command args from TakeBackupCommand details and writes an encryption passphrase temp file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh
Comment thread core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java Outdated
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
- nasbackup.sh: Replace exit 1 with return 1 in encrypt_backup and
  verify_backup so callers can run cleanup before terminating
- nasbackup.sh: Append (>>) instead of truncate (>) agent.log in
  qemu-img convert for stopped VM backups
- nasbackup.sh: Add return 1 after cleanup on qemu-img convert failure
  to stop execution
- nasbackup.sh: Callers of encrypt_backup/verify_backup now check
  return code and run cleanup on failure
- LibvirtTakeBackupCommandWrapper: Fail with error when encryption is
  enabled but passphrase is missing instead of silently skipping
- LibvirtTakeBackupCommandWrapper: Delete temp passphrase file in
  finally block, set 0600 permissions, use explicit UTF-8 charset
- NASBackupProvider: Throw CloudRuntimeException when encryption is
  enabled but passphrase is null/empty
- NASBackupProviderTest: Add tests for compression, bandwidth,
  integrity check, encryption+passphrase, and encryption-without-
  passphrase failure scenarios
- TakeBackupCommand: Add @loglevel(Off) to details field to prevent
  passphrase leaking in debug logs
- TakeBackupCommand: Normalize null to empty HashMap in setDetails
@sureshanaparti

Copy link
Copy Markdown
Contributor

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@sureshanaparti a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 17323

@sureshanaparti

sureshanaparti commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

@jmsperu can you check/fix the build failure.

jmsperu added 2 commits April 2, 2026 00:45
Address remaining Copilot review feedback on PR apache#12898:
- Replace `2>&1 | tee -a` with `>> logFile 2>&1` in encrypt_backup,
  compress, and mount_operation to prevent tee from masking non-zero
  exit codes of qemu-img and mount commands
- Add `return 1` after cleanup on virsh backup job failure to prevent
  continuing execution with broken state
The test helper overrideConfigValue() was only setting _value on
ConfigKey, but zone-scoped configs (valueIn(zoneId)) fall back to
_defaultValue when s_depot is null in test context. Also set
_defaultValue via ReflectionTestUtils to ensure valueIn() returns
the expected test value.

Fixes: 4 assertion failures (compression, bandwidth, encryption,
integrity_check details all returned null) and 1 error
(encryption without passphrase expected CloudRuntimeException
but got NullPointerException from null config value).
@jmsperu

jmsperu commented Apr 2, 2026

Copy link
Copy Markdown
Collaborator Author

@sureshanaparti Fixed. The test failures were caused by overrideConfigValue() in NASBackupProviderTest only setting _value on ConfigKey, but the zone-scoped configs (valueIn(zoneId)) fall back to _defaultValue when s_depot is null in the test context. All 5 config values (compression, bandwidth, encryption, encryption passphrase, integrity check) were returning null instead of the test values.

The fix also sets _defaultValue via ReflectionTestUtils so valueIn() correctly resolves test values.

Also addressed in the previous commit: replaced 2>&1 | tee -a with >> logFile 2>&1 in nasbackup.sh to prevent tee from masking non-zero exit codes, and added return 1 after cleanup on virsh backup job failure.

Could you please retrigger the build? @blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@jmsperu a [SL] Jenkins job has been kicked to build packages. It will be bundled with` SystemVM template(s). I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 17346

@jmsperu

jmsperu commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

@DaanHoogland Thanks for the review — addressed both points in 0effda26e4:

Method extraction — pulled the inline enhancement logic into dedicated methods:

  • LibvirtTakeBackupCommandWrapper: extracted appendEnhancementFlags() + writePassphraseFile() (so execute() is a clean dispatch instead of a ~40-line inline block)
  • NASBackupProvider: extracted applyBackupEnhancementDetails()

(The shell side was already factored — encrypt_backup() / verify_backup() are standalone functions in nasbackup.sh.)

Testing — added LibvirtTakeBackupCommandWrapperTest (5 cases: compression/bandwidth/integrity flag mapping, bandwidth=0 skip, encryption writes the 0600 passphrase file + -e flag, and fail-fast when encryption is enabled without a passphrase). The existing NASBackupProviderTest (14 cases, covering the details map) stays green — confirming the refactor is behavior-preserving.

@DaanHoogland

Copy link
Copy Markdown
Contributor

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@DaanHoogland a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 18329

@DaanHoogland

Copy link
Copy Markdown
Contributor

@blueorangutan test

@blueorangutan

Copy link
Copy Markdown

@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

@blueorangutan

Copy link
Copy Markdown

[SF] Trillian test result (tid-16388)
Environment: kvm-ol8 (x2), zone: Advanced Networking with Mgmt server ol8
Total time taken: 51742 seconds
Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr12898-t16388-kvm-ol8.zip
Smoke tests completed. 149 look OK, 0 have errors, 0 did not run
Only failed and skipped tests results shown below:

Test Result Time (s) Test File

@weizhouapache weizhouapache modified the milestones: 4.23.0, 4.24.0 Jul 10, 2026
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
…; drop trailing '(Copilot review)' comment artifacts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

scripts/vm/hypervisor/kvm/nasbackup.sh:461

  • The -b/--bandwidth flag is accepted without validating that it is a positive integer. If a non-numeric value (or 0/negative) is passed, the later virsh/qemu-img calls will fail in confusing ways (and could treat the value as additional CLI args). Validate the argument at parse time and reject invalid values with a clear error.
    -b|--bandwidth)
      BANDWIDTH="$2"
      shift
      shift
      ;;

scripts/vm/hypervisor/kvm/nasbackup.sh:387

  • On mount failure, the script now only prints a generic "Failed to mount" message and redirects the actual mount error output solely into the agent log. This makes failures hard to diagnose from the management server side (stdout/stderr). Capture the mount output and include it in the error message while still appending it to the log.
  if mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS}) >> "$logFile" 2>&1; then
      log -ne "Successfully mounted ${NAS_TYPE} store"
  else
      echo "Failed to mount ${NAS_TYPE} store"
      exit 1

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

  • The temporary passphrase file deletion is currently best-effort but silent (passphraseFile.delete() return value is ignored). If deletion fails, the key file may remain on disk without any indication. Prefer Files.deleteIfExists(...) and log failures so operators can detect and remediate leftover secret files.
            // Clean up passphrase file after backup completes (best-effort).
            if (passphraseFile != null && passphraseFile.exists()) {
                passphraseFile.delete();
            }

scripts/vm/hypervisor/kvm/nasbackup.sh:233

  • The bandwidth-throttling virsh calls use unquoted $VM and $disk, which can break if either contains unexpected characters/whitespace. Quote these arguments to avoid word-splitting/globbing and keep behavior consistent with the other virsh invocations in this function.

This issue also appears in the following locations of the same file:

  • line 383
  • line 457
    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

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

  • The config key name "nas.backup.bandwidth.limit.mbps" suggests megabits/sec (Mbps), but the description and the script usage treat the value as MiB/sec. This mismatch is likely to confuse operators and lead to misconfiguration. Consider renaming the key (or changing the documented units and implementing conversion) to make the units unambiguous.
    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,

@jmsperu

jmsperu commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

The failing build shard here (test_lb_secondary_ip, test_list_nics, test_list_pod, …) is unrelated to this backup change — those are load-balancer/networking smoke tests, and the other 24 shards pass. Looks like a flaky/infra failure. Could a committer kick off a re-run of that shard? Thanks.

@github-project-automation github-project-automation Bot moved this to Backlog in Testing Aug 10, 2026
@DaanHoogland DaanHoogland removed the status in Testing Aug 10, 2026
@DaanHoogland DaanHoogland moved this to Backlog in Testing Aug 10, 2026
The restore path ran plain 'qemu-img check' and rsync/convert, so a backup taken
with nas.backup.encryption.enabled could not be verified or restored.

- RestoreBackupCommand carries the zone's passphrase (@loglevel Off); the NAS
  provider sets it whenever one is configured so older encrypted backups stay
  restorable after encryption is switched off.
- LibvirtRestoreBackupCommandWrapper probes 'qemu-img info' for an encrypted
  image and then checks/converts with '--object secret' + '--image-opts'. File
  based pools are decrypted during a qcow2 convert instead of being rsync'd
  (a copied LUKS volume would be unbootable); RBD/LINSTOR use the same secret
  on the raw convert. A clear error is returned when the backup is encrypted
  and no passphrase is configured.
- NasBackupPassphraseFile is the shared 0600 temp key file helper for the take
  and restore wrappers.
- Unit tests for the encrypted check/convert path, the missing-passphrase
  failure and the provider side.

Signed-off-by: James Peru <jmsperu@gmail.com>
@jmsperu

jmsperu commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 4d919dc. The important one is Copilot's most recent point: encrypted backups could be taken but not restored, because the restore wrapper opened the qcow2 without the LUKS secret. That is now fixed end to end (details in-thread): the passphrase travels on RestoreBackupCommand, the wrapper detects an encrypted image and checks/converts with --object secret + --image-opts, file-based pools are decrypted during a qcow2 convert instead of being rsync'd, and a missing passphrase gives an explicit error. The older Copilot threads were already addressed in the branch and I've replied on each. CI on the previous SHA was fully green and Trillian passed on 22 Jun; would appreciate approve-and-run on the new SHA and a review whenever someone has time.

@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.

…ments-combined

Signed-off-by: James Peru <jmsperu@gmail.com>

# Conflicts:
#	plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
#	plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java
@jmsperu

jmsperu commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current 4.22 (merge 7f2c57ab1a) — this is mergeable again.

Two files conflicted, both from the recent command-injection hardening on 4.22:

  • LibvirtRestoreBackupCommandWrapper — 4.22 reordered the imports and dropped the shell-string constants (MOUNT_COMMAND, UMOUNT_COMMAND, ATTACH_*, CURRRENT_DEVICE, RSYNC_COMMAND) in favour of argv-array Script.executeCommand(...). I took 4.22's structure wholesale and re-applied the LUKS work on top, so the non-encrypted path is now upstream's hardened rsync argv form and only the encrypted path builds a qemu-img convert. The one constant this PR still needs, LUKS_SECRET_ID, is kept.
  • LibvirtRestoreBackupCommandWrapperTest — import-ordering only.

No behaviour from either side was dropped: the hardening applies to the plain path, the decrypt-on-restore applies to the encrypted path, and they do not overlap.

Verified locally on JDK17:

  • LibvirtRestoreBackupCommandWrapperTest + LibvirtTakeBackupCommandWrapperTest18/18 pass
  • NASBackupProviderTest15/15 pass
  • bash -n nasbackup.sh clean

Ready for @blueorangutan package / test whenever a committer can approve-and-run.

@blueorangutan

Copy link
Copy Markdown

@jmsperu a [SL] Jenkins job has been kicked to build packages. It will be bundled with /test` whenever a committer can approve-and-run. SystemVM template(s). I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19011

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

8 participants