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

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


The following commit(s) were added to refs/heads/4.22 by this push:
     new b196e97cc36 Prevent deletion of account and domain if either of them 
has deleted protected instance (#12901)
b196e97cc36 is described below

commit b196e97cc36ca3e58dff188eac1ad69eb0d3af95
Author: Manoj Kumar <[email protected]>
AuthorDate: Fri Apr 10 19:21:22 2026 +0530

    Prevent deletion of account and domain if either of them has deleted 
protected instance (#12901)
---
 .../main/java/com/cloud/vm/dao/VMInstanceDao.java  |  5 ++++
 .../java/com/cloud/vm/dao/VMInstanceDaoImpl.java   | 35 ++++++++++++++++++++++
 .../java/com/cloud/user/AccountManagerImpl.java    | 18 +++++++++++
 .../java/com/cloud/user/DomainManagerImpl.java     | 20 +++++++++++++
 .../java/com/cloud/user/DomainManagerImplTest.java |  7 ++++-
 5 files changed, 84 insertions(+), 1 deletion(-)

diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java
index 23541c2431e..4fd3e729e0d 100755
--- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java
@@ -21,6 +21,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import com.cloud.hypervisor.Hypervisor;
 import com.cloud.utils.Pair;
@@ -192,4 +193,8 @@ public interface VMInstanceDao extends 
GenericDao<VMInstanceVO, Long>, StateDao<
     int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List<Long> 
domainIds);
 
     List<VMInstanceVO> listByIdsIncludingRemoved(List<Long> ids);
+
+    List<VMInstanceVO> listDeleteProtectedVmsByAccountId(long accountId);
+
+    List<VMInstanceVO> listDeleteProtectedVmsByDomainIds(Set<Long> domainIds);
 }
diff --git 
a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java 
b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java
index 7ebf61366fe..a38b6af3aa0 100755
--- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java
@@ -25,11 +25,13 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
 
+import org.apache.cloudstack.api.ApiConstants;
 import org.apache.commons.collections.CollectionUtils;
 import org.springframework.stereotype.Component;
 
@@ -106,6 +108,8 @@ public class VMInstanceDaoImpl extends 
GenericDaoBase<VMInstanceVO, Long> implem
     protected SearchBuilder<VMInstanceVO> IdsPowerStateSelectSearch;
     GenericSearchBuilder<VMInstanceVO, Integer> CountByOfferingId;
     GenericSearchBuilder<VMInstanceVO, Integer> CountUserVmNotInDomain;
+    SearchBuilder<VMInstanceVO> DeleteProtectedVmSearchByAccount;
+    SearchBuilder<VMInstanceVO> DeleteProtectedVmSearchByDomainIds;
 
     @Inject
     ResourceTagDao tagsDao;
@@ -368,6 +372,19 @@ public class VMInstanceDaoImpl extends 
GenericDaoBase<VMInstanceVO, Long> implem
         CountUserVmNotInDomain.and("domainIdsNotIn", 
CountUserVmNotInDomain.entity().getDomainId(), Op.NIN);
         CountUserVmNotInDomain.done();
 
+        DeleteProtectedVmSearchByAccount = createSearchBuilder();
+        
DeleteProtectedVmSearchByAccount.selectFields(DeleteProtectedVmSearchByAccount.entity().getUuid());
+        DeleteProtectedVmSearchByAccount.and(ApiConstants.ACCOUNT_ID, 
DeleteProtectedVmSearchByAccount.entity().getAccountId(), Op.EQ);
+        DeleteProtectedVmSearchByAccount.and(ApiConstants.DELETE_PROTECTION, 
DeleteProtectedVmSearchByAccount.entity().isDeleteProtection(), Op.EQ);
+        DeleteProtectedVmSearchByAccount.and(ApiConstants.REMOVED, 
DeleteProtectedVmSearchByAccount.entity().getRemoved(), Op.NULL);
+        DeleteProtectedVmSearchByAccount.done();
+
+        DeleteProtectedVmSearchByDomainIds = createSearchBuilder();
+        
DeleteProtectedVmSearchByDomainIds.selectFields(DeleteProtectedVmSearchByDomainIds.entity().getUuid());
+        DeleteProtectedVmSearchByDomainIds.and(ApiConstants.DOMAIN_IDS, 
DeleteProtectedVmSearchByDomainIds.entity().getDomainId(), Op.IN);
+        DeleteProtectedVmSearchByDomainIds.and(ApiConstants.DELETE_PROTECTION, 
DeleteProtectedVmSearchByDomainIds.entity().isDeleteProtection(), Op.EQ);
+        DeleteProtectedVmSearchByDomainIds.and(ApiConstants.REMOVED, 
DeleteProtectedVmSearchByDomainIds.entity().getRemoved(), Op.NULL);
+        DeleteProtectedVmSearchByDomainIds.done();
     }
 
     @Override
