Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,8 @@ List<VMInstanceVO> searchRemovedByRemoveDate(final Date startDate, final Date en
int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List<Long> domainIds);

List<VMInstanceVO> listByIdsIncludingRemoved(List<Long> ids);

List<VMInstanceVO> listDeleteProtectedVmsByAccountId(long accountId);

List<VMInstanceVO> listDeleteProtectedVmsByDomainIds(List<Long> domainIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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;

Expand Down Expand Up @@ -106,6 +107,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;
Expand Down Expand Up @@ -368,6 +371,19 @@ protected void init() {
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
Expand Down Expand Up @@ -1296,4 +1312,20 @@ public List<VMInstanceVO> listByIdsIncludingRemoved(List<Long> ids) {
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);
return listBy(sc);
}

@Override
public List<VMInstanceVO> listDeleteProtectedVmsByDomainIds(List<Long> domainIds) {
SearchCriteria<VMInstanceVO> sc = DeleteProtectedVmSearchByDomainIds.create();
sc.setParameters(ApiConstants.DOMAIN_IDS, domainIds.toArray());
sc.setParameters(ApiConstants.DELETE_PROTECTION, true);
return listBy(sc);
Comment on lines +1324 to +1329
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Op.IN parameters in this codebase are typically set using an array (e.g., ids.toArray()). Passing a List directly (sc.setParameters(ApiConstants.DOMAIN_IDS, domainIds)) may not bind correctly and can break the query at runtime. Convert domainIds to an array when setting the parameter (and consider handling empty lists defensively).

Copilot uses AI. Check for mistakes.
}
}
18 changes: 18 additions & 0 deletions server/src/main/java/com/cloud/user/AccountManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,7 @@ public boolean deleteUserAccount(long accountId) {
return true;
}

validateNoDeleteProtectedVmsForAccount(account);
checkIfAccountManagesProjects(accountId);
verifyCallerPrivilegeForUserOrAccountOperations(account);

Comment on lines +2100 to 2103
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior (blocking account deletion when delete-protected VMs exist) should be covered by unit tests. There are existing tests for deleteUserAccount in server/src/test/java/com/cloud/user/AccountManagerImplTest.java; please add a case asserting an InvalidParameterValueException is thrown when _vmDao.listDeleteProtectedVmsByAccountId returns a non-empty list.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -2138,6 +2139,23 @@ protected boolean isDeleteNeeded(AccountVO account, long accountId, Account call
return true;
}

