Skip to content

linstor: incremental snapshots on secondary storage - #13746

Open
rp- wants to merge 1 commit into
apache:4.22from
LINBIT:linstor-4.22-incremental-snapshots
Open

linstor: incremental snapshots on secondary storage#13746
rp- wants to merge 1 commit into
apache:4.22from
LINBIT:linstor-4.22-incremental-snapshots

Conversation

@rp-

@rp- rp- commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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

  • 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

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

Screenshots (if appropriate):

How Has This Been Tested?

Tested on a Linstor cluster.

@rp-
rp- requested review from JoaoJandre and sureshanaparti July 30, 2026 09:03
@rp- rp- self-assigned this Jul 30, 2026
@rp- rp- changed the title linstor: incremental snapshots on secondary storage via qemu-img reba… linstor: incremental snapshots on secondary storage Jul 30, 2026
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 34.25926% with 71 lines in your changes missing coverage. Please review.
✅ Project coverage is 17.70%. Comparing base (0339f31) to head (8643df8).

Files with missing lines Patch % Lines
...e/wrapper/LinstorBackupSnapshotCommandWrapper.java 0.00% 41 Missing ⚠️
...tore/driver/LinstorPrimaryDataStoreDriverImpl.java 57.14% 13 Missing and 5 partials ⚠️
...storage/datastore/db/SnapshotDataStoreDaoImpl.java 42.85% 7 Missing and 1 partial ⚠️
...torage/datastore/ObjectInDataStoreManagerImpl.java 0.00% 2 Missing ⚠️
...om/cloud/storage/snapshot/SnapshotManagerImpl.java 66.66% 1 Missing and 1 partial ⚠️
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     
Flag Coverage Δ
uitests 3.69% <ø> (ø)
unittests 18.78% <34.25%> (+<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.

…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).
@rp-
rp- force-pushed the linstor-4.22-incremental-snapshots branch from 0a4822c to 8643df8 Compare July 30, 2026 09:27
@DaanHoogland
DaanHoogland requested a review from Copilot July 30, 2026 12:39

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

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.

Comment on lines +364 to +369
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;
Comment on lines 1076 to +1094
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;
Comment on lines +222 to +238
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());
}
}
Comment on lines +411 to +419
@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
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.

2 participants