Compare commits

...

8 Commits

Author SHA1 Message Date
Abhishek Kumar 67dc24df79
Merge f384872d0f into bce3e54a7e 2026-01-22 15:13:13 +01:00
Daman Arora bce3e54a7e
improve error handling for template upload notifications (#12412)
Co-authored-by: Daman Arora <daman.arora@shapeblue.com>
2026-01-22 15:02:46 +01:00
Nicolas Vazquez 6a9835904c
Fix for zoneids parameters length on updateAPIs (#12440) 2026-01-22 14:57:46 +01:00
Nicolas Vazquez 6846619a6f
Fix update network offering domainids size limitation (#12431) 2026-01-22 14:32:46 +01:00
Vishesh d1eb2822d9
Remove redundant Exceptions from logs for vm schedules (#12428) 2026-01-22 14:29:35 +01:00
Abhishek Kumar f384872d0f fix and tests
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
2026-01-07 14:55:58 +05:30
Abhishek Kumar e80e89279f
Merge branch '4.20' into acc-userdata-unlink 2026-01-07 13:37:14 +05:30
Abhishek Kumar 869ffad6b3
server,engine-schema: add check for account userdata cleanup
Fixes #9477

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
2025-11-18 20:16:43 +05:30
17 changed files with 259 additions and 14 deletions

View File

@ -78,6 +78,7 @@ public class UpdateNetworkOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.STRING,
length = 4096,
description = "The ID of the containing domain(s) as comma separated string, public for public offerings")
private String domainIds;

View File

@ -75,6 +75,7 @@ public class UpdateDiskOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.STRING,
description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings",
length = 4096,
since = "4.13")
private String zoneIds;

View File

@ -69,6 +69,7 @@ public class UpdateServiceOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.STRING,
description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings",
length = 4096,
since = "4.13")
private String zoneIds;

View File

@ -65,6 +65,7 @@ public class UpdateVPCOfferingCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.STRING,
description = "The ID of the containing zone(s) as comma separated string, all for all zones offerings",
length = 4096,
since = "4.13")
private String zoneIds;

View File

@ -99,4 +99,6 @@ public interface VMTemplateDao extends GenericDao<VMTemplateVO, Long>, StateDao<
List<VMTemplateVO> listByIds(List<Long> ids);
List<Long> listIdsByTemplateTag(String tag);
List<Long> listByUserdataIdsNotAccount(List<Long> userdataIds, long accountId);
}

View File

@ -863,4 +863,22 @@ public class VMTemplateDaoImpl extends GenericDaoBase<VMTemplateVO, Long> implem
}
return rows > 0;
}
@Override
public List<Long> listByUserdataIdsNotAccount(List<Long> userdataIds, long accountId) {
if (CollectionUtils.isEmpty(userdataIds)) {
return Collections.emptyList();
}
GenericSearchBuilder<VMTemplateVO, Long> sb = createSearchBuilder(Long.class);
sb.selectFields(sb.entity().getId());
sb.and("userDataId", sb.entity().getUserDataId(), SearchCriteria.Op.EQ);
sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ);
sb.and("accountId", sb.entity().getAccountId(), SearchCriteria.Op.NEQ);
sb.done();
SearchCriteria<Long> sc = sb.create();
sc.setParameters("userDataId", userdataIds.toArray());
sc.setParameters("state", VirtualMachineTemplate.State.Active.toString());
sc.setParameters("accountId", accountId);
return customSearch(sc, null);
}
}

View File

@ -16,6 +16,8 @@
// under the License.
package com.cloud.user.dao;
import java.util.List;
import com.cloud.user.UserDataVO;
import com.cloud.utils.db.GenericDao;
@ -25,6 +27,8 @@ public interface UserDataDao extends GenericDao<UserDataVO, Long> {
public UserDataVO findByName(long accountId, long domainId, String name);
List<Long> listIdsByAccountId(long accountId);
int removeByAccountId(long accountId);
}

View File

