Skip to content

Backup: support StorPool volumes in NAS backup provider on KVM - #14203

Open
slavkap wants to merge 1 commit into
apache:mainfrom
storpool:sp-nas-backup
Open

slavkap wants to merge 1 commit into
apache:mainfrom
storpool:sp-nas-backup

Conversation

@slavkap

@slavkap slavkap commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Description

Volume pool/path info is now sent to the agent whenever a VM has a StorPool volume, not just when the VM looks Stopped, since nasbackup.sh re-checks the VM's actual liveness itself right before acting and needs the StorPool path to clone a backup source disk if it finds the VM already stopped.

  • StorPoolStorageAdaptor.createPhysicalDisk() now creates and attaches a StorPool volume (previously a no-op returning null), used when restoring a backup provisions a new volume.
  • nasbackup.sh clones the live StorPool volume into a point-in-time volume before reading from it for a cold backup, and cleans it up afterwards (with an EXIT trap as a safety net).
  • Backup size is now reported via an explicit BACKUP_SIZE_TOTAL= marker on stdout instead of being inferred from output position/shape, which broke down once StorPool added a third code shape; take-backup errors are now always returned as a BackupAnswer rather than letting an uncaught exception surface as a plain Answer.
  • Restore reports back the volume path StorPool actually assigned (BackupAnswer.restoredVolumePath) instead of the path CloudStack guessed, since StorPool controls the device path itself.
  • Backup file identifiers are normalized to the qcow2 basename so StorPool's full device path matches what nasbackup.sh expects.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Feature/Enhancement Scale

  • Major
  • Minor

How Has This Been Tested?

Manual testing and smoke tests with StorPool as a primary storage, KVM hypervisors

Volume pool/path info is now sent to the agent whenever a VM has a
StorPool volume, not just when the VM looks Stopped, since nasbackup.sh
re-checks the VM's actual liveness itself right before acting and needs
the StorPool path to clone a backup source disk if it finds the VM
already stopped.

- StorPoolStorageAdaptor.createPhysicalDisk() now creates and attaches
  a StorPool volume (previously a no-op returning null), used when
  restoring a backup provisions a new volume.
- nasbackup.sh clones the live StorPool volume into a point-in-time
  volume before reading from it for a cold backup, and cleans it up
  afterwards (with an EXIT trap as a safety net).
- Backup size is now reported via an explicit BACKUP_SIZE_TOTAL=<bytes>
  marker on stdout instead of being inferred from output position/shape,
  which broke down once StorPool added a third code shape; take-backup
  errors are now always returned as a BackupAnswer rather than letting
  an uncaught exception surface as a plain Answer.
- Restore reports back the volume path StorPool actually assigned
  (BackupAnswer.restoredVolumePath) instead of the path CloudStack
  guessed, since StorPool controls the device path itself.
- Backup file identifiers are normalized to the qcow2 basename so
  StorPool's full device path matches what nasbackup.sh expects.

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.

🟡 Changes recommended

Restore attachment compatibility and StorPool volume cleanup failures remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds StorPool support to KVM NAS backup and restore workflows.

Changes:

  • Adds StorPool volume provisioning and restored-path reporting.
  • Adds point-in-time cloning for cold backups.
  • Improves backup size reporting, cleanup, and error handling.
  • Expands integration and provider tests.
File summaries
File Summary
test/integration/plugins/storpool/TestNasBackupStorPool.py StorPool NAS backup/restore tests
test/integration/plugins/storpool/sp_util.py StorPool test helpers
scripts/vm/hypervisor/kvm/nasbackup.sh StorPool cloning and cleanup
plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java StorPool volume provisioning
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java Backup output and size parsing
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java StorPool restore handling
plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java Provider restore-path tests
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java Backup and restore path propagation
core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java Restored-volume path response
Review details

