You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by we...@apache.org on 2024/02/05 10:13:20 UTC
(cloudstack) 01/02: Merge remote-tracking branch 'apache/4.18' into 4.19
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 <we...@apache.org>
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
}
]
}