Skip to content

Commit 619f01e

Browse files
committed
Add support for backup policy listing without tying it to the vmid
1 parent 8f806ff commit 619f01e

File tree

11 files changed

+293
-21
lines changed

11 files changed

+293
-21
lines changed

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.apache.cloudstack.api.APICommand;
2525
import org.apache.cloudstack.api.ApiConstants;
2626
import org.apache.cloudstack.api.ApiErrorCode;
27-
import org.apache.cloudstack.api.BaseCmd;
27+
import org.apache.cloudstack.api.BaseListDomainResourcesCmd;
2828
import org.apache.cloudstack.api.Parameter;
2929
import org.apache.cloudstack.api.ServerApiException;
3030
import org.apache.cloudstack.api.response.BackupScheduleResponse;
@@ -39,7 +39,6 @@
3939
import com.cloud.exception.NetworkRuleConflictException;
4040
import com.cloud.exception.ResourceAllocationException;
4141
import com.cloud.exception.ResourceUnavailableException;
42-
import com.cloud.utils.exception.CloudRuntimeException;
4342

4443
import java.util.ArrayList;
4544
import java.util.List;
@@ -48,7 +47,7 @@
4847
description = "List backup schedule of a VM",
4948
responseObject = BackupScheduleResponse.class, since = "4.14.0",
5049
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
51-
public class ListBackupScheduleCmd extends BaseCmd {
50+
public class ListBackupScheduleCmd extends BaseListDomainResourcesCmd {
5251

5352
@Inject
5453
private BackupManager backupManager;
@@ -63,6 +62,12 @@ public class ListBackupScheduleCmd extends BaseCmd {
6362
description = "ID of the VM")
6463
private Long vmId;
6564

65+
@Parameter(name = ApiConstants.ID,
66+
type = CommandType.UUID,
67+
entityType = BackupScheduleResponse.class,
68+
description = "the ID of the backup schedule")
69+
private Long id;
70+
6671
/////////////////////////////////////////////////////
6772
/////////////////// Accessors ///////////////////////
6873
/////////////////////////////////////////////////////
@@ -71,6 +76,10 @@ public Long getVmId() {
7176
return vmId;
7277
}
7378

79+
public Long getId() {
80+
return id;
81+
}
82+
7483
/////////////////////////////////////////////////////
7584
/////////////// API Implementation///////////////////
7685
/////////////////////////////////////////////////////
@@ -81,16 +90,15 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE
8190
List<BackupSchedule> schedules = backupManager.listBackupSchedule(this);
8291
ListResponse<BackupScheduleResponse> response = new ListResponse<>();
8392
List<BackupScheduleResponse> scheduleResponses = new ArrayList<>();
93+
8494
if (!CollectionUtils.isNullOrEmpty(schedules)) {
8595
for (BackupSchedule schedule : schedules) {
8696
scheduleResponses.add(_responseGenerator.createBackupScheduleResponse(schedule));
8797
}
88-
response.setResponses(scheduleResponses, schedules.size());
89-
response.setResponseName(getCommandName());
90-
setResponseObject(response);
91-
} else {
92-
throw new CloudRuntimeException("No backup schedule exists for the VM");
9398
}
99+
response.setResponses(scheduleResponses, schedules.size());
100+
response.setResponseName(getCommandName());
101+
setResponseObject(response);
94102
} catch (Exception e) {
95103
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
96104
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public class SnapshotPolicyResponse extends BaseResponseWithTagInformation {
3737
@Param(description = "the ID of the disk volume")
3838
private String volumeId;
3939

40+
@SerializedName("volumename")
41+
@Param(description = "the name of the disk volume")
42+
private String volumeName;
43+
4044
@SerializedName("schedule")
4145
@Param(description = "time the snapshot is scheduled to be taken.")
4246
private String schedule;
@@ -87,6 +91,10 @@ public void setVolumeId(String volumeId) {
8791
this.volumeId = volumeId;
8892
}
8993

94+
public void setVolumeName(String volumeName) {
95+
this.volumeName = volumeName;
96+
}
97+
9098
public String getSchedule() {
9199
return schedule;
92100
}

engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42100to42200.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public InputStream[] getPrepareScripts() {
5050
@Override
5151
public void performDataMigration(Connection conn) {
5252
updateSnapshotPolicyOwnership(conn);
53+
updateBackupScheduleOwnership(conn);
5354
}
5455

5556
private void updateSnapshotPolicyOwnership(Connection conn) {
@@ -76,5 +77,27 @@ private void updateSnapshotPolicyOwnership(Connection conn) {
7677
}
7778
}
7879

80+
private void updateBackupScheduleOwnership(Connection conn) {
81+
// Set account_id and domain_id in backup_schedule table from vm_instance table
82+
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)";
83+
String updateSql = "UPDATE backup_schedule SET account_id = ?, domain_id = ? WHERE id = ?";
7984

85+
try (PreparedStatement selectPstmt = conn.prepareStatement(selectSql);
86+
ResultSet rs = selectPstmt.executeQuery();
87+
PreparedStatement updatePstmt = conn.prepareStatement(updateSql)) {
88+
89+
while (rs.next()) {
90+
long scheduleId = rs.getLong(1);
91+
long accountId = rs.getLong(2);
92+
long domainId = rs.getLong(3);
93+
94+
updatePstmt.setLong(1, accountId);
95+
updatePstmt.setLong(2, domainId);
96+
updatePstmt.setLong(3, scheduleId);
97+
updatePstmt.executeUpdate();
98+
}
99+
} catch (SQLException e) {
100+
throw new CloudRuntimeException("Unable to update backup_schedule table with account_id and domain_id", e);
101+
}
102+
}
80103
}

engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.domain_router', 'scripts_version'
2424

2525
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_policy','domain_id', 'BIGINT(20) DEFAULT NULL');
2626
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_policy','account_id', 'BIGINT(20) DEFAULT NULL');
27+
28+
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule','domain_id', 'BIGINT(20) DEFAULT NULL');
29+
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule','account_id', 'BIGINT(20) DEFAULT NULL');

server/src/main/java/com/cloud/api/ApiResponseHelper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ public SnapshotPolicyResponse createSnapshotPolicyResponse(SnapshotPolicy policy
861861
Volume vol = ApiDBUtils.findVolumeById(policy.getVolumeId());
862862
if (vol != null) {
863863
policyResponse.setVolumeId(vol.getUuid());
864+
policyResponse.setVolumeName(vol.getName());
864865
}
865866
policyResponse.setSchedule(policy.getSchedule());
866867
policyResponse.setIntervalType(policy.getInterval());

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -638,16 +638,51 @@ protected int validateAndGetDefaultBackupRetentionIfRequired(Integer maxBackups,
638638
return maxBackups;
639639
}
640640

641-
@Override
642641
public List<BackupSchedule> listBackupSchedule(ListBackupScheduleCmd cmd) {
642+
Account caller = CallContext.current().getCallingAccount();
643+
boolean isRootAdmin = accountManager.isRootAdmin(caller.getId());
644+
Long id = cmd.getId();
643645
Long vmId = cmd.getVmId();
646+
List<Long> permittedAccounts = new ArrayList<>();
647+
Long domainId = null;
648+
Boolean isRecursive = null;
649+
Project.ListProjectResourcesCriteria listProjectResourcesCriteria = null;
650+
651+
if (!isRootAdmin) {
652+
Ternary<Long, Boolean, Project.ListProjectResourcesCriteria> domainIdRecursiveListProject =
653+
new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null);
654+
accountManager.buildACLSearchParameters(caller, id, null, null, permittedAccounts, domainIdRecursiveListProject, true, false);
655+
domainId = domainIdRecursiveListProject.first();
656+
isRecursive = domainIdRecursiveListProject.second();
657+
listProjectResourcesCriteria = domainIdRecursiveListProject.third();
658+
}
659+
660+
Filter searchFilter = new Filter(BackupScheduleVO.class, "id", false, null, null);
661+
SearchBuilder<BackupScheduleVO> searchBuilder = backupScheduleDao.createSearchBuilder();
662+
663+
if (!isRootAdmin) {
664+
accountManager.buildACLSearchBuilder(searchBuilder, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
665+
}
666+
667+
searchBuilder.and("id", searchBuilder.entity().getId(), SearchCriteria.Op.EQ);
644668
if (vmId != null) {
645-
final VMInstanceVO vm = findVmById(vmId);
646-
validateBackupForZone(vm.getDataCenterId());
647-
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm);
669+
searchBuilder.and("vmId", searchBuilder.entity().getVmId(), SearchCriteria.Op.EQ);
670+
}
671+
672+
SearchCriteria<BackupScheduleVO> sc = searchBuilder.create();
673+
if (!isRootAdmin) {
674+
accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
675+
}
676+
677+
if (id != null) {
678+
sc.setParameters("id", id);
679+
}
680+
if (vmId != null) {
681+
sc.setParameters("vmId", vmId);
648682
}
649683

650-
return backupScheduleDao.listByVM(vmId).stream().map(BackupSchedule.class::cast).collect(Collectors.toList());
684+
Pair<List<BackupScheduleVO>, Integer> result = backupScheduleDao.searchAndCount(sc, searchFilter);
685+
return new ArrayList<>(result.first());
651686
}
652687

653688
@Override

ui/public/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@
451451
"label.backup.repository": "Backup Repository",
452452
"label.backup.restore": "Restore Instance backup",
453453
"label.backuplimit": "Backup Limits",
454+
"label.backup.schedules": "Backup Schedules",
454455
"label.backup.storage": "Backup Storage",
455456
"label.backupstoragelimit": "Backup Storage Limits (GiB)",
456457
"label.backupofferingid": "Backup Offering ID",
@@ -742,6 +743,7 @@
742743
"label.delete.asnrange": "Delete AS Range",
743744
"label.delete.autoscale.vmgroup": "Delete AutoScaling Group",
744745
"label.delete.backup": "Delete backup",
746+
"label.delete.backup.schedule": "Delete backup schedule",
745747
"label.delete.bgp.peer": "Delete BGP peer",
746748
"label.delete.bigswitchbcf": "Remove BigSwitch BCF controller",
747749
"label.delete.brocadevcs": "Remove Brocade Vcs switch",
@@ -1523,6 +1525,7 @@
15231525
"label.maxresolutiony": "Max. resolution Y",
15241526
"label.maxsecondarystorage": "Max. secondary storage (GiB)",
15251527
"label.maxsize": "Maximum size",
1528+
"label.maxsnaps": "Max. Snapshots",
15261529
"label.maxsnapshot": "Max. Snapshots",
15271530
"label.maxtemplate": "Max. Templates",
15281531
"label.maxuservm": "Max. User Instances",
@@ -2269,6 +2272,8 @@
22692272
"label.snapshot.name": "Snapshot name",
22702273
"label.snapshotlimit": "Snapshot limits",
22712274
"label.snapshotmemory": "Snapshot memory",
2275+
"label.snapshotpolicy": "Snapshot policy",
2276+
"label.snapshotpolicies": "Snapshot policies",
22722277
"label.snapshots": "Volume Snapshots",
22732278
"label.snapshottype": "Snapshot Type",
22742279
"label.sockettimeout": "Socket timeout",
@@ -2860,6 +2865,7 @@
28602865
"message.action.delete.autoscale.vmgroup": "Please confirm that you want to delete this autoscaling group.",
28612866
"message.action.delete.backup.offering": "Please confirm that you want to delete this backup offering?",
28622867
"message.action.delete.backup.repository": "Please confirm that you want to delete this backup repository?",
2868+
"message.action.delete.backup.schedule": "Please confirm that you want to delete this backup schedule?",
28632869
"message.action.delete.cluster": "Please confirm that you want to delete this Cluster.",
28642870
"message.action.delete.custom.action": "Please confirm that you want to delete this custom action.",
28652871
"message.action.delete.domain": "Please confirm that you want to delete this domain.",
@@ -2885,6 +2891,7 @@
28852891
"message.action.delete.secondary.storage": "Please confirm that you want to delete this secondary storage.",
28862892
"message.action.delete.security.group": "Please confirm that you want to delete this security group.",
28872893
"message.action.delete.snapshot": "Please confirm that you want to delete this Snapshot.",
2894+
"message.action.delete.snapshot.policy": "Please confirm that you want to delete the selected Snapshot Policy.",
28882895
"message.action.delete.template": "Please confirm that you want to delete this Template.",
28892896
"message.action.delete.tungsten.router.table": "Please confirm that you want to remove Route Table from this Network?",
28902897
"message.action.delete.vgpu.profile": "Please confirm that you want to delete this vGPU profile.",

ui/src/components/view/DetailsTab.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@
161161
<div>{{ dataResource[item] }}</div>
162162
</div>
163163
</a-list-item>
164+
<a-list-item v-else-if="(item === 'zoneid' && $route.path.includes('/snapshotpolicy'))">
165+
<div>
166+
<strong>{{ $t('label.' + String(item).toLowerCase()) }}</strong>
167+
<br/>
168+
<div>{{ dataResource[item] }}</div>
169+
</div>
170+
</a-list-item>
164171
<a-list-item v-else-if="['startdate', 'enddate'].includes(item)">
165172
<div>
166173
<strong>{{ $t('label.' + item.replace('date', '.date.and.time'))}}</strong>

ui/src/components/view/ListView.vue

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,49 @@
233233
>{{ $t(text.toLowerCase()) }}</span>
234234
<span v-else>{{ text }}</span>
235235
</template>
236-
237236
<template v-if="column.key === 'schedule'">
238-
{{ text }}
239-
<br />
240-
({{ generateHumanReadableSchedule(text) }})
237+
<div v-if="['/snapshotpolicy', '/backupschedule'].some(path => $route.path.endsWith(path))">
238+
<label class="interval-content">
239+
<span v-if="record.intervaltype===0">{{ record.schedule + $t('label.min.past.hour') }}</span>
240+
<span v-else>{{ record.schedule.split(':')[1] + ':' + record.schedule.split(':')[0] }}</span>
241+
</label>
242+
<span v-if="record.intervaltype===2">
243+
{{ ` ${$t('label.every')} ${$t(listDayOfWeek[record.schedule.split(':')[2] - 1])}` }}
244+
</span>
245+
<span v-else-if="record.intervaltype===3">
246+
{{ ` ${$t('label.day')} ${record.schedule.split(':')[2]} ${$t('label.of.month')}` }}
247+
</span>
248+
</div>
249+
<div v-else>
250+
{{ text }}
251+
<br />
252+
({{ generateHumanReadableSchedule(text) }})
253+
</div>
254+
</template>
255+
<template v-if="column.key === 'intervaltype' && ['/snapshotpolicy', '/backupschedule'].some(path => $route.path.endsWith(path))">
256+
<QuickView
257+
style="margin-right: 8px"
258+
:actions="actions"
259+
:resource="record"
260+
:enabled="quickViewEnabled() && actions.length > 0"
261+
@exec-action="$parent.execAction"
262+
/>
263+
<span v-if="record.intervaltype===0">
264+
<clock-circle-outlined />
265+
</span>
266+
<span class="custom-icon icon-daily" v-else-if="record.intervaltype===1">
267+
<calendar-outlined />
268+
</span>
269+
<span class="custom-icon icon-weekly" v-else-if="record.intervaltype===2">
270+
<calendar-outlined />
271+
</span>
272+
<span class="custom-icon icon-monthly" v-else-if="record.intervaltype===3">
273+
<calendar-outlined />
274+
</span>
275+
{{ getIntervalTypeText(record.intervaltype) }}
276+
</template>
277+
<template v-if="column.key === 'timezone'">
278+
<label>{{ getTimeZone(record.timezone) }}</label>
241279
</template>
242280
<template v-if="column.key === 'displayname'">
243281
<QuickView
@@ -1007,6 +1045,7 @@ import { createPathBasedOnVmType } from '@/utils/plugins'
10071045
import { validateLinks } from '@/utils/links'
10081046
import cronstrue from 'cronstrue/i18n'
10091047
import moment from 'moment-timezone'
1048+
import { timeZoneName } from '@/utils/timezone'
10101049
import { FileTextOutlined } from '@ant-design/icons-vue'
10111050
10121051
export default {
@@ -1107,7 +1146,8 @@ export default {
11071146
}
11081147
},
11091148
usageTypeMap: {},
1110-
resourceIdToValidLinksMap: {}
1149+
resourceIdToValidLinksMap: {},
1150+
listDayOfWeek: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
11111151
}
11121152
},
11131153
watch: {
@@ -1144,7 +1184,7 @@ export default {
11441184
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
11451185
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
11461186
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', '/quotatariff', '/sharedfs',
1147-
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension'].join('|'))
1187+
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule'].join('|'))
11481188
.test(this.$route.path)
11491189
},
11501190
enableGroupAction () {
@@ -1158,6 +1198,13 @@ export default {
11581198
getDateAtTimeZone (date, timezone) {
11591199
return date ? moment(date).tz(timezone).format('YYYY-MM-DD HH:mm:ss') : null
11601200
},
1201+
getIntervalTypeText (intervaltype) {
1202+
const types = { 0: 'HOURLY', 1: 'DAILY', 2: 'WEEKLY', 3: 'MONTHLY' }
1203+
return types[intervaltype] || intervaltype
1204+
},
1205+
getTimeZone (timeZone) {
1206+
return timeZoneName(timeZone)
1207+
},
11611208
fetchColumns () {
11621209
if (this.isOrderUpdatable()) {
11631210
return this.columns
@@ -1559,4 +1606,31 @@ export default {
15591606
color: #f50000;
15601607
padding: 10%;
15611608
}
1609+
1610+
.custom-icon:before {
1611+
font-size: 8px;
1612+
position: absolute;
1613+
top: 8px;
1614+
left: 3.5px;
1615+
color: #000;
1616+
font-weight: 700;
1617+
line-height: 1.7;
1618+
}
1619+
1620+
.icon-daily:before {
1621+
content: "01";
1622+
left: 5px;
1623+
top: 7px;
1624+
line-height: 1.9;
1625+
}
1626+
1627+
.icon-weekly:before {
1628+
content: "1-7";
1629+
left: 3px;
1630+
line-height: 1.7;
1631+
}
1632+
1633+
.icon-monthly:before {
1634+
content: "***";
1635+
}
15621636
</style>

0 commit comments

Comments
 (0)