Suppressed comments (3)

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java:362

  • If qemu-img convert fails after this StorPool volume has been created and attached, replaceBlockDeviceWithBackup only returns a failed answer; it never detaches or deletes the newly provisioned volume. Because the restore does not persist a CloudStack volume on this path, every failed restore can leave an attached, orphaned StorPool volume. Track the created disk and clean it up on all subsequent failure paths (or add an explicit rollback around the conversion).
                    disk = volumeStoragePool.createPhysicalDisk(volumeUuid, QemuImg.PhysicalDiskFormat.RAW, Storage.ProvisioningType.THIN, size, null);
                    if (disk == null) {
                        throw new CloudRuntimeException(String.format("Failed to provision a %s volume for restore [%s]", poolType, volumeUuid));
                    }
                    if (Storage.StoragePoolType.StorPool.equals(poolType)) {
                        volumePath = disk.getPath();

plugins/storage/volume/storpool/src/main/java/com/cloud/hypervisor/kvm/storage/StorPoolStorageAdaptor.java:490

  • attachOrDetachVolume() throws CloudRuntimeException when the StorPool attach command fails, so the if (!...) branch is not entered in that failure mode. The newly created volume is therefore left behind even though restore reports failure, leaking an unattached StorPool volume on every attach exception; wrap the attach in cleanup logic that deletes globalId for both a false return and an exception.
        if (!attachOrDetachVolume("attach", "volume", volumePath)) {
            volumeDelete(globalId);
            throw new CloudRuntimeException(String.format("Could not attach newly created StorPool volume %s", volumePath));

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

  • Because read_disk=$(sp_create_backup_source_disk ...) executes the function in a command-substitution subshell, the function's append to SP_CLEANUP_VOLUMES is lost when it returns. If qemu-img convert or a later per-disk step fails, the parent EXIT trap therefore has no clone name to release, leaving the attached backup-source StorPool volume orphaned. Return the path through a variable/file without command substitution (or otherwise register the clone in the parent shell) before proceeding.
      if ! read_disk=$(sp_create_backup_source_disk "$disk"); then
        log -ne "Failed to create a StorPool backup source volume for $disk"
        echo "Failed to create a StorPool backup source volume for $disk"
        cleanup
        exit 1
      fi
      sp_clone_name=$(sp_volume_name_from_path "$read_disk")
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +318 to 319
if (List.of(Storage.StoragePoolType.RBD, Storage.StoragePoolType.Linstor, Storage.StoragePoolType.StorPool).contains(volumePool.getPoolType())) {
return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size);
Comment on lines +595 to +602
if [[ -n "$sp_clone_name" ]]; then
sp_delete_backup_source_disk "$sp_clone_name"
local -a remaining_clones=()
local tracked_clone
for tracked_clone in "${SP_CLEANUP_VOLUMES[@]}"; do
[[ "$tracked_clone" == "$sp_clone_name" ]] || remaining_clones+=("$tracked_clone")
done
SP_CLEANUP_VOLUMES=("${remaining_clones[@]}")
@nvazquez nvazquez added this to the 24.0 milestone Sep 18, 2026
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 20.39474% with 121 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.92%. Comparing base (5812290) to head (2c92601).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ource/wrapper/LibvirtTakeBackupCommandWrapper.java 0.00% 44 Missing ⚠️
...hypervisor/kvm/storage/StorPoolStorageAdaptor.java 0.00% 36 Missing ⚠️
...ce/wrapper/LibvirtRestoreBackupCommandWrapper.java 32.35% 19 Missing and 4 partials ⚠️
...rg/apache/cloudstack/backup/NASBackupProvider.java 56.25% 9 Missing and 5 partials ⚠️
...ava/org/apache/cloudstack/backup/BackupAnswer.java 33.33% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #14203      +/-   ##
============================================
+ Coverage     19.90%   19.92%   +0.01%     
- Complexity    20171    20207      +36     
============================================
  Files          6372     6373       +1     
  Lines        577180   577311     +131     
  Branches      70693    70713      +20     
============================================
+ Hits         114869   115004     +135     
+ Misses       449755   449736      -19     
- Partials      12556    12571      +15     
Flag Coverage Δ
uitests 3.71% <ø> (ø)
unittests 21.19% <20.39%> (+0.02%) ⬆️

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.

* pool/path info must be sent to the agent even though the VM currently looks Running — see
* the caller in {@link #takeBackup}.
*/
private boolean hasStorPoolVolume(List<VolumeVO> volumes) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would suggest to pass pooltype as a parameter of the method

@weizhouapache

weizhouapache commented Sep 18, 2026

Copy link
Copy Markdown
Member

this needs testing on NFS, Ceph,Linstor and Storpool to ensure there is no regression

cc @abh1sar @rp- @slavkap
I will run some tests on ceph

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants