From 8f806ff221ebd48109a2d68a350fbba3e0183634 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 5 Sep 2025 14:09:37 -0400 Subject: [PATCH 01/23] API: Add support to list all snapshot policies & backup schedules --- .../storage/snapshot/SnapshotApiService.java | 2 +- .../storage/snapshot/SnapshotPolicy.java | 3 +- .../user/backup/ListBackupScheduleCmd.java | 3 +- .../snapshot/ListSnapshotPoliciesCmd.java | 7 ++- .../cloudstack/backup/BackupManager.java | 3 +- .../cloudstack/backup/BackupSchedule.java | 3 +- .../com/cloud/storage/SnapshotPolicyVO.java | 38 +++++++++++- .../upgrade/dao/Upgrade42100to42200.java | 50 ++++++++++++++++ .../cloudstack/backup/BackupScheduleVO.java | 30 +++++++++- .../META-INF/db/schema-42100to42200.sql | 3 + .../test/SnapshotTestWithFakeData.java | 2 +- .../storage/snapshot/SnapshotManagerImpl.java | 59 +++++++++++++------ .../cloudstack/backup/BackupManagerImpl.java | 13 ++-- .../storage/snapshot/SnapshotManagerTest.java | 6 +- 14 files changed, 184 insertions(+), 38 deletions(-) diff --git a/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java b/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java index 67afd6aa4e24..d52e645ec799 100644 --- a/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java +++ b/api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java @@ -85,7 +85,7 @@ public interface SnapshotApiService { * the command that specifies the volume criteria * @return list of snapshot policies */ - Pair, Integer> listPoliciesforVolume(ListSnapshotPoliciesCmd cmd); + Pair, Integer> listSnapshotPolicies(ListSnapshotPoliciesCmd cmd); boolean deleteSnapshotPolicies(DeleteSnapshotPoliciesCmd cmd); diff --git a/api/src/main/java/com/cloud/storage/snapshot/SnapshotPolicy.java b/api/src/main/java/com/cloud/storage/snapshot/SnapshotPolicy.java index 22d5dfb9c1b8..13009a9808aa 100644 --- a/api/src/main/java/com/cloud/storage/snapshot/SnapshotPolicy.java +++ b/api/src/main/java/com/cloud/storage/snapshot/SnapshotPolicy.java @@ -16,11 +16,12 @@ // under the License. package com.cloud.storage.snapshot; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.Displayable; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; -public interface SnapshotPolicy extends Identity, InternalIdentity, Displayable { +public interface SnapshotPolicy extends ControlledEntity, Identity, InternalIdentity, Displayable { long getVolumeId(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java index fa6e3ea5d455..0d494834344f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java @@ -60,7 +60,6 @@ public class ListBackupScheduleCmd extends BaseCmd { @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, - required = true, description = "ID of the VM") private Long vmId; @@ -79,7 +78,7 @@ public Long getVmId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { try{ - List schedules = backupManager.listBackupSchedule(getVmId()); + List schedules = backupManager.listBackupSchedule(this); ListResponse response = new ListResponse<>(); List scheduleResponses = new ArrayList<>(); if (!CollectionUtils.isNullOrEmpty(schedules)) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ListSnapshotPoliciesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ListSnapshotPoliciesCmd.java index 126a4080e6d2..20e362472cd2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ListSnapshotPoliciesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ListSnapshotPoliciesCmd.java @@ -23,7 +23,7 @@ import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.BaseListDomainResourcesCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.SnapshotPolicyResponse; @@ -34,7 +34,7 @@ @APICommand(name = "listSnapshotPolicies", description = "Lists snapshot policies.", responseObject = SnapshotPolicyResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class ListSnapshotPoliciesCmd extends BaseListCmd { +public class ListSnapshotPoliciesCmd extends BaseListDomainResourcesCmd { ///////////////////////////////////////////////////// @@ -69,13 +69,14 @@ public boolean isDisplay() { public Long getId() { return id; } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @Override public void execute() { - Pair, Integer> result = _snapshotService.listPoliciesforVolume(this); + Pair, Integer> result = _snapshotService.listSnapshotPolicies(this); ListResponse response = new ListResponse(); List policyResponses = new ArrayList(); for (SnapshotPolicy policy : result.first()) { diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index c4b92fc9e05c..f9d396ecc1df 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.framework.config.ConfigKey; @@ -174,7 +175,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer * @param vmId * @return */ - List listBackupSchedule(Long vmId); + List listBackupSchedule(ListBackupScheduleCmd cmd); /** * Deletes VM backup schedule for a VM diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java index b5138d34de11..44fdf70c4c15 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java @@ -19,11 +19,12 @@ import java.util.Date; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.InternalIdentity; import com.cloud.utils.DateUtil; -public interface BackupSchedule extends InternalIdentity { +public interface BackupSchedule extends ControlledEntity, InternalIdentity { Long getVmId(); DateUtil.IntervalType getScheduleType(); String getSchedule(); diff --git a/engine/schema/src/main/java/com/cloud/storage/SnapshotPolicyVO.java b/engine/schema/src/main/java/com/cloud/storage/SnapshotPolicyVO.java index f57d9d3dccf2..299c6380ab63 100644 --- a/engine/schema/src/main/java/com/cloud/storage/SnapshotPolicyVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/SnapshotPolicyVO.java @@ -59,6 +59,12 @@ public class SnapshotPolicyVO implements SnapshotPolicy { @Column(name = "uuid") String uuid; + @Column(name = "account_id") + long accountId; + + @Column(name = "domain_id") + long domainId; + @Column(name = "display", updatable = true, nullable = false) protected boolean display = true; @@ -66,7 +72,7 @@ public SnapshotPolicyVO() { this.uuid = UUID.randomUUID().toString(); } - public SnapshotPolicyVO(long volumeId, String schedule, String timezone, IntervalType intvType, int maxSnaps, boolean display) { + public SnapshotPolicyVO(long volumeId, String schedule, String timezone, IntervalType intvType, int maxSnaps, long accountId, long domainId, boolean display) { this.volumeId = volumeId; this.schedule = schedule; this.timezone = timezone; @@ -75,6 +81,8 @@ public SnapshotPolicyVO(long volumeId, String schedule, String timezone, Interva this.active = true; this.display = display; this.uuid = UUID.randomUUID().toString(); + this.accountId = accountId; + this.domainId = domainId; } @Override @@ -160,4 +168,32 @@ public boolean isDisplay() { public void setDisplay(boolean display) { this.display = display; } + + @Override + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + @Override + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + @Override + public Class getEntityType() { + return SnapshotPolicy.class; + } + + @Override + public String getName() { + return null; + } } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java index c2cfd02c15cf..169e10d1ecad 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java @@ -16,6 +16,14 @@ // under the License. package com.cloud.upgrade.dao; +import com.cloud.utils.exception.CloudRuntimeException; + +import java.io.InputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + public class Upgrade42100to42200 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { @Override @@ -27,4 +35,46 @@ public String[] getUpgradableVersionRange() { public String getUpgradedVersion() { return "4.22.0.0"; } + + @Override + public InputStream[] getPrepareScripts() { + final String scriptFile = "META-INF/db/schema-42100to42200.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + + return new InputStream[] {script}; + } + + @Override + public void performDataMigration(Connection conn) { + updateSnapshotPolicyOwnership(conn); + } + + private void updateSnapshotPolicyOwnership(Connection conn) { + // set account_id and domain_id in snapshot_policy table from volume table + String selectSql = "SELECT sp.id, v.account_id, v.domain_id FROM snapshot_policy sp, volumes v WHERE sp.volume_id = v.id AND (sp.account_id IS NULL AND sp.domain_id IS NULL)"; + String updateSql = "UPDATE snapshot_policy SET account_id = ?, domain_id = ? WHERE id = ?"; + + try (PreparedStatement selectPstmt = conn.prepareStatement(selectSql); + ResultSet rs = selectPstmt.executeQuery(); + PreparedStatement updatePstmt = conn.prepareStatement(updateSql)) { + + while (rs.next()) { + long policyId = rs.getLong(1); + long accountId = rs.getLong(2); + long domainId = rs.getLong(3); + + updatePstmt.setLong(1, accountId); + updatePstmt.setLong(2, domainId); + updatePstmt.setLong(3, policyId); + updatePstmt.executeUpdate(); + } + } catch (SQLException e) { + throw new CloudRuntimeException("Unable to update snapshot_policy table with account_id and domain_id", e); + } + } + + } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java index 37e8105e3d51..511a6b735d85 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java @@ -68,10 +68,16 @@ public class BackupScheduleVO implements BackupSchedule { @Column(name = "quiescevm") Boolean quiesceVM = false; + @Column(name = "account_id") + Long accountId; + + @Column(name = "domain_id") + Long domainId; + public BackupScheduleVO() { } - public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp, int maxBackups, Boolean quiesceVM) { + public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp, int maxBackups, Boolean quiesceVM, Long accountId, Long domainId) { this.vmId = vmId; this.scheduleType = (short) scheduleType.ordinal(); this.schedule = schedule; @@ -79,6 +85,8 @@ public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String sc this.scheduledTimestamp = scheduledTimestamp; this.maxBackups = maxBackups; this.quiesceVM = quiesceVM; + this.accountId = accountId; + this.domainId = domainId; } @Override @@ -161,4 +169,24 @@ public void setQuiesceVM(Boolean quiesceVM) { public Boolean getQuiesceVM() { return quiesceVM; } + + @Override + public Class getEntityType() { + return BackupSchedule.class; + } + + @Override + public String getName() { + return null; + } + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public long getAccountId() { + return accountId; + } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index cf3fe2ed7726..e9f30f6408f4 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -21,3 +21,6 @@ -- Increase length of scripts_version column to 128 due to md5sum to sha512sum change CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.domain_router', 'scripts_version', 'scripts_version', 'VARCHAR(128)'); + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_policy','domain_id', 'BIGINT(20) DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_policy','account_id', 'BIGINT(20) DEFAULT NULL'); diff --git a/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/SnapshotTestWithFakeData.java b/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/SnapshotTestWithFakeData.java index 152c279547c6..9868ccdf29a3 100644 --- a/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/SnapshotTestWithFakeData.java +++ b/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/SnapshotTestWithFakeData.java @@ -305,7 +305,7 @@ public void testTakeSnapshotFromVolume() throws URISyntaxException { } protected SnapshotPolicyVO createSnapshotPolicy(Long volId) { - SnapshotPolicyVO policyVO = new SnapshotPolicyVO(volId, "jfkd", "fdfd", DateUtil.IntervalType.DAILY, 8, true); + SnapshotPolicyVO policyVO = new SnapshotPolicyVO(volId, "jfkd", "fdfd", DateUtil.IntervalType.DAILY, 8, 1, 1, true); policyVO = snapshotPolicyDao.persist(policyVO); return policyVO; } diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index f6c8655fd66f..e6e42c8b2444 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1301,7 +1301,8 @@ protected SnapshotPolicyVO persistSnapshotPolicy(VolumeVO volume, String schedul } protected SnapshotPolicyVO createSnapshotPolicy(long volumeId, String schedule, String timezone, IntervalType intervalType, int maxSnaps, boolean display, List zoneIds, List poolIds) { - SnapshotPolicyVO policy = new SnapshotPolicyVO(volumeId, schedule, timezone, intervalType, maxSnaps, display); + VolumeVO volume = _volsDao.findById(volumeId); + SnapshotPolicyVO policy = new SnapshotPolicyVO(volumeId, schedule, timezone, intervalType, maxSnaps, volume.getAccountId(), volume.getDomainId(), display); policy = _snapshotPolicyDao.persist(policy); if (CollectionUtils.isNotEmpty(zoneIds)) { List details = new ArrayList<>(); @@ -1386,28 +1387,48 @@ protected boolean deletePolicy(Long policyId) { } @Override - public Pair, Integer> listPoliciesforVolume(ListSnapshotPoliciesCmd cmd) { + public Pair, Integer> listSnapshotPolicies(ListSnapshotPoliciesCmd cmd) { Long volumeId = cmd.getVolumeId(); - boolean display = cmd.isDisplay(); Long id = cmd.getId(); - Pair, Integer> result = null; - // TODO - Have a better way of doing this. - if (id != null) { - result = _snapshotPolicyDao.listAndCountById(id, display, null); - if (result != null && result.first() != null && !result.first().isEmpty()) { - SnapshotPolicyVO snapshotPolicy = result.first().get(0); - volumeId = snapshotPolicy.getVolumeId(); - } + Account caller = CallContext.current().getCallingAccount(); + boolean isRootAdmin = _accountMgr.isRootAdmin(caller.getId()); + List permittedAccounts = new ArrayList<>(); + Long domainId = null; + Boolean isRecursive = null; + ListProjectResourcesCriteria listProjectResourcesCriteria = null; + + if (!isRootAdmin) { + Ternary domainIdRecursiveListProject = + new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + _accountMgr.buildACLSearchParameters(caller, id, null, null, permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + domainId = domainIdRecursiveListProject.first(); + isRecursive = domainIdRecursiveListProject.second(); + listProjectResourcesCriteria = domainIdRecursiveListProject.third(); } - VolumeVO volume = _volsDao.findById(volumeId); - if (volume == null) { - throw new InvalidParameterValueException("Unable to find a volume with id " + volumeId); + Filter searchFilter = new Filter(SnapshotPolicyVO.class, "id", false, cmd.getStartIndex(), cmd.getPageSizeVal()); + SearchBuilder policySearch = _snapshotPolicyDao.createSearchBuilder(); + + if (!isRootAdmin) { + _accountMgr.buildACLSearchBuilder(policySearch, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); } - _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, volume); - if (result != null) - return new Pair, Integer>(result.first(), result.second()); - result = _snapshotPolicyDao.listAndCountByVolumeId(volumeId, display); - return new Pair, Integer>(result.first(), result.second()); + + policySearch.and("id", policySearch.entity().getId(), SearchCriteria.Op.EQ); + policySearch.and("volumeId", policySearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + + SearchCriteria sc = policySearch.create(); + if (!isRootAdmin) { + _accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + } + + if (volumeId != null) { + sc.setParameters("volumeId", volumeId); + } + if (id != null) { + sc.setParameters("id", id); + } + + Pair, Integer> result = _snapshotPolicyDao.searchAndCount(sc, searchFilter); + return new Pair<>(result.first(), result.second()); } private List listPoliciesforVolume(long volumeId) { diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 112626adf4fe..72c5e481523b 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -588,7 +588,7 @@ public BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd) { final BackupScheduleVO schedule = backupScheduleDao.findByVMAndIntervalType(vmId, intervalType); if (schedule == null) { - return backupScheduleDao.persist(new BackupScheduleVO(vmId, intervalType, scheduleString, timezoneId, nextDateTime, maxBackups, cmd.getQuiesceVM())); + return backupScheduleDao.persist(new BackupScheduleVO(vmId, intervalType, scheduleString, timezoneId, nextDateTime, maxBackups, cmd.getQuiesceVM(), vm.getAccountId(), vm.getDomainId())); } schedule.setScheduleType((short) intervalType.ordinal()); @@ -639,10 +639,13 @@ protected int validateAndGetDefaultBackupRetentionIfRequired(Integer maxBackups, } @Override - public List listBackupSchedule(final Long vmId) { - final VMInstanceVO vm = findVmById(vmId); - validateBackupForZone(vm.getDataCenterId()); - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + public List listBackupSchedule(ListBackupScheduleCmd cmd) { + Long vmId = cmd.getVmId(); + if (vmId != null) { + final VMInstanceVO vm = findVmById(vmId); + validateBackupForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + } return backupScheduleDao.listByVM(vmId).stream().map(BackupSchedule.class::cast).collect(Collectors.toList()); } diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java index 4d8023199351..3b5d92103e77 100755 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java @@ -209,6 +209,8 @@ public class SnapshotManagerTest { private static final String TEST_SNAPSHOT_POLICY_TIMEZONE = ""; private static final IntervalType TEST_SNAPSHOT_POLICY_INTERVAL = IntervalType.MONTHLY; private static final int TEST_SNAPSHOT_POLICY_MAX_SNAPS = 1; + private static final long TEST_SNAPSHOT_POLICY_ACCOUNT_ID = 1; + private static final long TEST_SNAPSHOT_POLICY_DOMAIN_ID = 1; private static final boolean TEST_SNAPSHOT_POLICY_DISPLAY = true; private static final boolean TEST_SNAPSHOT_POLICY_ACTIVE = true; private static final long TEST_ZONE_ID = 7L; @@ -251,7 +253,7 @@ public void setup() throws ResourceAllocationException { when(_resourceMgr.listAllUpAndEnabledHostsInOneZoneByHypervisor(any(HypervisorType.class), anyLong())).thenReturn(null); snapshotPolicyVoInstance = new SnapshotPolicyVO(TEST_VOLUME_ID, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, TEST_SNAPSHOT_POLICY_INTERVAL, - TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY); + TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_ACCOUNT_ID, TEST_SNAPSHOT_POLICY_DOMAIN_ID, TEST_SNAPSHOT_POLICY_DISPLAY); apiDBUtilsMock = Mockito.mockStatic(ApiDBUtils.class); } @@ -442,7 +444,7 @@ public void validateUpdateSnapshotPolicy(){ Mockito.doReturn(true).when(taggedResourceServiceMock).deleteTags(any(), any(), any()); SnapshotPolicyVO snapshotPolicyVo = new SnapshotPolicyVO(TEST_VOLUME_ID, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, TEST_SNAPSHOT_POLICY_INTERVAL, - TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY); + TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_ACCOUNT_ID, TEST_SNAPSHOT_POLICY_DOMAIN_ID, TEST_SNAPSHOT_POLICY_DISPLAY); _snapshotMgr.updateSnapshotPolicy(snapshotPolicyVo, TEST_SNAPSHOT_POLICY_SCHEDULE, TEST_SNAPSHOT_POLICY_TIMEZONE, TEST_SNAPSHOT_POLICY_INTERVAL, TEST_SNAPSHOT_POLICY_MAX_SNAPS, TEST_SNAPSHOT_POLICY_DISPLAY, TEST_SNAPSHOT_POLICY_ACTIVE, null, null); From 619f01ec497641415c8c5c8481939d2e585ebc9a Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 8 Sep 2025 13:31:22 -0400 Subject: [PATCH 02/23] Add support for backup policy listing without tying it to the vmid --- .../user/backup/ListBackupScheduleCmd.java | 24 ++++-- .../api/response/SnapshotPolicyResponse.java | 8 ++ .../upgrade/dao/Upgrade42100to42200.java | 23 +++++ .../META-INF/db/schema-42100to42200.sql | 3 + .../java/com/cloud/api/ApiResponseHelper.java | 1 + .../cloudstack/backup/BackupManagerImpl.java | 45 ++++++++-- ui/public/locales/en.json | 7 ++ ui/src/components/view/DetailsTab.vue | 7 ++ ui/src/components/view/ListView.vue | 86 +++++++++++++++++-- ui/src/components/view/SearchView.vue | 50 ++++++++++- ui/src/config/section/storage.js | 60 +++++++++++++ 11 files changed, 293 insertions(+), 21 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java index 0d494834344f..c76e3da240ee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java @@ -24,7 +24,7 @@ import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseListDomainResourcesCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.BackupScheduleResponse; @@ -39,7 +39,6 @@ import com.cloud.exception.NetworkRuleConflictException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; -import com.cloud.utils.exception.CloudRuntimeException; import java.util.ArrayList; import java.util.List; @@ -48,7 +47,7 @@ description = "List backup schedule of a VM", responseObject = BackupScheduleResponse.class, since = "4.14.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) -public class ListBackupScheduleCmd extends BaseCmd { +public class ListBackupScheduleCmd extends BaseListDomainResourcesCmd { @Inject private BackupManager backupManager; @@ -63,6 +62,12 @@ public class ListBackupScheduleCmd extends BaseCmd { description = "ID of the VM") private Long vmId; + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupScheduleResponse.class, + description = "the ID of the backup schedule") + private Long id; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -71,6 +76,10 @@ public Long getVmId() { return vmId; } + public Long getId() { + return id; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -81,16 +90,15 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE List schedules = backupManager.listBackupSchedule(this); ListResponse response = new ListResponse<>(); List scheduleResponses = new ArrayList<>(); + if (!CollectionUtils.isNullOrEmpty(schedules)) { for (BackupSchedule schedule : schedules) { scheduleResponses.add(_responseGenerator.createBackupScheduleResponse(schedule)); } - response.setResponses(scheduleResponses, schedules.size()); - response.setResponseName(getCommandName()); - setResponseObject(response); - } else { - throw new CloudRuntimeException("No backup schedule exists for the VM"); } + response.setResponses(scheduleResponses, schedules.size()); + response.setResponseName(getCommandName()); + setResponseObject(response); } catch (Exception e) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java index 4ce77cfdf6e2..998d6af871dc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/SnapshotPolicyResponse.java @@ -37,6 +37,10 @@ public class SnapshotPolicyResponse extends BaseResponseWithTagInformation { @Param(description = "the ID of the disk volume") private String volumeId; + @SerializedName("volumename") + @Param(description = "the name of the disk volume") + private String volumeName; + @SerializedName("schedule") @Param(description = "time the snapshot is scheduled to be taken.") private String schedule; @@ -87,6 +91,10 @@ public void setVolumeId(String volumeId) { this.volumeId = volumeId; } + public void setVolumeName(String volumeName) { + this.volumeName = volumeName; + } + public String getSchedule() { return schedule; } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java index 169e10d1ecad..8b6a8d7670b9 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java @@ -50,6 +50,7 @@ public InputStream[] getPrepareScripts() { @Override public void performDataMigration(Connection conn) { updateSnapshotPolicyOwnership(conn); + updateBackupScheduleOwnership(conn); } private void updateSnapshotPolicyOwnership(Connection conn) { @@ -76,5 +77,27 @@ private void updateSnapshotPolicyOwnership(Connection conn) { } } + private void updateBackupScheduleOwnership(Connection conn) { + // Set account_id and domain_id in backup_schedule table from vm_instance table + String selectSql = "SELECT bs.id, vm.account_id, vm.domain_id FROM backup_schedule bs, vm_instance vm WHERE bs.vm_id = vm.id AND (bs.account_id IS NULL AND bs.domain_id IS NULL)"; + String updateSql = "UPDATE backup_schedule SET account_id = ?, domain_id = ? WHERE id = ?"; + try (PreparedStatement selectPstmt = conn.prepareStatement(selectSql); + ResultSet rs = selectPstmt.executeQuery(); + PreparedStatement updatePstmt = conn.prepareStatement(updateSql)) { + + while (rs.next()) { + long scheduleId = rs.getLong(1); + long accountId = rs.getLong(2); + long domainId = rs.getLong(3); + + updatePstmt.setLong(1, accountId); + updatePstmt.setLong(2, domainId); + updatePstmt.setLong(3, scheduleId); + updatePstmt.executeUpdate(); + } + } catch (SQLException e) { + throw new CloudRuntimeException("Unable to update backup_schedule table with account_id and domain_id", e); + } + } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index e9f30f6408f4..ecb0d833e4da 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -24,3 +24,6 @@ CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.domain_router', 'scripts_version' CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_policy','domain_id', 'BIGINT(20) DEFAULT NULL'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_policy','account_id', 'BIGINT(20) DEFAULT NULL'); + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule','domain_id', 'BIGINT(20) DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule','account_id', 'BIGINT(20) DEFAULT NULL'); diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 64d6e8b6929d..6a312644e97f 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -861,6 +861,7 @@ public SnapshotPolicyResponse createSnapshotPolicyResponse(SnapshotPolicy policy Volume vol = ApiDBUtils.findVolumeById(policy.getVolumeId()); if (vol != null) { policyResponse.setVolumeId(vol.getUuid()); + policyResponse.setVolumeName(vol.getName()); } policyResponse.setSchedule(policy.getSchedule()); policyResponse.setIntervalType(policy.getInterval()); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 72c5e481523b..4405381d4bec 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -638,16 +638,51 @@ protected int validateAndGetDefaultBackupRetentionIfRequired(Integer maxBackups, return maxBackups; } - @Override public List listBackupSchedule(ListBackupScheduleCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + boolean isRootAdmin = accountManager.isRootAdmin(caller.getId()); + Long id = cmd.getId(); Long vmId = cmd.getVmId(); + List permittedAccounts = new ArrayList<>(); + Long domainId = null; + Boolean isRecursive = null; + Project.ListProjectResourcesCriteria listProjectResourcesCriteria = null; + + if (!isRootAdmin) { + Ternary domainIdRecursiveListProject = + new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + accountManager.buildACLSearchParameters(caller, id, null, null, permittedAccounts, domainIdRecursiveListProject, true, false); + domainId = domainIdRecursiveListProject.first(); + isRecursive = domainIdRecursiveListProject.second(); + listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + } + + Filter searchFilter = new Filter(BackupScheduleVO.class, "id", false, null, null); + SearchBuilder searchBuilder = backupScheduleDao.createSearchBuilder(); + + if (!isRootAdmin) { + accountManager.buildACLSearchBuilder(searchBuilder, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + } + + searchBuilder.and("id", searchBuilder.entity().getId(), SearchCriteria.Op.EQ); if (vmId != null) { - final VMInstanceVO vm = findVmById(vmId); - validateBackupForZone(vm.getDataCenterId()); - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + searchBuilder.and("vmId", searchBuilder.entity().getVmId(), SearchCriteria.Op.EQ); + } + + SearchCriteria sc = searchBuilder.create(); + if (!isRootAdmin) { + accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + } + + if (id != null) { + sc.setParameters("id", id); + } + if (vmId != null) { + sc.setParameters("vmId", vmId); } - return backupScheduleDao.listByVM(vmId).stream().map(BackupSchedule.class::cast).collect(Collectors.toList()); + Pair, Integer> result = backupScheduleDao.searchAndCount(sc, searchFilter); + return new ArrayList<>(result.first()); } @Override diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 394de6ca6d26..411b412b35a6 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -451,6 +451,7 @@ "label.backup.repository": "Backup Repository", "label.backup.restore": "Restore Instance backup", "label.backuplimit": "Backup Limits", +"label.backup.schedules": "Backup Schedules", "label.backup.storage": "Backup Storage", "label.backupstoragelimit": "Backup Storage Limits (GiB)", "label.backupofferingid": "Backup Offering ID", @@ -742,6 +743,7 @@ "label.delete.asnrange": "Delete AS Range", "label.delete.autoscale.vmgroup": "Delete AutoScaling Group", "label.delete.backup": "Delete backup", +"label.delete.backup.schedule": "Delete backup schedule", "label.delete.bgp.peer": "Delete BGP peer", "label.delete.bigswitchbcf": "Remove BigSwitch BCF controller", "label.delete.brocadevcs": "Remove Brocade Vcs switch", @@ -1523,6 +1525,7 @@ "label.maxresolutiony": "Max. resolution Y", "label.maxsecondarystorage": "Max. secondary storage (GiB)", "label.maxsize": "Maximum size", +"label.maxsnaps": "Max. Snapshots", "label.maxsnapshot": "Max. Snapshots", "label.maxtemplate": "Max. Templates", "label.maxuservm": "Max. User Instances", @@ -2269,6 +2272,8 @@ "label.snapshot.name": "Snapshot name", "label.snapshotlimit": "Snapshot limits", "label.snapshotmemory": "Snapshot memory", +"label.snapshotpolicy": "Snapshot policy", +"label.snapshotpolicies": "Snapshot policies", "label.snapshots": "Volume Snapshots", "label.snapshottype": "Snapshot Type", "label.sockettimeout": "Socket timeout", @@ -2860,6 +2865,7 @@ "message.action.delete.autoscale.vmgroup": "Please confirm that you want to delete this autoscaling group.", "message.action.delete.backup.offering": "Please confirm that you want to delete this backup offering?", "message.action.delete.backup.repository": "Please confirm that you want to delete this backup repository?", +"message.action.delete.backup.schedule": "Please confirm that you want to delete this backup schedule?", "message.action.delete.cluster": "Please confirm that you want to delete this Cluster.", "message.action.delete.custom.action": "Please confirm that you want to delete this custom action.", "message.action.delete.domain": "Please confirm that you want to delete this domain.", @@ -2885,6 +2891,7 @@ "message.action.delete.secondary.storage": "Please confirm that you want to delete this secondary storage.", "message.action.delete.security.group": "Please confirm that you want to delete this security group.", "message.action.delete.snapshot": "Please confirm that you want to delete this Snapshot.", +"message.action.delete.snapshot.policy": "Please confirm that you want to delete the selected Snapshot Policy.", "message.action.delete.template": "Please confirm that you want to delete this Template.", "message.action.delete.tungsten.router.table": "Please confirm that you want to remove Route Table from this Network?", "message.action.delete.vgpu.profile": "Please confirm that you want to delete this vGPU profile.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 7829121fdb24..135ea7384fa5 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -161,6 +161,13 @@
{{ dataResource[item] }}
+ +
+ {{ $t('label.' + String(item).toLowerCase()) }} +
+
{{ dataResource[item] }}
+
+
{{ $t('label.' + item.replace('date', '.date.and.time'))}} diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index f4acba595b96..863bcec34536 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -233,11 +233,49 @@ >{{ $t(text.toLowerCase()) }} {{ text }} - + +