@@ -1296,4 +1313,22 @@ public class VMInstanceDaoImpl extends 
GenericDaoBase<VMInstanceVO, Long> implem
         sc.setParameters("ids", ids.toArray());
         return listIncludingRemovedBy(sc);
     }
+
+    @Override
+    public List<VMInstanceVO> listDeleteProtectedVmsByAccountId(long 
accountId)  {
+        SearchCriteria<VMInstanceVO> sc = 
DeleteProtectedVmSearchByAccount.create();
+        sc.setParameters(ApiConstants.ACCOUNT_ID, accountId);
+        sc.setParameters(ApiConstants.DELETE_PROTECTION, true);
+        Filter filter = new Filter(VMInstanceVO.class, null, false, 0L, 10L);
+        return listBy(sc, filter);
+    }
+
+    @Override
+    public List<VMInstanceVO> listDeleteProtectedVmsByDomainIds(Set<Long> 
domainIds)  {
+        SearchCriteria<VMInstanceVO> sc = 
DeleteProtectedVmSearchByDomainIds.create();
+        sc.setParameters(ApiConstants.DOMAIN_IDS, domainIds.toArray());
+        sc.setParameters(ApiConstants.DELETE_PROTECTION, true);
+        Filter filter = new Filter(VMInstanceVO.class, null, false, 0L, 10L);
+        return listBy(sc, filter);
+    }
 }
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java 
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index bfe6a1b0a47..e01137cffbc 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -2099,6 +2099,7 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
 
         checkIfAccountManagesProjects(accountId);
         verifyCallerPrivilegeForUserOrAccountOperations(account);
+        validateNoDeleteProtectedVmsForAccount(account);
 
         CallContext.current().putContextParameter(Account.class, 
account.getUuid());
 
@@ -2138,6 +2139,23 @@ public class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
         return true;
     }
 
+    private void validateNoDeleteProtectedVmsForAccount(Account account) {
+        long accountId = account.getId();
+        List<VMInstanceVO> deleteProtectedVms = 
_vmDao.listDeleteProtectedVmsByAccountId(accountId);
+        if (CollectionUtils.isEmpty(deleteProtectedVms)) {
+            return;
+        }
+
+        if (logger.isDebugEnabled()) {
+            List<String> vmUuids = 
deleteProtectedVms.stream().map(VMInstanceVO::getUuid).collect(Collectors.toList());
+            logger.debug("Cannot delete Account {}, delete protection enabled 
for Instances: {}", account, vmUuids);
+        }
+
+        throw new InvalidParameterValueException(
+                String.format("Cannot delete Account '%s'. One or more 
Instances have delete protection enabled.",
+                        account.getAccountName()));
+    }
+
     @Override
     @ActionEvent(eventType = EventTypes.EVENT_ACCOUNT_ENABLE, eventDescription 
= "enabling account", async = true)
     public AccountVO enableAccount(String accountName, Long domainId, Long 
accountId) {
diff --git a/server/src/main/java/com/cloud/user/DomainManagerImpl.java 
b/server/src/main/java/com/cloud/user/DomainManagerImpl.java
index 28f9bd3ab39..a590c5ad833 100644
--- a/server/src/main/java/com/cloud/user/DomainManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/DomainManagerImpl.java
@@ -25,6 +25,7 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
@@ -104,6 +105,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.NetUtils;
 import com.cloud.vm.ReservationContext;
 import com.cloud.vm.ReservationContextImpl;
+import com.cloud.vm.VMInstanceVO;
 import com.cloud.vm.dao.VMInstanceDao;
 
 import org.apache.commons.lang3.StringUtils;
@@ -364,6 +366,8 @@ public class DomainManagerImpl extends ManagerBase 
implements DomainManager, Dom
         }
 
         _accountMgr.checkAccess(caller, domain);
+        // Check across the domain hierarchy (current + children) for any 
delete-protected instances
+        validateNoDeleteProtectedVmsForDomain(domain);
 
         return deleteDomain(domain, cleanup);
     }
