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 {