linstor: incremental snapshots on secondary storage - #13746
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 4.22 #13746 +/- ##
=========================================
Coverage 17.69% 17.70%
- Complexity 15833 15854 +21
=========================================
Files 5925 5925
Lines 533534 533627 +93
Branches 65273 65295 +22
=========================================
+ Hits 94421 94462 +41
- Misses 428434 428481 +47
- Partials 10679 10684 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…se diff Linstor volume snapshots were always backed up to NFS secondary storage as full qcow2 copies, so every snapshot consumed full-volume space. The existing KVM incremental snapshot feature (kvm.incremental.snapshot) is limited to file-based primary storage because it relies on libvirt checkpoints backed by qcow2-persistent dirty bitmaps, which do not exist for raw DRBD devices. Add incremental snapshots for Linstor through a content diff instead: the agent creates an empty qcow2 overlay backed by the raw snapshot device (with an explicit virtual size, clipping the trailing DRBD metadata), then safe-rebases it onto the previous snapshot qcow2 on secondary storage. qemu writes only the clusters in which the two backing files differ, yielding a delta qcow2 chained to its parent by a relative backing name (portable across mount points). No bitmaps, no libvirt version gates, no LINSTOR snapshot retained on primary, and it works for running and stopped VMs alike. Because the diff is content-based, any ready previous snapshot is a correct diff base, which makes fallback-to-full trivial. The feature is gated by the existing cluster-scoped kvm.incremental.snapshot setting plus the pool being Linstor; chain length is capped by snapshot.delta.max and the end_of_chain rotation. Encrypted volumes stay full-only (a rebase would need the LUKS secret for delta and backing file). - SnapshotDataStoreDaoImpl.findParent: parent selection for Linstor volumes (non-encrypted) uses the plain search instead of requiring kvm_checkpoint_path, so chains can start; ObjectInDataStoreManagerImpl then links parent_snapshot_id on the image store ref as usual. - SnapshotObject.getCorrectIncrementalParent: fall back to the plain parent instead of throwing when no ref carries a checkpoint path. The TO constructor still early-returns for such parents, keeping parents[] null so the file-based agent paths are unaffected, while getParent() correctly drives convert-before-use and chain-aware deletion. - SnapshotManagerImpl: generalize the three checkpoint-keyed predicates (extract URL flattening, endLastChainIfNeeded, copySnapshotChainToZone) through isIncrementalChainRef(), which also recognizes parent-linked KVM chains; XenServer per-member chain copy semantics are preserved. - Linstor driver: copySnapshot passes fullSnapshot=false and the parent install path when the dest ref has a ready, same-sized parent on the same store; the parent link is cleared whenever a full copy was made (encrypted, parent missing/unready/resized or agent-side fallback) so the DB chain always matches what is on disk. - LinstorBackupSnapshotCommandWrapper: new incremental branch (qemu-img create -F raw -b <snapdev> + safe rebase onto the parent + rebase -u to the relative backing name); falls back to the full convert path if the parent file is missing. The answer TO reports parentSnapshotPath so SnapshotObject.processEvent keeps the parent link only for real deltas. Restore, volume/template-from-snapshot, extract and cross-zone copy all reuse the existing machinery: qemu-img follows the relative backing chain on the same mount, and chain members are flattened via ConvertSnapshotCommand before standalone use. Delete ordering is handled by the generic parent-link logic (children hide their parents, leaf-first physical deletion).
0a4822c to
8643df8
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds support for incremental (content-diff) snapshot backups to secondary storage for Linstor volumes, reusing the existing KVM incremental snapshot configuration semantics.
Changes:
- Introduces content-based incremental chaining for Linstor snapshot backups via parent snapshot install paths.
- Updates snapshot parent selection logic to avoid applying checkpoint-based (cross-role) handling to content-based chains.
- Adds/extends unit tests covering incremental-chain detection, parent-path resolution, and chain bookkeeping adjustments.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java | Centralizes “is part of incremental chain” detection for KVM (checkpoint-based and content-based parent-link chains). |
| server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java | Adds tests for incremental-chain detection behavior across hypervisors and ref states. |
| plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java | Implements incremental-parent resolution on secondary storage and clears chain parent when a full copy is taken. |
| plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImplTest.java | Adds tests for incremental parent path selection and parent-link clearing behavior. |
| plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java | Adds agent-side incremental qcow2 creation using safe rebase onto the parent qcow2 backing file. |
| plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java | Adds a new command option for passing the parent snapshot install path to the agent. |
| engine/storage/src/main/java/org/apache/cloudstack/storage/datastore/ObjectInDataStoreManagerImpl.java | Skips checkpoint-based parent handling for volumes using content-based chains. |
| engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotObject.java | Makes incremental-parent resolution tolerate chains without checkpoints (fallback to plain parent). |
| engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/SnapshotObjectTest.java | New tests for incremental-parent selection logic with/without checkpoint-bearing refs. |
| engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java | Adds usesContentBasedChain(volumeId) API to distinguish chain types. |
| engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java | Implements chain-type detection and adjusts findParent behavior accordingly. |
| engine/schema/src/test/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImplTest.java | Adds tests for usesContentBasedChain behavior across volume/pool conditions. |
| plugins/storage/volume/linstor/CHANGELOG.md | Documents the new incremental snapshot backup support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| boolean contentBasedChain = kvmIncrementalSnapshot && Hypervisor.HypervisorType.KVM.equals(hypervisorType) && usesContentBasedChain(volumeId); | ||
| if (contentBasedChain && (role == null || !role.isImageStore())) { | ||
| logger.trace("Content-based snapshot chains only exist on the image store. Returning null as parent for volume [{}] and role [{}].", volumeId, role); | ||
| return null; | ||
| } | ||
| boolean checkpointBasedChain = kvmIncrementalSnapshot && Hypervisor.HypervisorType.KVM.equals(hypervisorType) && !contentBasedChain; |
| SnapshotObject snapshotObject = (SnapshotObject)srcData; | ||
| Boolean snapshotFullBackup = snapshotObject.getFullBackup(); | ||
| final StoragePoolVO pool = _storagePoolDao.findById(srcData.getDataStore().getId()); | ||
| final DevelopersApi api = getLinstorAPI(pool); | ||
| boolean fullSnapshot = true; | ||
| if (snapshotFullBackup != null) { | ||
| fullSnapshot = snapshotFullBackup; | ||
| } | ||
|
|
||
| // For encrypted volumes Linstor adds a LUKS layer (DRBD -> LUKS -> STORAGE). The storage | ||
| // layer snapshot device (getSnapshotPath) therefore only exposes the raw LUKS ciphertext, | ||
| // while restore writes onto the decrypted DRBD device (/dev/drbd/by-res/.../0). Backing up | ||
| // the ciphertext and writing it back to the decrypted layer corrupts the volume (and the | ||
| // shrink to the net volume size would even truncate the ciphertext). So for encrypted | ||
| // volumes we never read the storage snapshot directly: restore the snapshot into a temporary | ||
| // resource and back up its decrypted DRBD device instead, symmetric to the restore path. | ||
| final boolean encrypted = snapshotObject.getBaseVolume().getPassphraseId() != null; | ||
|
|
||
| SnapshotDataStoreVO destRef = _snapshotStoreDao.findByStoreSnapshot( | ||
| destData.getDataStore().getRole(), destData.getDataStore().getId(), destData.getId()); | ||
| // encrypted volumes are always backed up as full copies: an incremental rebase would need | ||
| // the LUKS secret for both the delta and the backing file | ||
| String parentPath = encrypted ? null : getIncrementalParentPath(destRef); | ||
| boolean fullSnapshot = parentPath == null; |
| final Map<String, String> options = cmd.getOptions(); | ||
| final String parentInstallPath = options != null ? | ||
| options.get(LinstorBackupSnapshotCommand.OPTION_PARENT_PATH) : null; | ||
|
|
||
| boolean incremental = false; | ||
| String dstPath = null; | ||
| if (!encrypted && parentInstallPath != null && src.getVolume() != null) { | ||
| final File parentFile = new File(secondaryPool.getLocalPath() + File.separator + parentInstallPath); | ||
| if (parentFile.isFile()) { | ||
| dstPath = createIncrementalQCow2( | ||
| srcPath, dst, secondaryPool, parentFile, src.getVolume().getSize(), cmd.getWaitInMillSeconds()); | ||
| incremental = true; | ||
| } else { | ||
| LOGGER.warn("Parent snapshot file '{}' missing on secondary storage, taking a full backup instead", | ||
| parentFile.getAbsolutePath()); | ||
| } | ||
| } |
| @Override | ||
| public boolean usesContentBasedChain(long volumeId) { | ||
| VolumeVO volume = volumeDao.findByIdIncludingRemoved(volumeId); | ||
| if (volume == null || volume.getPoolId() == null || volume.getPassphraseId() != null) { | ||
| return false; | ||
| } | ||
| StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); | ||
| return pool != null && Storage.StoragePoolType.Linstor.equals(pool.getPoolType()); | ||
| } |
|
|
||
| ### Added | ||
|
|
||
| - Support for incremental snapshots on secondary storage backuped snapshots |
Description
In short, this PR adds incremental snapshot support for Linstor backuped (secondary storage) volume snapshots.
It uses the same configuration keys as the file bases KVM incremental snapshots settings.
Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
Bug Severity
Screenshots (if appropriate):
How Has This Been Tested?
Tested on a Linstor cluster.