private void validateNoDeleteProtectedVmsForAccount(Account account) {
long accountId = account.getId();
List<VMInstanceVO> deleteProtectedVms = _vmDao.listDeleteProtectedVmsByAccountId(accountId);
if (deleteProtectedVms.isEmpty()) {
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_vmDao.listDeleteProtectedVmsByAccountId is invoked here but the return value is assumed to be non-null; with Mockito mocks (e.g., AccountManagerImplTest.deleteUserAccount*) the default return is null, which will cause a NullPointerException on .isEmpty(). Consider making this check null-safe (e.g., CollectionUtils.isEmpty(...)) and/or updating the existing unit tests to stub listDeleteProtectedVmsByAccountId to return an empty list by default.

Suggested change
if (deleteProtectedVms.isEmpty()) {
if (CollectionUtils.isEmpty(deleteProtectedVms)) {

Copilot uses AI. Check for mistakes.
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);
Comment on lines +2150 to +2151
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation currently fetches all delete-protected VMs for the account and then builds a UUID list for debug logging. To avoid potentially large queries/allocations, consider changing the DAO call to an existence check or applying a Filter limit (e.g., 1 row) and only logging a bounded number of UUIDs.

Suggested change
List<String> vmUuids = deleteProtectedVms.stream().map(VMInstanceVO::getUuid).collect(Collectors.toList());
logger.debug("Cannot delete Account {}, delete protection enabled for Instances: {}", account, vmUuids);
final int maxLoggedVms = 10;
List<String> vmUuids = deleteProtectedVms.stream()
.limit(maxLoggedVms)
.map(VMInstanceVO::getUuid)
.collect(Collectors.toList());
boolean hasMore = deleteProtectedVms.size() > maxLoggedVms;
if (hasMore) {
logger.debug("Cannot delete Account {}, delete protection enabled for at least {} Instances (showing first {}): {}",
account, deleteProtectedVms.size(), vmUuids.size(), vmUuids);
} else {
logger.debug("Cannot delete Account {}, delete protection enabled for Instances: {}", account, vmUuids);
}

Copilot uses AI. Check for mistakes.
}

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) {
Expand Down
30 changes: 27 additions & 3 deletions server/src/main/java/com/cloud/user/DomainManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.inject.Inject;

Expand Down Expand Up @@ -104,6 +105,7 @@
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;
Expand Down Expand Up @@ -622,9 +624,6 @@ protected boolean cleanupDomain(Long domainId, Long ownerId) throws ConcurrentOp
DomainVO domainHandle = _domainDao.findById(domainId);
logger.debug("Cleaning up domain {}", domainHandle);
{
domainHandle.setState(Domain.State.Inactive);
_domainDao.update(domainId, domainHandle);

SearchCriteria<DomainVO> sc = _domainDao.createSearchCriteria();
sc.addAnd("parent", SearchCriteria.Op.EQ, domainId);
List<DomainVO> domains = _domainDao.search(sc, null);
Expand All @@ -633,6 +632,12 @@ protected boolean cleanupDomain(Long domainId, Long ownerId) throws ConcurrentOp
sc1.addAnd("path", SearchCriteria.Op.LIKE, "%" + "replace(" + domainHandle.getPath() + ", '%', '[%]')" + "%");
List<DomainVO> domainsToBeInactivated = _domainDao.search(sc1, null);

// Validate that no Instance in this domain or its subdomains has delete protection
validateNoDeleteProtectedVmsForDomain(domainHandle, domainsToBeInactivated);

Comment on lines +635 to +637
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior (blocking domain cleanup/deletion when delete-protected VMs exist) should be covered by a unit test. DomainManagerImplTest currently only stubs the DAO to return an empty list; please add a test asserting deleteDomain(..., true) throws InvalidParameterValueException when vmInstanceDao.listDeleteProtectedVmsByDomainIds(...) returns a non-empty list.

Copilot uses AI. Check for mistakes.
domainHandle.setState(Domain.State.Inactive);
_domainDao.update(domainId, domainHandle);

Comment on lines +635 to +640
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says the cleanup should "skip Instances having deleteProtection enabled", but this change throws an exception and prevents domain deletion entirely when any delete-protected VM exists. Please clarify/update the PR description (or adjust behavior) so intent and implementation match.

Copilot uses AI. Check for mistakes.
// update all subdomains to inactive so no accounts/users can be created
for (DomainVO domain : domainsToBeInactivated) {
domain.setState(Domain.State.Inactive);
Expand Down Expand Up @@ -724,6 +729,25 @@ protected boolean cleanupDomain(Long domainId, Long ownerId) throws ConcurrentOp
return success && deleteDomainSuccess;
}

private void validateNoDeleteProtectedVmsForDomain(Domain domainHandle, List<DomainVO> subDomains) {
List<Long> allDomainIds = subDomains.stream().map(Domain::getId).collect(Collectors.toList());
allDomainIds.add(domainHandle.getId());

List<VMInstanceVO> deleteProtectedVms = vmInstanceDao.listDeleteProtectedVmsByDomainIds(allDomainIds);
if (deleteProtectedVms.isEmpty()) {
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: {}", domainHandle, vmUuids);
Comment on lines +742 to +743
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to account deletion, this loads the full list of delete-protected VMs (and potentially many UUIDs) just to decide whether deletion is allowed. Consider optimizing to an existence query / limited fetch and bounding UUID logging to reduce DB load and memory usage for large domains.

Suggested change
List<String> vmUuids = deleteProtectedVms.stream().map(VMInstanceVO::getUuid).collect(Collectors.toList());
logger.debug("Cannot delete Domain {}, it has delete protection enabled for Instances: {}", domainHandle, vmUuids);
final int maxLoggedUuids = 10;
List<String> vmUuids = deleteProtectedVms.stream()
.limit(maxLoggedUuids)
.map(VMInstanceVO::getUuid)
.collect(Collectors.toList());
logger.debug("Cannot delete Domain {}, it has delete protection enabled for {} Instances. "
+ "Example Instance UUIDs (up to {}): {}",
domainHandle, deleteProtectedVms.size(), maxLoggedUuids, vmUuids);

Copilot uses AI. Check for mistakes.
}

throw new InvalidParameterValueException(
String.format("Cannot delete Domain '%s'. One or more Instances have delete protection enabled.",
domainHandle.getName()));
}

@Override
public Pair<List<? extends Domain>, Integer> searchForDomains(ListDomainsCmd cmd) {
Account caller = getCaller();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
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)
Expand Down Expand Up @@ -123,6 +124,8 @@ public class DomainManagerImplTest {
Account adminAccount;
@Mock
GlobalLock lock;
@Mock
VMInstanceDao vmInstanceDao;

List<AccountVO> domainAccountsForCleanup;
List<Long> domainNetworkIds;
Expand Down Expand Up @@ -309,7 +312,7 @@ public void deleteDomainCleanup() {
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 {
Expand Down
Loading