@ -16,8 +16,11 @@
// under the License.
package com.cloud.user.dao;
import java.util.List;
import com.cloud.user.UserDataVO;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.springframework.stereotype.Component;
@ -64,6 +67,18 @@ public class UserDataDaoImpl extends GenericDaoBase<UserDataVO, Long> implements
return findOneBy(sc);
}
@Override
public List<Long> listIdsByAccountId(long accountId) {
GenericSearchBuilder<UserDataVO, Long> sb = createSearchBuilder(Long.class);
sb.selectFields(sb.entity().getId());
sb.and("accountId", sb.entity().getAccountId(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<Long> sc = sb.create();
sc.setParameters("accountId", accountId);
return customSearch(sc, null);
}
@Override
public int removeByAccountId(long accountId) {
SearchCriteria<UserDataVO> sc = userdataSearch.create();

View File

@ -31,4 +31,6 @@ public interface VMScheduledJobDao extends GenericDao<VMScheduledJobVO, Long> {
int expungeJobsForSchedules(List<Long> scheduleId, Date dateAfter);
int expungeJobsBefore(Date currentTimestamp);
VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp);
}

View File

@ -39,6 +39,8 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
private final SearchBuilder<VMScheduledJobVO> expungeJobForScheduleSearch;
private final SearchBuilder<VMScheduledJobVO> scheduleAndTimestampSearch;
static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp";
static final String VM_SCHEDULE_ID = "vm_schedule_id";
@ -58,6 +60,11 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN);
expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ);
expungeJobForScheduleSearch.done();
scheduleAndTimestampSearch = createSearchBuilder();
scheduleAndTimestampSearch.and(VM_SCHEDULE_ID, scheduleAndTimestampSearch.entity().getVmScheduleId(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.and(SCHEDULED_TIMESTAMP, scheduleAndTimestampSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.done();
}
/**
@ -92,4 +99,12 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
sc.setParameters(SCHEDULED_TIMESTAMP, date);
return expunge(sc);
}
@Override
public VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) {
SearchCriteria<VMScheduledJobVO> sc = scheduleAndTimestampSearch.create();
sc.setParameters(VM_SCHEDULE_ID, scheduleId);
sc.setParameters(SCHEDULED_TIMESTAMP, scheduledTimestamp);
return findOneBy(sc);
}
}

View File

@ -19,6 +19,7 @@ package com.cloud.storage.dao;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@ -47,6 +48,7 @@ import com.cloud.storage.Storage;
import com.cloud.storage.VMTemplateVO;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@ -207,4 +209,49 @@ public class VMTemplateDaoImplTest {
VMTemplateVO readyTemplate = templateDao.findSystemVMReadyTemplate(zoneId, Hypervisor.HypervisorType.KVM, CPU.CPUArch.arm64.getType());
Assert.assertEquals(CPU.CPUArch.arm64, readyTemplate.getArch());
}
@Test
public void listByUserdataIdsNotAccount_ReturnsEmptyListWhenUserdataIdsIsEmpty() {
List<Long> result = templateDao.listByUserdataIdsNotAccount(Collections.emptyList(), 1L);
assertNotNull(result);
assertTrue(result.isEmpty());
}
@Test
public void listByUserdataIdsNotAccount_ReturnsMatchingTemplates() {
List<Long> userdataIds = Arrays.asList(1L, 2L);
long accountId = 3L;
GenericSearchBuilder<VMTemplateVO, Long> sb = mock(GenericSearchBuilder.class);
when(sb.entity()).thenReturn(mock(VMTemplateVO.class));
SearchCriteria<Long> sc = mock(SearchCriteria.class);
doReturn(sb).when(templateDao).createSearchBuilder(Long.class);
doReturn(sc).when(sb).create();
doReturn(Arrays.asList(10L, 20L)).when(templateDao).customSearch(sc, null);
List<Long> result = templateDao.listByUserdataIdsNotAccount(userdataIds, accountId);
assertNotNull(result);
assertEquals(2, result.size());
assertTrue(result.contains(10L));
assertTrue(result.contains(20L));
}
@Test
public void listByUserdataIdsNotAccount_ReturnsEmptyListWhenNoMatchingTemplates() {
List<Long> userdataIds = Arrays.asList(1L, 2L);
long accountId = 3L;
GenericSearchBuilder<VMTemplateVO, Long> sb = mock(GenericSearchBuilder.class);
when(sb.entity()).thenReturn(mock(VMTemplateVO.class));
SearchCriteria<Long> sc = mock(SearchCriteria.class);
doReturn(sb).when(templateDao).createSearchBuilder(Long.class);
doReturn(sc).when(sb).create();
doReturn(Collections.emptyList()).when(templateDao).customSearch(sc, null);
List<Long> result = templateDao.listByUserdataIdsNotAccount(userdataIds, accountId);
assertNotNull(result);
assertTrue(result.isEmpty());
}
}

View File

@ -0,0 +1,82 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.user.dao;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.user.UserDataVO;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class UserDataDaoImplTest {
@Spy
@InjectMocks
UserDataDaoImpl userDataDaoImpl;
@Test
public void listIdsByAccountId_ReturnsEmptyListWhenNoIdsFound() {
long accountId = 1L;
GenericSearchBuilder<UserDataVO, Long> sb = mock(GenericSearchBuilder.class);
when(sb.entity()).thenReturn(mock(UserDataVO.class));
SearchCriteria<Long> sc = mock(SearchCriteria.class);
doReturn(sb).when(userDataDaoImpl).createSearchBuilder(Long.class);
doReturn(sc).when(sb).create();
doReturn(Collections.emptyList()).when(userDataDaoImpl).customSearch(sc, null);
List<Long> result = userDataDaoImpl.listIdsByAccountId(accountId);
assertNotNull(result);
assertTrue(result.isEmpty());
}
@Test
public void listIdsByAccountId_ReturnsListOfIdsWhenFound() {
long accountId = 1L;
GenericSearchBuilder<UserDataVO, Long> sb = mock(GenericSearchBuilder.class);
when(sb.entity()).thenReturn(mock(UserDataVO.class));
SearchCriteria<Long> sc = mock(SearchCriteria.class);
doReturn(sb).when(userDataDaoImpl).createSearchBuilder(Long.class);
doReturn(sc).when(sb).create();
doReturn(Arrays.asList(10L, 20L)).when(userDataDaoImpl).customSearch(sc, null);
List<Long> result = userDataDaoImpl.listIdsByAccountId(accountId);
assertNotNull(result);
assertEquals(2, result.size());
assertTrue(result.contains(10L));
assertTrue(result.contains(20L));
}
}

View File

@ -475,6 +475,21 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
_querySelectors = querySelectors;
}
protected void deleteUserDataForAccount(long accountId) {
List<Long> userdataIdsList = userDataDao.listIdsByAccountId(accountId);
if (CollectionUtils.isEmpty(userdataIdsList)) {
return;
}
List<Long> conflictingTemplateIds = _templateDao.listByUserdataIdsNotAccount(userdataIdsList, accountId);
if (CollectionUtils.isNotEmpty(conflictingTemplateIds)) {
logger.warn("User data IDs {} owned by account ID {} cannot be deleted as some of them are " +
"linked to templates {} not owned by the account.", userdataIdsList, accountId,
conflictingTemplateIds);
throw new CloudRuntimeException("User data owned by account linked to templates not owned by the account");
}
userDataDao.removeByAccountId(accountId);
}
protected void deleteWebhooksForAccount(long accountId) {
try {
WebhookHelper webhookService = ComponentContext.getDelegateComponentOfType(WebhookHelper.class);
@ -1200,7 +1215,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
}
// Delete registered UserData
userDataDao.removeByAccountId(accountId);
deleteUserDataForAccount(accountId);
// Delete Webhooks
deleteWebhooksForAccount(accountId);

View File

@ -162,7 +162,13 @@ public class VMSchedulerImpl extends ManagerBase implements VMScheduler, Configu
}
Date scheduledDateTime = Date.from(ts.toInstant());
VMScheduledJobVO scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime);
VMScheduledJobVO scheduledJob = vmScheduledJobDao.findByScheduleAndTimestamp(vmSchedule.getId(), scheduledDateTime);
if (scheduledJob != null) {
logger.trace("Job is already scheduled for schedule {} at {}", vmSchedule, scheduledDateTime);
return scheduledDateTime;
}
scheduledJob = new VMScheduledJobVO(vmSchedule.getVmId(), vmSchedule.getId(), vmSchedule.getAction(), scheduledDateTime);
try {
vmScheduledJobDao.persist(scheduledJob);
ActionEventUtils.onScheduledActionEvent(User.UID_SYSTEM, vm.getAccountId(), actionEventMap.get(vmSchedule.getAction()),

View File

@ -23,6 +23,7 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -198,7 +199,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
sshkeyList.add(sshkey);
Mockito.when(_sshKeyPairDao.listKeyPairs(Mockito.anyLong(), Mockito.anyLong())).thenReturn(sshkeyList);
Mockito.when(_sshKeyPairDao.remove(Mockito.anyLong())).thenReturn(true);
Mockito.when(userDataDao.removeByAccountId(Mockito.anyLong())).thenReturn(222);
Mockito.doNothing().when(accountManagerImpl).deleteUserDataForAccount(Mockito.anyLong());
Mockito.doNothing().when(accountManagerImpl).deleteWebhooksForAccount(Mockito.anyLong());
Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations((Account) any());
@ -1589,4 +1590,41 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock);
}
@Test
public void deleteUserDataForAccountWhenNoUserDataExists() {
long accountId = 1L;
Mockito.when(userDataDao.listIdsByAccountId(accountId)).thenReturn(Collections.emptyList());
accountManagerImpl.deleteUserDataForAccount(accountId);
Mockito.verify(userDataDao, Mockito.times(1)).listIdsByAccountId(accountId);
Mockito.verify(userDataDao, Mockito.times(0)).removeByAccountId(accountId);
Mockito.verifyNoInteractions(_templateDao);
}
@Test
public void deleteUserDataForAccountWhenNoConflictingTemplatesExist() {
long accountId = 1L;
List<Long> userdataIds = List.of(101L, 102L);
Mockito.when(userDataDao.listIdsByAccountId(accountId)).thenReturn(userdataIds);
Mockito.when(_templateDao.listByUserdataIdsNotAccount(userdataIds, accountId)).thenReturn(Collections.emptyList());
accountManagerImpl.deleteUserDataForAccount(accountId);
Mockito.verify(userDataDao, Mockito.times(1)).listIdsByAccountId(accountId);
Mockito.verify(_templateDao, Mockito.times(1)).listByUserdataIdsNotAccount(userdataIds, accountId);
Mockito.verify(userDataDao, Mockito.times(1)).removeByAccountId(accountId);
}
@Test(expected = CloudRuntimeException.class)
public void deleteUserDataForAccountWhenConflictingTemplatesExist() {
long accountId = 1L;
List<Long> userdataIds = List.of(101L, 102L);
List<Long> conflictingTemplateIds = List.of(201L, 202L);
Mockito.when(userDataDao.listIdsByAccountId(accountId)).thenReturn(userdataIds);
Mockito.when(_templateDao.listByUserdataIdsNotAccount(userdataIds, accountId)).thenReturn(conflictingTemplateIds);
accountManagerImpl.deleteUserDataForAccount(accountId);
}
}

View File

@ -218,18 +218,19 @@ export const notifierPlugin = {
if (error.response.status) {
msg = `${i18n.global.t('message.request.failed')} (${error.response.status})`
}
if (error.message) {
desc = error.message
}
if (error.response.headers && 'x-description' in error.response.headers) {
if (error.response.headers?.['x-description']) {
desc = error.response.headers['x-description']
}
if (desc === '' && error.response.data) {
} else if (error.response.data) {
const responseKey = _.findKey(error.response.data, 'errortext')
if (responseKey) {
desc = error.response.data[responseKey].errortext
} else if (typeof error.response.data === 'string') {
desc = error.response.data
}
}
if (!desc && error.message) {
desc = error.message
}
}
let countNotify = store.getters.countNotify
countNotify++

View File

@ -638,11 +638,7 @@ export default {
this.$emit('refresh-data')
this.closeAction()
}).catch(e => {
this.$notification.error({
message: this.$t('message.upload.failed'),
description: `${this.$t('message.upload.template.failed.description')} - ${e}`,
duration: 0
})
this.$notifyError(e)
})
},
fetchCustomHypervisorName () {