@@ -724,6 +728,22 @@ public class DomainManagerImpl extends ManagerBase 
implements DomainManager, Dom
         return success && deleteDomainSuccess;
     }
 
+    private void validateNoDeleteProtectedVmsForDomain(Domain parentDomain) {
+        Set<Long> allDomainIds = getDomainChildrenIds(parentDomain.getPath());
+        List<VMInstanceVO> deleteProtectedVms = 
vmInstanceDao.listDeleteProtectedVmsByDomainIds(allDomainIds);
+        if (CollectionUtils.isEmpty(deleteProtectedVms)) {
+            return;
+        }
+        if (logger.isDebugEnabled()) {
+            List<String> vmUuids = 
deleteProtectedVms.stream().map(VMInstanceVO::getUuid).collect(Collectors.toList());
+            logger.debug("Cannot delete Domain {}, it has delete protection 
enabled for Instances: {}", parentDomain, vmUuids);
+        }
+
+        throw new InvalidParameterValueException(
+                String.format("Cannot delete Domain '%s'. One or more 
Instances have delete protection enabled.",
+                        parentDomain.getName()));
+    }
+
     @Override
     public Pair<List<? extends Domain>, Integer> 
searchForDomains(ListDomainsCmd cmd) {
         Account caller = getCaller();
diff --git a/server/src/test/java/com/cloud/user/DomainManagerImplTest.java 
b/server/src/test/java/com/cloud/user/DomainManagerImplTest.java
index 5a1dba215ae..06a8eba903b 100644
--- a/server/src/test/java/com/cloud/user/DomainManagerImplTest.java
+++ b/server/src/test/java/com/cloud/user/DomainManagerImplTest.java
@@ -68,6 +68,7 @@ import com.cloud.utils.db.GlobalLock;
 import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.NetUtils;
+import com.cloud.vm.dao.VMInstanceDao;
 
 
 @RunWith(MockitoJUnitRunner.class)
@@ -123,6 +124,8 @@ public class DomainManagerImplTest {
     Account adminAccount;
     @Mock
     GlobalLock lock;
+    @Mock
+    VMInstanceDao vmInstanceDao;
 
     List<AccountVO> domainAccountsForCleanup;
     List<Long> domainNetworkIds;
@@ -213,6 +216,7 @@ public class DomainManagerImplTest {
     @Test
     public void testDeleteDomainNoCleanup() {
         
Mockito.when(_configMgr.releaseDomainSpecificVirtualRanges(Mockito.any())).thenReturn(true);
+        
Mockito.doReturn(Collections.emptySet()).when(domainManager).getDomainChildrenIds(Mockito.any());
         domainManager.deleteDomain(DOMAIN_ID, testDomainCleanup);
         Mockito.verify(domainManager).deleteDomain(domain, testDomainCleanup);
         
Mockito.verify(domainManager).removeDomainWithNoAccountsForCleanupNetworksOrDedicatedResources(domain);
@@ -278,6 +282,7 @@ public class DomainManagerImplTest {
         
Mockito.when(_dedicatedDao.listByDomainId(Mockito.anyLong())).thenReturn(new 
ArrayList<DedicatedResourceVO>());
         Mockito.when(domainDaoMock.remove(Mockito.anyLong())).thenReturn(true);
         
Mockito.when(_configMgr.releaseDomainSpecificVirtualRanges(Mockito.any())).thenReturn(true);
+        
Mockito.doReturn(Collections.emptySet()).when(domainManager).getDomainChildrenIds(Mockito.any());
 
         try {
             Assert.assertTrue(domainManager.deleteDomain(20l, false));
@@ -309,7 +314,7 @@ public class DomainManagerImplTest {
         Mockito.when(_resourceCountDao.removeEntriesByOwner(Mockito.anyLong(), 
Mockito.eq(ResourceOwnerType.Domain))).thenReturn(1l);
         Mockito.when(_resourceLimitDao.removeEntriesByOwner(Mockito.anyLong(), 
Mockito.eq(ResourceOwnerType.Domain))).thenReturn(1l);
         
Mockito.when(_configMgr.releaseDomainSpecificVirtualRanges(Mockito.any())).thenReturn(true);
-
+        
Mockito.when(vmInstanceDao.listDeleteProtectedVmsByDomainIds(Mockito.any())).thenReturn(Collections.emptyList());
         try {
             Assert.assertTrue(domainManager.deleteDomain(20l, true));
         } finally {

Reply via email to