This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 8171d9568c9 Block use of internal and external snapshots on KVM 
(#11039)
8171d9568c9 is described below

commit 8171d9568c941877e0290f77051ed45f31a73544
Author: João Jandre <[email protected]>
AuthorDate: Mon Nov 24 07:39:19 2025 -0300

    Block use of internal and external snapshots on KVM (#11039)
---
 .../apache/cloudstack/backup/BackupProvider.java   |  4 ++
 .../apache/cloudstack/backup/BackupService.java    |  7 ++++
 .../storage/snapshot/DefaultSnapshotStrategy.java  |  6 +++
 .../vmsnapshot/DefaultVMSnapshotStrategy.java      | 48 +++++++++++++++++++---
 .../vmsnapshot/VMSnapshotStrategyKVMTest.java      | 12 ++++++
 .../storage/vmsnapshot/VMSnapshotStrategyTest.java | 23 +++++++++++
 .../cloudstack/backup/NASBackupProvider.java       | 12 ++++++
 .../cloudstack/backup/NASBackupProviderTest.java   |  4 ++
 8 files changed, 110 insertions(+), 6 deletions(-)

diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java 
b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java
index 32a714370df..23b8092425d 100644
--- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java
+++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java
@@ -124,6 +124,10 @@ public interface BackupProvider {
      */
     boolean supportsInstanceFromBackup();
 
+    default boolean supportsMemoryVmSnapshot() {
+        return true;
+    }
+
     /**
      * Returns the backup storage usage (Used, Total) for a backup provider
      * @param zoneId the zone for which to return metrics
diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupService.java 
b/api/src/main/java/org/apache/cloudstack/backup/BackupService.java
index d4beb629fe0..3ba2978c0fa 100644
--- a/api/src/main/java/org/apache/cloudstack/backup/BackupService.java
+++ b/api/src/main/java/org/apache/cloudstack/backup/BackupService.java
@@ -34,4 +34,11 @@ public interface BackupService {
      * @return backup provider
      */
     BackupProvider getBackupProvider(final Long zoneId);
+
+    /**
+     * Find backup provider by name
+     * @param name backup provider name
+     * @return backup provider
+     */
+    BackupProvider getBackupProvider(final String name);
 }
diff --git 
a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java
 
b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java
index c1981941ac0..c6d89a87014 100644
--- 
a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java
+++ 
b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java
@@ -672,6 +672,12 @@ public class DefaultSnapshotStrategy extends 
SnapshotStrategyBase {
             }
         }
 
+        if 
(CollectionUtils.isNotEmpty(vmSnapshotDao.findByVmAndByType(volumeVO.getInstanceId(),
 VMSnapshot.Type.DiskAndMemory))) {
+            logger.debug("DefaultSnapshotStrategy cannot handle snapshot [{}] 
for volume [{}] as the volume is attached to a VM with disk-and-memory VM 
snapshots." +
+                    "Restoring the volume snapshot will corrupt any newer 
disk-and-memory VM snapshots.", snapshot);
+            return StrategyPriority.CANT_HANDLE;
+        }
+
         return StrategyPriority.DEFAULT;
     }
 
diff --git 
a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java
 
b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java
index 6e933396871..527b7fbeda1 100644
--- 
a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java
+++ 
b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java
@@ -26,14 +26,21 @@ import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
 import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.Snapshot;
+import com.cloud.storage.dao.SnapshotDao;
 import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
 import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
+import org.apache.cloudstack.backup.BackupManager;
+import org.apache.cloudstack.backup.BackupOfferingVO;
+import org.apache.cloudstack.backup.BackupProvider;
+import org.apache.cloudstack.backup.dao.BackupOfferingDao;
 import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority;
 import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions;
 import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.to.VolumeObjectTO;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 
 import com.cloud.agent.AgentManager;
@@ -104,7 +111,16 @@ public class DefaultVMSnapshotStrategy extends ManagerBase 
implements VMSnapshot
     PrimaryDataStoreDao primaryDataStoreDao;
 
     @Inject
-    VMSnapshotDetailsDao vmSnapshotDetailsDao;
+    private VMSnapshotDetailsDao vmSnapshotDetailsDao;
+
+    @Inject
+    private BackupManager backupManager;
+
+    @Inject
+    private BackupOfferingDao backupOfferingDao;
+
+    @Inject
+    private SnapshotDao snapshotDao;
 
     protected static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = 
"kvmFileBasedStorageSnapshot";
 
@@ -480,24 +496,44 @@ public class DefaultVMSnapshotStrategy extends 
ManagerBase implements VMSnapshot
     @Override
     public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean 
snapshotMemory) {
         UserVmVO vm = userVmDao.findById(vmId);
+        String cantHandleLog = String.format("Default VM snapshot cannot 
handle VM snapshot for [%s]", vm);
         if (State.Running.equals(vm.getState()) && !snapshotMemory) {
-            logger.debug("Default VM snapshot strategy cannot handle VM 
snapshot for [{}] as it is running and its memory will not be affected.", vm);
+            logger.debug("{} as it is running and its memory will not be 
affected.", cantHandleLog, vm);
             return StrategyPriority.CANT_HANDLE;
         }
 
         if (vmHasKvmDiskOnlySnapshot(vm)) {
-            logger.debug("Default VM snapshot strategy cannot handle VM 
snapshot for [{}] as it has a disk-only VM snapshot using 
kvmFileBasedStorageSnapshot strategy." +
-                    "These two strategies are not compatible, as reverting a 
disk-only VM snapshot will erase newer disk-and-memory VM snapshots.", vm);
+            logger.debug("{} as it is not compatible with disk-only VM 
snapshot on KVM. As disk-and-memory snapshots use internal snapshots and 
disk-only VM snapshots use" +
+                    " external snapshots. When restoring external snapshots, 
any newer internal snapshots are lost.", cantHandleLog);
             return StrategyPriority.CANT_HANDLE;
         }
 
         List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
         for (VolumeVO volume : volumes) {
             if (volume.getFormat() != ImageFormat.QCOW2) {
-                logger.debug("Default VM snapshot strategy cannot handle VM 
snapshot for [{}] as it has a volume [{}] that is not in the QCOW2 format.", 
vm, volume);
+                logger.debug("{} as it has a volume [{}] that is not in the 
QCOW2 format.", cantHandleLog, vm, volume);
+                return StrategyPriority.CANT_HANDLE;
+            }
+
+            if 
(CollectionUtils.isNotEmpty(snapshotDao.listByVolumeIdAndTypeNotInAndStateNotRemoved(volume.getId(),
 Snapshot.Type.GROUP))) {
+                logger.debug("{} as it has a volume [{}] with volume 
snapshots. As disk-and-memory snapshots use internal snapshots and volume 
snapshots use external" +
+                        " snapshots. When restoring external snapshots, any 
newer internal snapshots are lost.", cantHandleLog, volume);
                 return StrategyPriority.CANT_HANDLE;
             }
         }
+
+
+        BackupOfferingVO backupOfferingVO = 
backupOfferingDao.findById(vm.getBackupOfferingId());
+        if (backupOfferingVO == null) {
+            return StrategyPriority.DEFAULT;
+        }
+
+        BackupProvider provider = 
backupManager.getBackupProvider(backupOfferingVO.getProvider());
+        if (!provider.supportsMemoryVmSnapshot()) {
+            logger.debug("{} as the VM has a backup offering for a provider 
that is not supported.", cantHandleLog);
+            return StrategyPriority.CANT_HANDLE;
+        }
+
         return StrategyPriority.DEFAULT;
     }
 
@@ -508,7 +544,7 @@ public class DefaultVMSnapshotStrategy extends ManagerBase 
implements VMSnapshot
 
         for (VMSnapshotVO vmSnapshotVO : 
vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) {
             List<VMSnapshotDetailsVO> vmSnapshotDetails = 
vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId());
-            if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> 
vmSnapshotDetailsVO.getName().equals(KVM_FILE_BASED_STORAGE_SNAPSHOT))) {
+            if (vmSnapshotDetails.stream().anyMatch(detailsVO -> 
KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(detailsVO.getName()) || 
STORAGE_SNAPSHOT.equals(detailsVO.getName()))) {
                 return true;
             }
         }
diff --git 
a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java
 
b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java
index 609a1225118..02c17f8f3bd 100644
--- 
a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java
+++ 
b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java
@@ -29,6 +29,8 @@ import java.util.UUID;
 
 import javax.inject.Inject;
 
+import org.apache.cloudstack.backup.BackupManager;
+import org.apache.cloudstack.backup.dao.BackupOfferingDao;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
 import 
org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManager;
@@ -431,5 +433,15 @@ public class VMSnapshotStrategyKVMTest extends TestCase{
         public VMSnapshotDetailsDao vmSnapshotDetailsDao () {
             return Mockito.mock(VMSnapshotDetailsDao.class);
         }
+
+        @Bean
+        public BackupOfferingDao backupOfferingDao() {
+            return Mockito.mock(BackupOfferingDao.class);
+        }
+
+        @Bean
+        public BackupManager backupManager() {
+            return Mockito.mock(BackupManager.class);
+        }
     }
 }
diff --git 
a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java
 
b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java
index 829ca5ade39..a20b52fac2d 100644
--- 
a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java
+++ 
b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java
@@ -25,7 +25,10 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import com.cloud.storage.dao.SnapshotDao;
 import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
+import org.apache.cloudstack.backup.BackupManager;
+import org.apache.cloudstack.backup.dao.BackupOfferingDao;
 import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@@ -318,5 +321,25 @@ public class VMSnapshotStrategyTest extends TestCase {
         public PrimaryDataStoreDao primaryDataStoreDao() {
             return Mockito.mock(PrimaryDataStoreDao.class);
         }
+
+        @Bean
+        public BackupOfferingDao backupOfferingDao() {
+            return Mockito.mock(BackupOfferingDao.class);
+        }
+
+        @Bean
+        public VMSnapshotDetailsDao VMSnapshotDetailsDao() {
+            return Mockito.mock(VMSnapshotDetailsDao.class);
+        }
+
+        @Bean
+        public SnapshotDao snapshotDao() {
+            return Mockito.mock(SnapshotDao.class);
+        }
+
+        @Bean
+        public BackupManager backupManager() {
+            return Mockito.mock(BackupManager.class);
+        }
     }
 }
diff --git 
a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
 
b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
index 3b2c2692b0d..a350d80d27f 100644
--- 
a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
+++ 
b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java
@@ -56,6 +56,7 @@ import org.apache.cloudstack.framework.config.Configurable;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
 import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.LogManager;
 
@@ -195,6 +196,12 @@ public class NASBackupProvider extends AdapterBase 
implements BackupProvider, Co
             throw new CloudRuntimeException("No valid backup repository found 
for the VM, please check the attached backup offering");
         }
 
+        if 
(CollectionUtils.isNotEmpty(vmSnapshotDao.findByVmAndByType(vm.getId(), 
VMSnapshot.Type.DiskAndMemory))) {
+            logger.debug("NAS backup provider cannot take backups of a VM [{}] 
with disk-and-memory VM snapshots. Restoring the backup will corrupt any newer 
disk-and-memory " +
+                    "VM snapshots.", vm);
+            throw new CloudRuntimeException(String.format("Cannot take backup 
of VM [%s] as it has disk-and-memory VM snapshots.", vm.getUuid()));
+        }
+
         final Date creationDate = new Date();
         final String backupPath = String.format("%s/%s", vm.getInstanceName(),
                 new 
SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(creationDate));
@@ -517,6 +524,11 @@ public class NASBackupProvider extends AdapterBase 
implements BackupProvider, Co
         return true;
     }
 
+    @Override
+    public boolean supportsMemoryVmSnapshot() {
+        return false;
+    }
+
     @Override
     public Pair<Long, Long> getBackupStorageStats(Long zoneId) {
         final List<BackupRepository> repositories = 
backupRepositoryDao.listByZoneAndProvider(zoneId, getName());
diff --git 
a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java
 
b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java
index 7540cabbbf5..a512292cd28 100644
--- 
a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java
+++ 
b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 
+import com.cloud.vm.snapshot.dao.VMSnapshotDao;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -92,6 +93,9 @@ public class NASBackupProviderTest {
     @Mock
     private PrimaryDataStoreDao storagePoolDao;
 
+    @Mock
+    private VMSnapshotDao vmSnapshotDaoMock;
+
     @Test
     public void testDeleteBackup() throws OperationTimedoutException, 
AgentUnavailableException {
         Long hostId = 1L;

Reply via email to