Linstor: fix create volume from snapshot on primary storage (#13043)

* Linstor: fix create volume from snapshot on primary storage

When creating a volume from a snapshot on Linstor primary storage
(with lin.backup.snapshots=false), the operation fails with:
"Only the following image types are currently supported: VHD, OVA,
QCOW2, RAW (for PowerFlex and FiberChannel)"

Root cause: the Linstor driver does not handle SNAPSHOT -> VOLUME in
its canCopy()/copyAsync() methods. This causes DataMotionServiceImpl
to fall through to StorageSystemDataMotionStrategy (selected because
Linstor advertises STORAGE_SYSTEM_SNAPSHOT=true). That strategy's
verifyFormatWithPoolType() rejects RAW format for Linstor pools,
since RAW is only allowed for PowerFlex and FiberChannel.

Additionally, VolumeOrchestrator.createVolumeFromSnapshot() attempts
to back up the snapshot to secondary storage when the storage plugin
does not advertise CAN_CREATE_TEMPLATE_FROM_SNAPSHOT. This backup
fails because the snapshot only exists on Linstor primary storage.

Fix:
- Add CAN_CREATE_TEMPLATE_FROM_SNAPSHOT capability so the
  orchestrator skips the backup-to-secondary path
- Add canCopySnapshotToVolumeCond() to match SNAPSHOT -> VOLUME
  when both are on the same Linstor primary store
- Wire it into canCopy() to intercept at DataMotionServiceImpl
  before strategy selection, bypassing StorageSystemDataMotionStrategy
- Implement copySnapshotToVolume() which delegates to the existing
  createResourceFromSnapshot() for native Linstor snapshot restore

This follows the same pattern used by the StorPool plugin, which
handles SNAPSHOT -> VOLUME directly in its driver rather than going
through StorageSystemDataMotionStrategy.

Tested on CloudStack 4.22 with Linstor LVM_THIN storage, creating
a volume from a 1TB CNPG Postgres database snapshot. Volume creates
successfully with correct path and deletes cleanly.

* Let CloudRuntimeException propagate from copySnapshotToVolume

Remove try/catch in copySnapshotToVolume so that CloudRuntimeException
from createResourceFromSnapshot propagates to the caller, ensuring
CloudStack properly notices and reports the failure.

* Fix CAN_CREATE_TEMPLATE_FROM_SNAPSHOT breaking template creation

Setting CAN_CREATE_TEMPLATE_FROM_SNAPSHOT unconditionally to true
caused createTemplate from snapshot to take the StorPool-specific
code path in TemplateManagerImpl, which sends a CopyCommand to a
system VM that Linstor cannot handle.

Fix: make CAN_CREATE_TEMPLATE_FROM_SNAPSHOT conditional on the same
flag as STORAGE_SYSTEM_SNAPSHOT (!BackupSnapshots). When snapshots
are backed up to secondary (the default), the old template creation
flow works. When snapshots stay on primary, the direct path is used.

Also fix checkstyle: remove unused DataObject import in test.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergiy Kukunin 2026-04-28 22:53:08 -07:00 committed by GitHub
parent e2c13da419
commit 089eb36e47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 137 additions and 2 deletions

View File

@ -153,6 +153,8 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
// CAN_CREATE_VOLUME_FROM_SNAPSHOT see note from CAN_CREATE_VOLUME_FROM_VOLUME
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_REVERT_VOLUME_TO_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString(),
Boolean.toString(system_snapshot));
return mapCapabilities;
}
@ -720,6 +722,13 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
}
}
private static boolean canCopySnapshotToVolumeCond(DataObject srcData, DataObject dstData) {
return srcData.getType() == DataObjectType.SNAPSHOT && dstData.getType() == DataObjectType.VOLUME
&& srcData.getDataStore().getRole() == DataStoreRole.Primary
&& dstData.getDataStore().getRole() == DataStoreRole.Primary
&& srcData.getDataStore().getId() == dstData.getDataStore().getId();
}
private static boolean canCopySnapshotCond(DataObject srcData, DataObject dstData) {
return srcData.getType() == DataObjectType.SNAPSHOT && dstData.getType() == DataObjectType.SNAPSHOT
&& (dstData.getDataStore().getRole() == DataStoreRole.Image
@ -747,7 +756,10 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
{
logger.debug("LinstorPrimaryDataStoreDriverImpl.canCopy: " + srcData.getType() + " -> " + dstData.getType());
if (canCopySnapshotCond(srcData, dstData)) {
if (canCopySnapshotToVolumeCond(srcData, dstData)) {
StoragePoolVO storagePool = _storagePoolDao.findById(srcData.getDataStore().getId());
return storagePool.getStorageProviderName().equals(LinstorUtil.PROVIDER_NAME);
} else if (canCopySnapshotCond(srcData, dstData)) {
SnapshotInfo sinfo = (SnapshotInfo) srcData;
VolumeInfo volume = sinfo.getBaseVolume();
StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId());
@ -766,6 +778,18 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
return false;
}
private CopyCommandResult copySnapshotToVolume(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo) {
StoragePoolVO storagePoolVO = _storagePoolDao.findById(snapshotInfo.getDataStore().getId());
String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid();
createResourceFromSnapshot(snapshotInfo.getId(), rscName, storagePoolVO);
VolumeObjectTO volumeTO = (VolumeObjectTO) volumeInfo.getTO();
volumeTO.setPath(volumeInfo.getUuid());
volumeTO.setSize(volumeInfo.getSize());
return new CopyCommandResult(null, new CopyCmdAnswer(volumeTO));
}
@Override
public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCallback<CopyCommandResult> callback)
{
@ -773,7 +797,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver
+ srcData.getType() + " -> " + dstData.getType());
final CopyCommandResult res;
if (canCopySnapshotCond(srcData, dstData)) {
if (canCopySnapshotToVolumeCond(srcData, dstData)) {
res = copySnapshotToVolume((SnapshotInfo) srcData, (VolumeInfo) dstData);
} else if (canCopySnapshotCond(srcData, dstData)) {
String errMsg = null;
Answer answer = copySnapshot(srcData, dstData);
if (answer != null && !answer.getResult()) {

View File

@ -25,13 +25,23 @@ import com.linbit.linstor.api.model.ResourceGroup;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.storage.DataStoreRole;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.mock;
@ -42,6 +52,9 @@ public class LinstorPrimaryDataStoreDriverImplTest {
private DevelopersApi api;
@Mock
private PrimaryDataStoreDao _storagePoolDao;
@InjectMocks
private LinstorPrimaryDataStoreDriverImpl linstorPrimaryDataStoreDriver;
@ -85,4 +98,100 @@ public class LinstorPrimaryDataStoreDriverImplTest {
layers = LinstorUtil.getEncryptedLayerList(api, "EncryptedGrp");
Assert.assertEquals(Arrays.asList(LayerType.DRBD, LayerType.LUKS, LayerType.STORAGE), layers);
}
@Test
public void testGetCapabilitiesIncludesCreateTemplateFromSnapshotMatchesSystemSnapshot() {
Map<String, String> caps = linstorPrimaryDataStoreDriver.getCapabilities();
Assert.assertEquals(
"CAN_CREATE_TEMPLATE_FROM_SNAPSHOT should match STORAGE_SYSTEM_SNAPSHOT",
caps.get(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString()),
caps.get(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString()));
}
@Test
public void testCanCopySnapshotToVolumeOnSamePrimary() {
DataStore primaryStore = mock(DataStore.class);
when(primaryStore.getRole()).thenReturn(DataStoreRole.Primary);
when(primaryStore.getId()).thenReturn(1L);
SnapshotInfo snapshot = mock(SnapshotInfo.class);
when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT);
when(snapshot.getDataStore()).thenReturn(primaryStore);
VolumeInfo volume = mock(VolumeInfo.class);
when(volume.getType()).thenReturn(DataObjectType.VOLUME);
when(volume.getDataStore()).thenReturn(primaryStore);
StoragePoolVO pool = mock(StoragePoolVO.class);
when(pool.getStorageProviderName()).thenReturn(LinstorUtil.PROVIDER_NAME);
when(_storagePoolDao.findById(1L)).thenReturn(pool);
Assert.assertTrue("canCopy should return true for SNAPSHOT -> VOLUME on same Linstor primary",
linstorPrimaryDataStoreDriver.canCopy(snapshot, volume));
}
@Test
public void testCanCopySnapshotToVolumeRejectsNonLinstor() {
DataStore primaryStore = mock(DataStore.class);
when(primaryStore.getRole()).thenReturn(DataStoreRole.Primary);
when(primaryStore.getId()).thenReturn(1L);
SnapshotInfo snapshot = mock(SnapshotInfo.class);
when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT);
when(snapshot.getDataStore()).thenReturn(primaryStore);
VolumeInfo volume = mock(VolumeInfo.class);
when(volume.getType()).thenReturn(DataObjectType.VOLUME);
when(volume.getDataStore()).thenReturn(primaryStore);
StoragePoolVO pool = mock(StoragePoolVO.class);
when(pool.getStorageProviderName()).thenReturn("SomeOtherProvider");
when(_storagePoolDao.findById(1L)).thenReturn(pool);
Assert.assertFalse("canCopy should return false for non-Linstor storage",
linstorPrimaryDataStoreDriver.canCopy(snapshot, volume));
}
@Test
public void testCanCopySnapshotToVolumeRejectsCrossPrimary() {
DataStore srcStore = mock(DataStore.class);
when(srcStore.getRole()).thenReturn(DataStoreRole.Primary);
when(srcStore.getId()).thenReturn(1L);
DataStore destStore = mock(DataStore.class);
when(destStore.getRole()).thenReturn(DataStoreRole.Primary);
when(destStore.getId()).thenReturn(2L);
SnapshotInfo snapshot = mock(SnapshotInfo.class);
when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT);
when(snapshot.getDataStore()).thenReturn(srcStore);
VolumeInfo volume = mock(VolumeInfo.class);
when(volume.getType()).thenReturn(DataObjectType.VOLUME);
when(volume.getDataStore()).thenReturn(destStore);
Assert.assertFalse("canCopy should return false for SNAPSHOT -> VOLUME across different primary stores",
linstorPrimaryDataStoreDriver.canCopy(snapshot, volume));
}
@Test
public void testCanCopySnapshotToVolumeRejectsImageDest() {
DataStore primaryStore = mock(DataStore.class);
when(primaryStore.getRole()).thenReturn(DataStoreRole.Primary);
DataStore imageStore = mock(DataStore.class);
when(imageStore.getRole()).thenReturn(DataStoreRole.Image);
SnapshotInfo snapshot = mock(SnapshotInfo.class);
when(snapshot.getType()).thenReturn(DataObjectType.SNAPSHOT);
when(snapshot.getDataStore()).thenReturn(primaryStore);
VolumeInfo volume = mock(VolumeInfo.class);
when(volume.getType()).thenReturn(DataObjectType.VOLUME);
when(volume.getDataStore()).thenReturn(imageStore);
Assert.assertFalse("canCopy should return false when destination is Image store",
linstorPrimaryDataStoreDriver.canCopy(snapshot, volume));
}
}