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
          }
        ]
      }

Reply via email to