Skip to content

Commit 9d04970

Browse files
Fix deletion of backup schedules (#11222)
1 parent e717216 commit 9d04970

File tree

9 files changed

+175
-28
lines changed

9 files changed

+175
-28
lines changed

api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
description = "Deletes the backup schedule of a VM",
4444
responseObject = SuccessResponse.class, since = "4.14.0",
4545
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
46-
public class DeleteBackupScheduleCmd extends BaseCmd {
46+
public class DeleteBackupScheduleCmd extends BaseCmd {
4747

4848
@Inject
4949
private BackupManager backupManager;
@@ -52,17 +52,13 @@ public class DeleteBackupScheduleCmd extends BaseCmd {
5252
//////////////// API parameters /////////////////////
5353
/////////////////////////////////////////////////////
5454

55-
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
56-
type = CommandType.UUID,
57-
entityType = UserVmResponse.class,
58-
description = "ID of the VM")
55+
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class,
56+
description = "ID of the VM from which all backup schedules will be deleted.")
5957
private Long vmId;
6058

61-
@Parameter(name = ApiConstants.ID,
62-
type = CommandType.UUID,
63-
entityType = BackupScheduleResponse.class,
64-
description = "ID of the schedule",
65-
since = "4.20.1")
59+
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = BackupScheduleResponse.class,
60+
since = "4.20.1", description = "ID of the backup schedule to be deleted. It has precedence over the 'virtualmachineid' parameter, " +
61+
"i.e., when the 'id' parameter is specified, the 'virtualmachineid' parameter will be ignored.")
6662
private Long id;
6763

6864
/////////////////////////////////////////////////////

api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
@EntityReference(value = BackupSchedule.class)
3030
public class BackupScheduleResponse extends BaseResponse {
31+
@SerializedName(ApiConstants.ID)
32+
@Param(description = "ID of the backup schedule.")
33+
private String id;
3134

3235
@SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME)
3336
@Param(description = "name of the VM")
@@ -51,7 +54,11 @@ public class BackupScheduleResponse extends BaseResponse {
5154

5255
@SerializedName(ApiConstants.MAX_BACKUPS)
5356
@Param(description = "maximum number of backups retained")
54-
private Integer maxBakups;
57+
private Integer maxBackups;
58+
59+
public void setId(String id) {
60+
this.id = id;
61+
}
5562

5663
public String getVmName() {
5764
return vmName;
@@ -93,7 +100,7 @@ public void setTimezone(String timezone) {
93100
this.timezone = timezone;
94101
}
95102

96-
public void setMaxBakups(Integer maxBakups) {
97-
this.maxBakups = maxBakups;
103+
public void setMaxBackups(Integer maxBackups) {
104+
this.maxBackups = maxBackups;
98105
}
99106
}

api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ public interface BackupSchedule extends InternalIdentity {
3131
Date getScheduledTimestamp();
3232
Long getAsyncJobId();
3333
Integer getMaxBackups();
34+
String getUuid();
3435
}

engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.cloudstack.backup;
1919

2020
import java.util.Date;
21+
import java.util.UUID;
2122

2223
import javax.persistence.Column;
2324
import javax.persistence.Entity;
@@ -39,6 +40,9 @@ public class BackupScheduleVO implements BackupSchedule {
3940
@Column(name = "id")
4041
private long id;
4142

43+
@Column(name = "uuid", nullable = false)
44+
private String uuid = UUID.randomUUID().toString();
45+
4246
@Column(name = "vm_id")
4347
private Long vmId;
4448

@@ -84,6 +88,11 @@ public long getId() {
8488
return id;
8589
}
8690

91+
@Override
92+
public String getUuid() {
93+
return uuid;
94+
}
95+
8796
public Long getVmId() {
8897
return vmId;
8998
}

engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,13 @@ public List<BackupScheduleVO> getSchedulesToExecute(Date currentTimestamp) {
9292
public BackupScheduleResponse newBackupScheduleResponse(BackupSchedule schedule) {
9393
VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(schedule.getVmId());
9494
BackupScheduleResponse response = new BackupScheduleResponse();
95+
response.setId(schedule.getUuid());
9596
response.setVmId(vm.getUuid());
9697
response.setVmName(vm.getHostName());
9798
response.setIntervalType(schedule.getScheduleType());
9899
response.setSchedule(schedule.getSchedule());
99100
response.setTimezone(schedule.getTimezone());
100-
response.setMaxBakups(schedule.getMaxBackups());
101+
response.setMaxBackups(schedule.getMaxBackups());
101102
response.setObjectName("backupschedule");
102103
return response;
103104
}

engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,6 @@ CREATE TABLE IF NOT EXISTS `cloud`.`gui_themes_details` (
233233
PRIMARY KEY (`id`),
234234
CONSTRAINT `fk_gui_themes_details__gui_theme_id` FOREIGN KEY (`gui_theme_id`) REFERENCES `gui_themes`(`id`)
235235
);
236+
237+
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule', 'uuid', 'VARCHAR(40) NOT NULL');
238+
UPDATE `cloud`.`backup_schedule` SET uuid = UUID();

server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
7272
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
7373
import org.apache.commons.lang3.BooleanUtils;
74+
import org.apache.commons.lang3.ObjectUtils;
7475
import org.apache.commons.lang3.StringUtils;
7576

7677
import com.amazonaws.util.CollectionUtils;
@@ -515,24 +516,44 @@ public List<BackupSchedule> listBackupSchedule(final Long vmId) {
515516
public boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd) {
516517
Long vmId = cmd.getVmId();
517518
Long id = cmd.getId();
518-
if (Objects.isNull(vmId) && Objects.isNull(id)) {
519-
throw new InvalidParameterValueException("Either instance ID or ID of backup schedule needs to be specified");
520-
}
521-
if (Objects.nonNull(vmId)) {
522-
final VMInstanceVO vm = findVmById(vmId);
523-
validateForZone(vm.getDataCenterId());
524-
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm);
525-
return deleteAllVMBackupSchedules(vm.getId());
526-
} else {
527-
final BackupSchedule schedule = backupScheduleDao.findById(id);
519+
if (ObjectUtils.allNull(vmId, id)) {
520+
throw new InvalidParameterValueException("Either instance ID or ID of backup schedule needs to be specified.");
521+
}
522+
523+
if (Objects.nonNull(id)) {
524+
BackupSchedule schedule = backupScheduleDao.findById(id);
528525
if (schedule == null) {
529-
throw new CloudRuntimeException("Could not find the requested backup schedule.");
526+
throw new InvalidParameterValueException("Could not find the requested backup schedule.");
530527
}
528+
checkCallerAccessToBackupScheduleVm(schedule.getVmId());
531529
return backupScheduleDao.remove(schedule.getId());
532530
}
531+
532+
checkCallerAccessToBackupScheduleVm(vmId);
533+
return deleteAllVmBackupSchedules(vmId);
534+
}
535+
536+
/**
537+
* Checks if the backup framework is enabled for the zone in which the VM with specified ID is allocated and
538+
* if the caller has access to the VM.
539+
*
540+
* @param vmId The ID of the virtual machine to check access for
541+
* @throws PermissionDeniedException if the caller doesn't have access to the VM
542+
* @throws CloudRuntimeException if the backup framework is disabled
543+
*/
544+
protected void checkCallerAccessToBackupScheduleVm(long vmId) {
545+
VMInstanceVO vm = findVmById(vmId);
546+
validateForZone(vm.getDataCenterId());
547+
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm);
533548
}
534549

535-
private boolean deleteAllVMBackupSchedules(long vmId) {
550+
/**
551+
* Deletes all backup schedules associated with a specific VM.
552+
*
553+
* @param vmId The ID of the virtual machine whose backup schedules should be deleted
554+
* @return true if all backup schedules were successfully deleted, false if any deletion failed
555+
*/
556+
protected boolean deleteAllVmBackupSchedules(long vmId) {
536557
List<BackupScheduleVO> vmBackupSchedules = backupScheduleDao.listByVM(vmId);
537558
boolean success = true;
538559
for (BackupScheduleVO vmBackupSchedule : vmBackupSchedules) {

server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.apache.cloudstack.api.ServerApiException;
4848
import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
4949
import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd;
50+
import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd;
5051
import org.apache.cloudstack.backup.dao.BackupDao;
5152
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
5253
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
@@ -75,6 +76,8 @@
7576
import java.util.TimeZone;
7677

7778
import static org.junit.Assert.assertEquals;
79+
import static org.junit.Assert.assertFalse;
80+
import static org.junit.Assert.assertTrue;
7881
import static org.junit.Assert.fail;
7982
import static org.mockito.ArgumentMatchers.any;
8083
import static org.mockito.Mockito.mock;
@@ -127,7 +130,21 @@ public class BackupManagerTest {
127130
@Mock
128131
AlertManager alertManager;
129132

130-
private AccountVO account;
133+
@Mock
134+
private VMInstanceVO vmInstanceVOMock;
135+
136+
@Mock
137+
private CallContext callContextMock;
138+
139+
@Mock
140+
private AccountVO accountVOMock;
141+
142+
@Mock
143+
private DeleteBackupScheduleCmd deleteBackupScheduleCmdMock;
144+
145+
@Mock
146+
private BackupScheduleVO backupScheduleVOMock;
147+
131148
private UserVO user;
132149

133150
private String[] hostPossibleValues = {"127.0.0.1", "hostname"};
@@ -657,4 +674,96 @@ public void testBackupSyncTask() {
657674
}
658675
}
659676
}
677+
678+
@Test
679+
public void checkCallerAccessToBackupScheduleVmTestExecuteAccessCheckMethods() {
680+
long vmId = 1L;
681+
long dataCenterId = 2L;
682+
683+
try (MockedStatic<CallContext> mockedCallContext = Mockito.mockStatic(CallContext.class)) {
684+
Mockito.when(vmInstanceDao.findById(vmId)).thenReturn(vmInstanceVOMock);
685+
Mockito.when(vmInstanceVOMock.getDataCenterId()).thenReturn(dataCenterId);
686+
Mockito.when(backupManager.isDisabled(dataCenterId)).thenReturn(false);
687+
688+
mockedCallContext.when(CallContext::current).thenReturn(callContextMock);
689+
Mockito.when(callContextMock.getCallingAccount()).thenReturn(accountVOMock);
690+
Mockito.doNothing().when(accountManager).checkAccess(accountVOMock, null, true, vmInstanceVOMock);
691+
backupManager.checkCallerAccessToBackupScheduleVm(vmId);
692+
693+
verify(accountManager, times(1)).checkAccess(accountVOMock, null, true, vmInstanceVOMock);
694+
}
695+
}
696+
697+
@Test
698+
public void deleteAllVmBackupSchedulesTestReturnSuccessWhenAllSchedulesAreDeleted() {
699+
long vmId = 1L;
700+
List<BackupScheduleVO> backupSchedules = List.of(Mockito.mock(BackupScheduleVO.class), Mockito.mock(BackupScheduleVO.class));
701+
Mockito.when(backupScheduleDao.listByVM(vmId)).thenReturn(backupSchedules);
702+
Mockito.when(backupSchedules.get(0).getId()).thenReturn(2L);
703+
Mockito.when(backupSchedules.get(1).getId()).thenReturn(3L);
704+
Mockito.when(backupScheduleDao.remove(Mockito.anyLong())).thenReturn(true);
705+
706+
boolean success = backupManager.deleteAllVmBackupSchedules(vmId);
707+
assertTrue(success);
708+
Mockito.verify(backupScheduleDao, times(2)).remove(Mockito.anyLong());
709+
}
710+
711+
@Test
712+
public void deleteAllVmBackupSchedulesTestReturnFalseWhenAnyDeletionFails() {
713+
long vmId = 1L;
714+
List<BackupScheduleVO> backupSchedules = List.of(Mockito.mock(BackupScheduleVO.class), Mockito.mock(BackupScheduleVO.class));
715+
Mockito.when(backupScheduleDao.listByVM(vmId)).thenReturn(backupSchedules);
716+
Mockito.when(backupSchedules.get(0).getId()).thenReturn(2L);
717+
Mockito.when(backupSchedules.get(1).getId()).thenReturn(3L);
718+
Mockito.when(backupScheduleDao.remove(2L)).thenReturn(true);
719+
Mockito.when(backupScheduleDao.remove(3L)).thenReturn(false);
720+
721+
boolean success = backupManager.deleteAllVmBackupSchedules(vmId);
722+
assertFalse(success);
723+
Mockito.verify(backupScheduleDao, times(2)).remove(Mockito.anyLong());
724+
}
725+
726+
@Test(expected = InvalidParameterValueException.class)
727+
public void deleteBackupScheduleTestThrowExceptionWhenVmIdAndScheduleIdAreNull() {
728+
when(deleteBackupScheduleCmdMock.getVmId()).thenReturn(null);
729+
when(deleteBackupScheduleCmdMock.getId()).thenReturn(null);
730+
731+
backupManager.deleteBackupSchedule(deleteBackupScheduleCmdMock);
732+
}
733+
734+
@Test
735+
public void deleteBackupScheduleTestDeleteVmSchedulesWhenVmIdIsSpecified() {
736+
long vmId = 1L;
737+
738+
when(deleteBackupScheduleCmdMock.getId()).thenReturn(null);
739+
when(deleteBackupScheduleCmdMock.getVmId()).thenReturn(vmId);
740+
Mockito.doNothing().when(backupManager).checkCallerAccessToBackupScheduleVm(vmId);
741+
Mockito.doReturn(true).when(backupManager).deleteAllVmBackupSchedules(vmId);
742+
743+
boolean success = backupManager.deleteBackupSchedule(deleteBackupScheduleCmdMock);
744+
assertTrue(success);
745+
}
746+
747+
@Test(expected = InvalidParameterValueException.class)
748+
public void deleteBackupScheduleTestThrowExceptionWhenSpecificScheduleIsNotFound() {
749+
long id = 1L;
750+
when(deleteBackupScheduleCmdMock.getId()).thenReturn(id);
751+
backupManager.deleteBackupSchedule(deleteBackupScheduleCmdMock);
752+
}
753+
754+
@Test
755+
public void deleteBackupScheduleTestDeleteSpecificScheduleWhenItsIdIsSpecified() {
756+
long id = 1L;
757+
long vmId = 2L;
758+
when(deleteBackupScheduleCmdMock.getId()).thenReturn(id);
759+
when(deleteBackupScheduleCmdMock.getVmId()).thenReturn(null);
760+
when(backupScheduleDao.findById(id)).thenReturn(backupScheduleVOMock);
761+
when(backupScheduleVOMock.getVmId()).thenReturn(vmId);
762+
Mockito.doNothing().when(backupManager).checkCallerAccessToBackupScheduleVm(vmId);
763+
when(backupScheduleVOMock.getId()).thenReturn(id);
764+
when(backupScheduleDao.remove(id)).thenReturn(true);
765+
766+
boolean success = backupManager.deleteBackupSchedule(deleteBackupScheduleCmdMock);
767+
assertTrue(success);
768+
}
660769
}

ui/src/views/compute/backup/BackupSchedule.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export default {
167167
methods: {
168168
handleClickDelete (record) {
169169
const params = {}
170-
params.virtualmachineid = record.virtualmachineid
170+
params.id = record.id
171171
this.actionLoading = true
172172
postAPI('deleteBackupSchedule', params).then(json => {
173173
if (json.deletebackupscheduleresponse.success) {

0 commit comments

Comments
 (0)