This is an automated email from the ASF dual-hosted git repository. weizhou pushed a commit to branch 4.19 in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit b8904f75ddf919cacbd5a00b38f3cbc24e96dbb4 Merge: c0643a8f6e4 b2e29931e89 Author: Wei Zhou <[email protected]> AuthorDate: Mon Feb 5 10:08:31 2024 +0100 Merge remote-tracking branch 'apache/4.18' into 4.19 .../cloudstack/api/command/user/vm/ListVMsCmd.java | 4 +- .../backup/PrepareForBackupRestorationCommand.java | 43 +++ .../org/apache/cloudstack/backup/BackupVO.java | 13 + plugins/backup/veeam/pom.xml | 15 + .../cloudstack/backup/VeeamBackupProvider.java | 70 ++++- .../cloudstack/backup/veeam/VeeamClient.java | 303 ++++++++++++++++--- .../cloudstack/backup/veeam/api/BackupFile.java | 160 ++++++++++ .../cloudstack/backup/veeam/api/BackupFiles.java | 39 +++ .../backup/veeam/api/VmRestorePoint.java | 149 ++++++++++ .../backup/veeam/api/VmRestorePoints.java | 39 +++ .../cloudstack/backup/veeam/VeeamClientTest.java | 329 ++++++++++++++++++++- .../cloudstack/utils/cryptsetup/CryptSetup.java | 2 +- .../java/com/cloud/hypervisor/guru/VMwareGuru.java | 23 +- .../hypervisor/vmware/resource/VmwareResource.java | 32 ++ .../resource/VmwareStorageLayoutHelper.java | 21 +- .../storage/resource/VmwareStorageProcessor.java | 23 +- .../com/cloud/server/ConfigurationServerImpl.java | 2 +- .../cloudstack/backup/BackupManagerImpl.java | 29 +- systemvm/agent/scripts/consoleproxy.sh | 33 --- systemvm/agent/scripts/secstorage.sh | 33 --- .../debian/opt/cloud/bin/setup/consoleproxy.sh | 2 +- .../smoke/test_backup_recovery_veeam.py | 308 +++++++++++++++++++ tools/marvin/marvin/lib/base.py | 66 ++++- ui/public/locales/en.json | 2 + ui/src/components/view/ListView.vue | 2 +- ui/src/config/section/compute.js | 4 +- ui/src/config/section/storage.js | 6 +- ui/src/views/compute/backup/BackupSchedule.vue | 8 +- ui/src/views/network/AclListRulesTab.vue | 8 +- .../hypervisor/vmware/mo/VirtualMachineMO.java | 25 ++ .../hypervisor/vmware/mo/VmdkFileDescriptor.java | 59 ++++ 31 files changed, 1682 insertions(+), 170 deletions(-) diff --cc engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java index e5582609d68,2ecbfd56460..3e5db0443d8 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java @@@ -52,9 -52,11 +54,12 @@@ public class BackupVO implements Backu private String backupType; @Column(name = "date") - private String date; + @Temporal(value = TemporalType.DATE) + private Date date; + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + @Column(name = "size") private Long size; diff --cc plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index c0091e47061,5c96e4b7057..e20f67995b9 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@@ -40,11 -41,17 +41,17 @@@ import org.apache.commons.collections.C import org.apache.commons.lang3.BooleanUtils; import org.apache.log4j.Logger; + import com.cloud.agent.AgentManager; + import com.cloud.agent.api.Answer; + import com.cloud.event.ActionEventUtils; + import com.cloud.event.EventTypes; + import com.cloud.event.EventVO; import com.cloud.hypervisor.Hypervisor; -import com.cloud.hypervisor.vmware.VmwareDatacenter; +import com.cloud.dc.VmwareDatacenter; import com.cloud.hypervisor.vmware.VmwareDatacenterZoneMap; -import com.cloud.hypervisor.vmware.dao.VmwareDatacenterDao; +import com.cloud.dc.dao.VmwareDatacenterDao; import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao; + import com.cloud.user.User; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.db.Transaction; diff --cc plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java index a01ddcc81ce,22d0a796e14..408904f1d29 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@@ -48,18 -48,10 +48,19 @@@ import java.util.stream.Collectors import javax.naming.ConfigurationException; import javax.xml.datatype.XMLGregorianCalendar; +import com.cloud.hypervisor.vmware.mo.HostDatastoreBrowserMO; +import com.vmware.vim25.FileInfo; +import com.vmware.vim25.FileQueryFlags; +import com.vmware.vim25.FolderFileInfo; +import com.vmware.vim25.HostDatastoreBrowserSearchResults; +import com.vmware.vim25.HostDatastoreBrowserSearchSpec; +import com.vmware.vim25.VirtualMachineConfigSummary; import org.apache.cloudstack.api.ApiConstants; + import org.apache.cloudstack.backup.PrepareForBackupRestorationCommand; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.StorageSubSystemCommand; +import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsAnswer; +import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsCommand; import org.apache.cloudstack.storage.configdrive.ConfigDrive; import org.apache.cloudstack.storage.resource.NfsSecondaryStorageResource; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; @@@ -605,12 -607,8 +606,14 @@@ public class VmwareResource extends Ser answer = execute((GetVmVncTicketCommand) cmd); } else if (clz == GetAutoScaleMetricsCommand.class) { answer = execute((GetAutoScaleMetricsCommand) cmd); + } else if (clz == CheckGuestOsMappingCommand.class) { + answer = execute((CheckGuestOsMappingCommand) cmd); + } else if (clz == GetHypervisorGuestOsNamesCommand.class) { + answer = execute((GetHypervisorGuestOsNamesCommand) cmd); + } else if (clz == ListDataStoreObjectsCommand.class) { + answer = execute((ListDataStoreObjectsCommand) cmd); + } else if (clz == PrepareForBackupRestorationCommand.class) { + answer = execute((PrepareForBackupRestorationCommand) cmd); } else { answer = Answer.createUnsupportedCommandAnswer(cmd); } @@@ -7499,129 -7754,35 +7502,158 @@@ } } + protected CheckGuestOsMappingAnswer execute(CheckGuestOsMappingCommand cmd) { + String guestOsName = cmd.getGuestOsName(); + String guestOsMappingName = cmd.getGuestOsHypervisorMappingName(); + s_logger.info("Checking guest os mapping name: " + guestOsMappingName + " for the guest os: " + guestOsName + " in the hypervisor"); + try { + VmwareContext context = getServiceContext(); + VmwareHypervisorHost hyperHost = getHyperHost(context); + GuestOsDescriptor guestOsDescriptor = hyperHost.getGuestOsDescriptor(guestOsMappingName); + if (guestOsDescriptor == null) { + return new CheckGuestOsMappingAnswer(cmd, "Guest os mapping name: " + guestOsMappingName + " not found in the hypervisor"); + } + s_logger.debug("Matching hypervisor guest os - id: " + guestOsDescriptor.getId() + ", full name: " + guestOsDescriptor.getFullName() + ", family: " + guestOsDescriptor.getFamily()); + if (guestOsDescriptor.getFullName().equalsIgnoreCase(guestOsName)) { + s_logger.debug("Hypervisor guest os name in the descriptor matches with os name: " + guestOsName); + } + s_logger.info("Hypervisor guest os name in the descriptor matches with os mapping: " + guestOsMappingName + " from user"); + return new CheckGuestOsMappingAnswer(cmd); + } catch (Exception e) { + s_logger.error("Failed to check the hypervisor guest os mapping name: " + guestOsMappingName, e); + return new CheckGuestOsMappingAnswer(cmd, e.getLocalizedMessage()); + } + } + + protected ListDataStoreObjectsAnswer execute(ListDataStoreObjectsCommand cmd) { + String path = cmd.getPath(); + int startIndex = cmd.getStartIndex(); + int pageSize = cmd.getPageSize(); + PrimaryDataStoreTO dataStore = (PrimaryDataStoreTO) cmd.getStore(); + + if (path.startsWith("/")) { + path = path.substring(1); + } + + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + VmwareContext context = getServiceContext(); + VmwareHypervisorHost hyperHost = getHyperHost(context); + ManagedObjectReference morDatastore = null; + + int count = 0; + List<String> names = new ArrayList<>(); + List<String> paths = new ArrayList<>(); + List<String> absPaths = new ArrayList<>(); + List<Boolean> isDirs = new ArrayList<>(); + List<Long> sizes = new ArrayList<>(); + List<Long> modifiedList = new ArrayList<>(); + + try { + morDatastore = HypervisorHostHelper.findDatastoreWithBackwardsCompatibility(hyperHost, dataStore.getUuid()); + + DatastoreMO dsMo = new DatastoreMO(context, morDatastore); + HostDatastoreBrowserMO browserMo = dsMo.getHostDatastoreBrowserMO(); + FileQueryFlags fqf = new FileQueryFlags(); + fqf.setFileSize(true); + fqf.setFileType(true); + fqf.setModification(true); + fqf.setFileOwner(false); + + HostDatastoreBrowserSearchSpec spec = new HostDatastoreBrowserSearchSpec(); + spec.setSearchCaseInsensitive(true); + spec.setDetails(fqf); + + String dsPath = String.format("[%s] %s", dsMo.getName(), path); + + HostDatastoreBrowserSearchResults results = browserMo.searchDatastore(dsPath, spec); + List<FileInfo> fileInfoList = results.getFile(); + count = fileInfoList.size(); + for (int i = startIndex; i < startIndex + pageSize && i < count; i++) { + FileInfo file = fileInfoList.get(i); + + names.add(file.getPath()); + paths.add(path + "/" + file.getPath()); + absPaths.add(dsPath + "/" + file.getPath()); + isDirs.add(file instanceof FolderFileInfo); + sizes.add(file.getFileSize()); + modifiedList.add(file.getModification().toGregorianCalendar().getTimeInMillis()); + } + + return new ListDataStoreObjectsAnswer(true, count, names, paths, absPaths, isDirs, sizes, modifiedList); + } catch (Exception e) { + if (e.getMessage().contains("was not found")) { + return new ListDataStoreObjectsAnswer(false, count, names, paths, absPaths, isDirs, sizes, modifiedList); + } + String errorMsg = String.format("Failed to list files at path [%s] due to: [%s].", path, e.getMessage()); + s_logger.error(errorMsg, e); + } + + return null; + } + + protected GetHypervisorGuestOsNamesAnswer execute(GetHypervisorGuestOsNamesCommand cmd) { + String keyword = cmd.getKeyword(); + s_logger.info("Getting guest os names in the hypervisor"); + try { + VmwareContext context = getServiceContext(); + VmwareHypervisorHost hyperHost = getHyperHost(context); + List<GuestOsDescriptor> guestOsDescriptors = hyperHost.getGuestOsDescriptors(); + if (guestOsDescriptors == null) { + return new GetHypervisorGuestOsNamesAnswer(cmd, "Guest os names not found in the hypervisor"); + } + List<Pair<String, String>> hypervisorGuestOsNames = new ArrayList<>(); + for (GuestOsDescriptor guestOsDescriptor : guestOsDescriptors) { + String osDescriptorFullName = guestOsDescriptor.getFullName(); + String osDescriptorId = guestOsDescriptor.getId(); + if (StringUtils.isNotBlank(keyword)) { + if (osDescriptorFullName.toLowerCase().contains(keyword.toLowerCase()) || osDescriptorId.toLowerCase().contains(keyword.toLowerCase())) { + Pair<String, String> hypervisorGuestOs = new Pair<>(osDescriptorFullName, osDescriptorId); + hypervisorGuestOsNames.add(hypervisorGuestOs); + } + } else { + Pair<String, String> hypervisorGuestOs = new Pair<>(osDescriptorFullName, osDescriptorId); + hypervisorGuestOsNames.add(hypervisorGuestOs); + } + } + return new GetHypervisorGuestOsNamesAnswer(cmd, hypervisorGuestOsNames); + } catch (Exception e) { + s_logger.error("Failed to get the hypervisor guest names due to: " + e.getLocalizedMessage(), e); + return new GetHypervisorGuestOsNamesAnswer(cmd, e.getLocalizedMessage()); + } + } + + private Answer execute(PrepareForBackupRestorationCommand command) { + try { + VmwareHypervisorHost hyperHost = getHyperHost(getServiceContext()); + + String vmName = command.getVmName(); + VirtualMachineMO vmMo = hyperHost.findVmOnHyperHost(vmName); + + if (vmMo == null) { + if (hyperHost instanceof HostMO) { + ClusterMO clusterMo = new ClusterMO(hyperHost.getContext(), ((HostMO) hyperHost).getParentMor()); + vmMo = clusterMo.findVmOnHyperHost(vmName); + } + } + + if (vmMo == null) { + String msg = "VM " + vmName + " no longer exists to execute PrepareForBackupRestorationCommand command"; + s_logger.error(msg); + throw new Exception(msg); + } + + vmMo.removeChangeTrackPathFromVmdkForDisks(); + + return new Answer(command, true, "success"); + } catch (Exception e) { + s_logger.error("Unexpected exception: ", e); + return new Answer(command, false, "Unable to execute PrepareForBackupRestorationCommand due to " + e.toString()); + } + } + private Integer getVmwareWindowTimeInterval() { Integer windowInterval = VmwareManager.VMWARE_STATS_TIME_WINDOW.value(); if (windowInterval == null || windowInterval < 20) { diff --cc server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index ddcb15f6151,bbdf730e06d..2e45066ff60 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@@ -607,88 -621,6 +624,96 @@@ public class BackupManagerImpl extends vm.getInstanceName(), vm.getHypervisorType(), backup); } + /** + * Tries to restore a VM from a backup. <br/> + * First update the VM state to {@link VirtualMachine.Event#RestoringRequested} and its volume states to {@link Volume.Event#RestoreRequested}, <br/> + * and then try to restore the backup. <br/> + * + * If restore fails, then update the VM state to {@link VirtualMachine.Event#RestoringFailed}, and its volumes to {@link Volume.Event#RestoreFailed} and throw an {@link CloudRuntimeException}. + */ + protected void tryRestoreVM(BackupVO backup, VMInstanceVO vm, BackupOffering offering, String backupDetailsInMessage) { + try { + updateVmState(vm, VirtualMachine.Event.RestoringRequested, VirtualMachine.State.Restoring); + updateVolumeState(vm, Volume.Event.RestoreRequested, Volume.State.Restoring); ++ ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventTypes.EVENT_VM_BACKUP_RESTORE, ++ String.format("Restoring VM %s from backup %s", vm.getUuid(), backup.getUuid()), ++ vm.getId(), ApiCommandResourceType.VirtualMachine.toString(), ++ true, 0); ++ + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + if (!backupProvider.restoreVMFromBackup(vm, backup)) { - throw new CloudRuntimeException(String.format("Error restoring %s from backup [%s].", vm, backupDetailsInMessage)); ++ ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, vm.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_BACKUP_RESTORE, ++ String.format("Failed to restore VM %s from backup %s", vm.getInstanceName(), backup.getUuid()), ++ vm.getId(), ApiCommandResourceType.VirtualMachine.toString(),0); ++ throw new CloudRuntimeException("Error restoring VM from backup with uuid " + backup.getUuid()); + } + // The restore process is executed by a backup provider outside of ACS, I am using the catch-all (Exception) to + // ensure that no provider-side exception is missed. Therefore, we have a proper handling of exceptions, and rollbacks if needed. + } catch (Exception e) { + LOG.error(String.format("Failed to restore backup [%s] due to: [%s].", backupDetailsInMessage, e.getMessage()), e); + updateVolumeState(vm, Volume.Event.RestoreFailed, Volume.State.Ready); + updateVmState(vm, VirtualMachine.Event.RestoringFailed, VirtualMachine.State.Stopped); + throw new CloudRuntimeException(String.format("Error restoring VM from backup [%s].", backupDetailsInMessage)); + } + } + + /** + * Tries to update the state of given VM, given specified event + * @param vm The VM to update its state + * @param event The event to update the VM state + * @param next The desired state, just needed to add more context to the logs + */ + private void updateVmState(VMInstanceVO vm, VirtualMachine.Event event, VirtualMachine.State next) { + LOG.debug(String.format("Trying to update state of VM [%s] with event [%s].", vm, event)); + Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback<VMInstanceVO>) status -> { + try { + if (!virtualMachineManager.stateTransitTo(vm, event, vm.getHostId())) { + throw new CloudRuntimeException(String.format("Unable to change state of VM [%s] to [%s].", vm, next)); + } + } catch (NoTransitionException e) { + String errMsg = String.format("Failed to update state of VM [%s] with event [%s] due to [%s].", vm, event, e.getMessage()); + LOG.error(errMsg, e); + throw new RuntimeException(errMsg); + } + return null; + }); + } + + /** + * Tries to update all volume states of given VM, given specified event + * @param vm The VM to which the volumes belong + * @param event The event to update the volume states + * @param next The desired state, just needed to add more context to the logs + */ + private void updateVolumeState(VMInstanceVO vm, Volume.Event event, Volume.State next) { + Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback<VolumeVO>) status -> { + for (VolumeVO volume : volumeDao.findIncludingRemovedByInstanceAndType(vm.getId(), null)) { + tryToUpdateStateOfSpecifiedVolume(volume, event, next); + } + return null; + }); + } + + /** + * Tries to update the state of just one volume using any passed {@link Volume.Event}. Throws an {@link RuntimeException} when fails. + * @param volume The volume to update it state + * @param event The event to update the volume state + * @param next The desired state, just needed to add more context to the logs + * + */ + private void tryToUpdateStateOfSpecifiedVolume(VolumeVO volume, Volume.Event event, Volume.State next) { + LOG.debug(String.format("Trying to update state of volume [%s] with event [%s].", volume, event)); + try { + if (!volumeApiService.stateTransitTo(volume, event)) { + throw new CloudRuntimeException(String.format("Unable to change state of volume [%s] to [%s].", volume, next)); + } + } catch (NoTransitionException e) { + String errMsg = String.format("Failed to update state of volume [%s] with event [%s] due to [%s].", volume, event, e.getMessage()); + LOG.error(errMsg, e); + throw new RuntimeException(errMsg); + } + } + private Backup.VolumeInfo getVolumeInfo(List<Backup.VolumeInfo> backedUpVolumes, String volumeUuid) { for (Backup.VolumeInfo volInfo : backedUpVolumes) { if (volInfo.getUuid().equals(volumeUuid)) { diff --cc ui/public/locales/en.json index b86e39aba7b,9a14bef74fa..71da3c6d0aa --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@@ -2532,13 -2349,14 +2532,14 @@@ "message.adding.host": "Adding host", "message.adding.netscaler.device": "Adding Netscaler device", "message.adding.netscaler.provider": "Adding Netscaler provider", -"message.advanced.security.group": "Choose this if you wish to use security groups to provide guest VM isolation.", +"message.advanced.security.group": "Choose this if you wish to use security groups to provide guest Instance isolation.", "message.allowed": "Allowed", + "message.alert.show.all.stats.data": "This may return a lot of data depending on VM statistics and retention settings", "message.apply.success": "Apply Successfully", -"message.assign.instance.another": "Please specify the account type, domain, account name and network (optional) of the new account. <br> If the default nic of the vm is on a shared network, CloudStack will check if the network can be used by the new account if you do not specify one network. <br> If the default nic of the vm is on a isolated network, and the new account has more one isolated networks, you should specify one.", -"message.assign.vm.failed": "Failed to assign VM", -"message.assign.vm.processing": "Assigning VM...", -"message.attach.volume": "Please fill in the following data to attach a new volume. If you are attaching a disk volume to a Windows based virtual machine, you will need to reboot the instance to see the attached disk.", +"message.assign.instance.another": "Please specify the Account type, domain, Account name and Network (optional) of the new Account. <br> If the default NIC of the Instance is on a shared Network, CloudStack will check if the Network can be used by the new Account if you do not specify one Network. <br> If the default NIC of the Instance is on a isolated Network, and the new Account has more one isolated Networks, you should specify one.", +"message.assign.vm.failed": "Failed to assign Instance", +"message.assign.vm.processing": "Assigning Instance...", +"message.attach.volume": "Please fill in the following data to attach a new volume. If you are attaching a disk volume to a Windows based Instance, you will need to reboot the Instance to see the attached disk.", "message.attach.volume.failed": "Failed to attach volume.", "message.attach.volume.progress": "Attaching volume", "message.attach.volume.success": "Successfully attached the volume to the instance", diff --cc ui/src/components/view/ListView.vue index 093e7d663a0,1afeae9c4a1..2beec672a3c --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@@ -600,14 -577,11 +600,14 @@@ export default }, enableGroupAction () { return ['vm', 'alert', 'vmgroup', 'ssh', 'userdata', 'affinitygroup', 'autoscalevmgroup', 'volume', 'snapshot', - 'vmsnapshot', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'vnfapp', - 'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', ++ 'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'vnfapp', 'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering', - 'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment' + 'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets' ].includes(this.$route.name) }, + getDateAtTimeZone (date, timezone) { + return date ? moment(date).tz(timezone).format('YYYY-MM-DD HH:mm:ss') : null + }, fetchColumns () { if (this.isOrderUpdatable()) { return this.columns diff --cc ui/src/config/section/compute.js index 4cb9ed8e2ba,0ef53012ba0..f189b48d56f --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@@ -31,11 -32,10 +31,11 @@@ export default permission: ['listVirtualMachinesMetrics'], resourceType: 'UserVm', params: () => { - var params = { details: 'servoff,tmpl,nics' } + var params = { details: 'servoff,tmpl,nics,backoff' } if (store.getters.metrics) { - params = { details: 'servoff,tmpl,nics,stats' } + params = { details: 'servoff,tmpl,nics,backoff,stats' } } + params.isvnf = false return params }, filters: () => { diff --cc ui/src/config/section/storage.js index a096067b135,d73b989f74e..3493232da45 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@@ -446,12 -420,21 +446,16 @@@ export default } }, { - api: 'deleteVMSnapshot', + api: 'deleteBackup', icon: 'delete-outlined', - label: 'label.action.vmsnapshot.delete', - message: 'message.action.vmsnapshot.delete', + label: 'label.delete.backup', + message: 'message.delete.backup', dataView: true, - show: (record) => { return record.state !== 'Destroyed' } - show: (record) => { return ['Ready', 'Expunging', 'Error'].includes(record.state) }, - args: ['vmsnapshotid'], - mapping: { - vmsnapshotid: { - value: (record) => { return record.id } - } - }, ++ show: (record) => { return record.state !== 'Destroyed' }, + groupAction: true, + popup: true, - groupMap: (selection) => { return selection.map(x => { return { vmsnapshotid: x } }) } ++ groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }, ++ args: ['forced'] } ] }, diff --cc ui/src/views/compute/backup/BackupSchedule.vue index 26c655a5be1,32da2d440a7..ffa53aa8b2a --- a/ui/src/views/compute/backup/BackupSchedule.vue +++ b/ui/src/views/compute/backup/BackupSchedule.vue @@@ -24,51 -24,52 +24,54 @@@ :rowKey="record => record.virtualmachineid" :pagination="false" :loading="loading"> - <template #icon="{ text, record }" :name="text"> - <label class="interval-icon"> - <span v-if="record.intervaltype==='HOURLY'"> - <clock-circle-outlined /> + <template #bodyCell="{ column, text, record }"> + <template v-if="column.key === 'icon'" :name="text"> + <label class="interval-icon"> + <span v-if="record.intervaltype==='HOURLY'"> + <clock-circle-outlined /> + </span> + <span class="custom-icon icon-daily" v-else-if="record.intervaltype==='DAILY'"> + <calendar-outlined /> + </span> + <span class="custom-icon icon-weekly" v-else-if="record.intervaltype==='WEEKLY'"> + <calendar-outlined /> + </span> + <span class="custom-icon icon-monthly" v-else-if="record.intervaltype==='MONTHLY'"> + <calendar-outlined /> + </span> + </label> + </template> ++ <template v-if="column.key === 'intervaltype'" :name="text"> ++ <label>{{ record.intervaltype }}</label> ++ </template> + <template v-if="column.key === 'time'" :name="text"> + <label class="interval-content"> + <span v-if="record.intervaltype==='HOURLY'">{{ record.schedule + ' ' + $t('label.min.past.hour') }}</span> + <span v-else>{{ record.schedule.split(':')[1] + ':' + record.schedule.split(':')[0] }}</span> + </label> + </template> + <template v-if="column.key === 'interval'" :name="text"> + <span v-if="record.intervaltype==='WEEKLY'"> + {{ `${$t('label.every')} ${$t(listDayOfWeek[record.schedule.split(':')[2] - 1])}` }} </span> - <span class="custom-icon icon-daily" v-else-if="record.intervaltype==='DAILY'"> - <calendar-outlined /> + <span v-else-if="record.intervaltype==='MONTHLY'"> + {{ `${$t('label.day')} ${record.schedule.split(':')[2]} ${$t('label.of.month')}` }} </span> - <span class="custom-icon icon-weekly" v-else-if="record.intervaltype==='WEEKLY'"> - <calendar-outlined /> - </span> - <span class="custom-icon icon-monthly" v-else-if="record.intervaltype==='MONTHLY'"> - <calendar-outlined /> - </span> - </label> - </template> - <template #intervaltype="{ text, record }" :name="text"> - <label>{{ record.intervaltype }}</label> - </template> - <template #time="{ text, record }" :name="text"> - <label class="interval-content"> - <span v-if="record.intervaltype==='HOURLY'">{{ record.schedule + ' ' + $t('label.min.past.hour') }}</span> - <span v-else>{{ record.schedule.split(':')[1] + ':' + record.schedule.split(':')[0] }}</span> - </label> - </template> - <template #interval="{ text, record }" :name="text"> - <span v-if="record.intervaltype==='WEEKLY'"> - {{ `${$t('label.every')} ${$t(listDayOfWeek[record.schedule.split(':')[2] - 1])}` }} - </span> - <span v-else-if="record.intervaltype==='MONTHLY'"> - {{ `${$t('label.day')} ${record.schedule.split(':')[2]} ${$t('label.of.month')}` }} - </span> - </template> - <template #timezone="{ text, record }" :name="text"> - <label>{{ getTimeZone(record.timezone) }}</label> - </template> - <template #action="{ text, record }" class="account-button-action" :name="text"> - <tooltip-button - tooltipPlacement="top" - :tooltip="$t('label.delete')" - type="primary" - :danger="true" - icon="close-outlined" - size="small" - :loading="actionLoading" - @onClick="handleClickDelete(record)"/> + </template> + <template v-if="column.key === 'timezone'" :name="text"> + <label>{{ getTimeZone(record.timezone) }}</label> + </template> + <template v-if="column.key === 'actions'" class="account-button-action" :name="text"> + <tooltip-button + tooltipPlacement="top" + :tooltip="$t('label.delete')" + type="primary" + :danger="true" + icon="close-outlined" + size="small" + :loading="actionLoading" + @onClick="handleClickDelete(record)"/> + </template> </template> </a-table> </div> @@@ -109,31 -110,36 +112,34 @@@ export default columns () { return [ { + key: 'icon', title: '', dataIndex: 'icon', - width: 30, - slots: { customRender: 'icon' } + width: 30 }, { - key: 'time', + title: this.$t('label.intervaltype'), - dataIndex: 'intervaltype', - slots: { customRender: 'intervaltype' } ++ dataIndex: 'intervaltype' + }, + { title: this.$t('label.time'), - dataIndex: 'schedule', - slots: { customRender: 'time' } + dataIndex: 'schedule' }, { + key: 'interval', title: '', - dataIndex: 'interval', - slots: { customRender: 'interval' } + dataIndex: 'interval' }, { + key: 'timezone', title: this.$t('label.timezone'), - dataIndex: 'timezone', - slots: { customRender: 'timezone' } + dataIndex: 'timezone' }, { - title: this.$t('label.action'), - dataIndex: 'action', - width: 80, - slots: { customRender: 'action' } + key: 'actions', + title: this.$t('label.actions'), + dataIndex: 'actions', + width: 80 } ] }
