You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by jo...@apache.org on 2026/09/10 14:12:03 UTC
(cloudstack) 01/01: Merge branch '4.22'
This is an automated email from the ASF dual-hosted git repository.
JoaoJandre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 4ce5291f7090768b89fda619c1403fd772c6c357
Merge: 7516e532f97 d0d49bc864a
Author: João Jandre <48...@users.noreply.github.com>
AuthorDate: Thu Sep 10 11:11:08 2026 -0300
Merge branch '4.22'
PendingReleaseNotes | 14 +
api/src/main/java/com/cloud/host/Host.java | 1 +
.../user/vmsnapshot/CreateVMSnapshotCmd.java | 3 +-
.../api/response/ImageStoreDetailResponse.java | 4 +-
.../api/response/NetworkOfferingResponse.java | 12 +
.../cloudstack/api/response/ProjectResponse.java | 2 +-
.../api/response/ImageStoreDetailResponseTest.java | 45 ++
.../api/response/ProjectResponseTest.java | 34 +
.../storage/CreateDiskOnlyVmSnapshotAnswer.java | 11 +
.../storage/CreateDiskOnlyVmSnapshotCommand.java | 17 +
.../storage/DeleteDiskOnlyVmSnapshotCommand.java | 25 +-
.../storage/RevertDiskOnlyVmSnapshotCommand.java | 22 +
.../java/com/cloud/agent/manager/AgentAttache.java | 3 +-
.../com/cloud/agent/manager/AgentManagerImpl.java | 38 +-
.../com/cloud/vm/VirtualMachineManagerImpl.java | 17 +-
.../agent/manager/AgentAttacheSendNextTest.java | 85 +++
.../cloud/agent/manager/AgentManagerImplTest.java | 37 +-
.../cloud/vm/VirtualMachineManagerImplTest.java | 92 +++
.../KvmFileBasedStorageVmSnapshotStrategy.java | 269 ++++++-
.../KvmFileBasedStorageVmSnapshotStrategyTest.java | 550 ++++++++++++++
.../storage/volume/VolumeServiceImpl.java | 4 +-
.../framework/config/impl/ConfigDepotImpl.java | 3 +-
.../framework/config/impl/ConfigDepotImplTest.java | 25 +
.../framework/jobs/impl/AsyncJobManagerImpl.java | 80 ++
.../kvm/resource/LibvirtComputingResource.java | 11 +
...virtCreateDiskOnlyVMSnapshotCommandWrapper.java | 377 +++++++++-
...virtDeleteDiskOnlyVMSnapshotCommandWrapper.java | 38 +
.../wrapper/LibvirtReadyCommandWrapper.java | 1 +
.../LibvirtRestoreBackupCommandWrapper.java | 71 +-
...virtRevertDiskOnlyVMSnapshotCommandWrapper.java | 94 ++-
.../kvm/storage/KVMStorageProcessor.java | 99 ++-
.../apache/cloudstack/utils/linux/KVMHostInfo.java | 11 +-
...ibvirtDiskOnlyVMSnapshotCommandWrapperTest.java | 821 +++++++++++++++++++++
.../LibvirtRestoreBackupCommandWrapperTest.java | 124 ++++
.../kvm/storage/KVMStorageProcessorTest.java | 77 ++
.../lifecycle/StorageVmSharedFSLifeCycle.java | 5 +-
.../lifecycle/StorageVmSharedFSLifeCycleTest.java | 41 +
.../command/SAML2LogoutAPIAuthenticatorCmd.java | 3 +
.../SAML2LogoutAPIAuthenticatorCmdTest.java | 26 +
.../main/java/com/cloud/api/ApiResponseHelper.java | 3 +-
server/src/main/java/com/cloud/api/ApiServlet.java | 26 +-
.../cloud/consoleproxy/ConsoleProxyManager.java | 3 +
.../consoleproxy/ConsoleProxyManagerImpl.java | 6 +-
.../network/lb/LoadBalancingRulesManagerImpl.java | 2 +-
.../network/vpn/RemoteAccessVpnManagerImpl.java | 2 +-
.../main/java/com/cloud/server/StatsCollector.java | 39 +-
.../main/java/com/cloud/vm/UserVmManagerImpl.java | 24 +-
.../lb/LoadBalancingRulesManagerImplTest.java | 80 +-
.../vpn/RemoteAccessVpnManagerImplTest.java | 50 ++
.../java/com/cloud/server/StatsCollectorTest.java | 88 +++
.../java/com/cloud/vm/UserVmManagerImplTest.java | 75 +-
.../java/com/cloud/consoleproxy/ConsoleProxy.java | 90 ++-
.../cloud/consoleproxy/ConsoleProxyGCThread.java | 1 +
.../consoleproxy/ConsoleProxyNoVNCHandler.java | 16 +-
.../consoleproxy/ConsoleProxyNoVncClient.java | 4 +
.../com/cloud/consoleproxy/vnc/NoVncClient.java | 22 +
.../cloud/consoleproxy/vnc/network/NioSocket.java | 24 +
.../consoleproxy/vnc/network/NioSocketHandler.java | 1 +
.../vnc/network/NioSocketHandlerImpl.java | 7 +
tools/checkstyle/src/main/resources/cloud-pmd.xml | 117 +--
ui/public/locales/en.json | 4 +-
ui/src/components/offering/ComputeOfferingForm.vue | 2 +-
.../InstanceVolumesStoragePoolSelectListView.vue | 6 +
ui/src/components/view/ListView.vue | 10 +-
ui/src/components/view/StoragePoolSelectView.vue | 15 +-
.../view/VolumeStoragePoolSelectForm.vue | 6 +
ui/src/config/section/compute.js | 1 +
ui/src/config/section/network.js | 1 +
ui/src/config/section/offering.js | 8 +-
ui/src/utils/util.js | 9 +
ui/src/views/compute/AutoScaleDownPolicyTab.vue | 19 +-
ui/src/views/compute/AutoScaleLoadBalancing.vue | 20 +-
ui/src/views/compute/AutoScaleUpPolicyTab.vue | 19 +-
ui/src/views/compute/AutoScaleVmProfile.vue | 57 +-
ui/src/views/compute/DeployVM.vue | 18 +-
ui/src/views/compute/MigrateWizard.vue | 4 +-
ui/src/views/dashboard/SetupTwoFaAtLogin.vue | 4 +-
ui/src/views/dashboard/VerifyTwoFa.vue | 6 +-
ui/src/views/iam/SetupTwoFaAtUserProfile.vue | 4 +-
.../compute/DeployVMTemplateHypervisor.spec.js | 153 ++++
80 files changed, 3944 insertions(+), 294 deletions(-)
diff --cc api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java
index d3128599b61,f2decf21a1d..35723a3bbb7
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java
@@@ -34,9 -34,11 +34,10 @@@ import org.apache.cloudstack.context.Ca
import com.cloud.event.EventTypes;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.uservm.UserVm;
-import com.cloud.vm.VirtualMachine;
import com.cloud.vm.snapshot.VMSnapshot;
- @APICommand(name = "createVMSnapshot", description = "Creates Snapshot for an Instance.", responseObject = VMSnapshotResponse.class, since = "4.2.0", entityType = {VMSnapshot.class},
+ @APICommand(name = "createVMSnapshot", description = "Creates Snapshot for an Instance. Running KVM UEFI disk-only snapshots briefly suspend the Instance while copying NVRAM state.",
+ responseObject = VMSnapshotResponse.class, since = "4.2.0", entityType = {VMSnapshot.class},
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class CreateVMSnapshotCmd extends BaseAsyncCreateCmd {
diff --cc core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java
index 9a571e34c7e,ffa4eaff296..7041fdedc98
--- a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java
+++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java
@@@ -25,14 -26,25 +25,25 @@@ import java.util.Map
public class CreateDiskOnlyVmSnapshotAnswer extends Answer {
- protected Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath;
+ protected Map<String, Long> mapVolumeToSnapshotSize;
+ private String nvramSnapshotPath;
- public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath) {
- this(command, success, details, mapVolumeToSnapshotSizeAndNewVolumePath, null);
+ public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Long> mapVolumeToSnapshotSize) {
++ this(command, success, details, mapVolumeToSnapshotSize, null);
+ }
+
- public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath,
++ public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Long> mapVolumeToSnapshotSize,
+ String nvramSnapshotPath) {
super(command, success, details);
- this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath;
+ this.mapVolumeToSnapshotSize = mapVolumeToSnapshotSize;
+ this.nvramSnapshotPath = nvramSnapshotPath;
}
- public Map<String, Pair<Long, String>> getMapVolumeToSnapshotSizeAndNewVolumePath() {
- return mapVolumeToSnapshotSizeAndNewVolumePath;
+ public Map<String, Long> getMapVolumeToSnapshotSize() {
+ return mapVolumeToSnapshotSize;
}
+
+ public String getNvramSnapshotPath() {
+ return nvramSnapshotPath;
+ }
}
diff --cc core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java
index da1b420625f,7f328bab81d..cb731898a4d
--- a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java
@@@ -30,20 -29,30 +30,37 @@@ import java.util.List
public class CreateDiskOnlyVmSnapshotCommand extends VMSnapshotBaseCommand {
protected VirtualMachine.State vmState;
+ private final String vmUuid;
+ private final boolean uefiEnabled;
- public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List<VolumeObjectTO> volumeTOs, String guestOSType, VirtualMachine.State vmState) {
- this(vmName, null, snapshot, volumeTOs, guestOSType, vmState, false);
+ List<Pair<VolumeObjectTO, String>> volumeTosAndNewPaths;
+
+ public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List<Pair<VolumeObjectTO, String>> volumeTosAndNewPaths, String guestOSType, VirtualMachine.State vmState) {
++ this(vmName, null, snapshot, volumeTosAndNewPaths, guestOSType, vmState, false);
+ }
+
- public CreateDiskOnlyVmSnapshotCommand(String vmName, String vmUuid, VMSnapshotTO snapshot, List<VolumeObjectTO> volumeTOs, String guestOSType,
++ public CreateDiskOnlyVmSnapshotCommand(String vmName, String vmUuid, VMSnapshotTO snapshot, List<Pair<VolumeObjectTO, String>> volumeTosAndNewPaths, String guestOSType,
+ VirtualMachine.State vmState, boolean uefiEnabled) {
- super(vmName, snapshot, volumeTOs, guestOSType);
+ super(vmName, snapshot, null, guestOSType);
+ this.vmUuid = vmUuid;
this.vmState = vmState;
+ this.volumeTosAndNewPaths = volumeTosAndNewPaths;
+ this.uefiEnabled = uefiEnabled;
}
public VirtualMachine.State getVmState() {
return vmState;
}
+ public List<Pair<VolumeObjectTO, String>> getVolumeTosAndNewPaths() {
+ return volumeTosAndNewPaths;
+ }
++
+ public String getVmUuid() {
+ return vmUuid;
+ }
+
+ public boolean isUefiEnabled() {
+ return uefiEnabled;
+ }
}
diff --cc engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java
index 14e6e2b12ed,79e2a972203..c812f5acf06
--- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java
+++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java
@@@ -18,40 -18,6 +18,43 @@@
*/
package org.apache.cloudstack.storage.vmsnapshot;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
++import com.cloud.utils.StringUtils;
++import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.backup.BackupManagerImpl;
+import org.apache.cloudstack.backup.BackupOfferingVO;
+import org.apache.cloudstack.backup.InternalBackupService;
+import org.apache.cloudstack.backup.InternalBackupStoragePoolVO;
+import org.apache.cloudstack.backup.dao.BackupOfferingDao;
+import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider;
+import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
+import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority;
+import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
+import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.snapshot.SnapshotObject;
+import org.apache.cloudstack.storage.to.BackupDeltaTO;
+import org.apache.cloudstack.storage.to.DeltaMergeTreeTO;
++import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
+import org.apache.cloudstack.storage.to.SnapshotObjectTO;
+import org.apache.cloudstack.storage.to.VolumeObjectTO;
+import org.apache.commons.collections.CollectionUtils;
+
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.VMSnapshotTO;
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer;
@@@ -60,7 -26,9 +63,8 @@@ import com.cloud.agent.api.storage.Dele
import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand;
import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotAnswer;
import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand;
-import com.cloud.agent.api.storage.SnapshotMergeTreeTO;
import com.cloud.agent.api.to.DataTO;
+ import com.cloud.alert.AlertManager;
import com.cloud.configuration.Resource;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
@@@ -97,16 -96,15 +107,25 @@@ public class KvmFileBasedStorageVmSnaps
@Inject
protected BackupOfferingDao backupOfferingDao;
+ @Inject
+ private InternalBackupService internalBackupService;
+
+ @Inject
+ private InternalBackupStoragePoolDao internalBackupStoragePoolDao;
+
+ @Inject
+ private SnapshotDao snapshotDao;
+
+
+ @Inject
+ protected VMInstanceDetailsDao vmInstanceDetailsDao;
+
+ @Inject
+ protected HostDetailsDao hostDetailsDao;
+
+ @Inject
+ protected AlertManager alertManager;
+
@Override
public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) {
Map<VolumeInfo, SnapshotObject> volumeInfoToSnapshotObjectMap = new HashMap<>();
@@@ -377,7 -384,8 +414,8 @@@
}
private List<SnapshotVO> deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) {
+ validateHostSupportsNvramSidecarCleanup(vmSnapshotVO, hostId, "delete");
- List<SnapshotDataStoreVO> volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVO);
+ List<SnapshotDataStoreVO> volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot((vmSnapshotVO.getId()));
List<DataTO> volumeSnapshotTOList = volumeSnapshots.stream()
.map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO())
.collect(Collectors.toList());
@@@ -398,7 -406,21 +436,21 @@@
return snapshotVOList;
}
+ protected void deleteNvramSnapshotIfNeeded(VMSnapshotVO vmSnapshotVO, Long hostId, PrimaryDataStoreTO primaryDataStore) {
+ String nvramSnapshotPath = getNvramSnapshotPath(vmSnapshotVO);
+ if (StringUtils.isBlank(nvramSnapshotPath) || primaryDataStore == null) {
+ return;
+ }
+
+ DeleteDiskOnlyVmSnapshotCommand deleteSnapshotCommand = new DeleteDiskOnlyVmSnapshotCommand(List.of(), nvramSnapshotPath, primaryDataStore);
+ Answer answer = agentMgr.easySend(hostId, deleteSnapshotCommand);
+ if (answer == null || !answer.getResult()) {
+ logger.warn("Failed to delete the NVRAM sidecar of VM snapshot [{}] due to {}.", vmSnapshotVO.getUuid(),
+ answer != null ? answer.getDetails() : "communication failure");
+ }
+ }
+
- private List<SnapshotVO> mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, List<VolumeObjectTO> volumeObjectTOS, Long hostId) {
+ private List<SnapshotVO> mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, Long hostId) {
logger.debug("Merging VM snapshot [{}] with its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid());
List<VMSnapshotVO> snapshotGrandChildren = vmSnapshotDao.listByParentAndStateIn(childSnapshot.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden);
@@@ -545,19 -546,22 +598,23 @@@
VMSnapshotTO target = new VMSnapshotTO(vmSnapshot.getId(), vmSnapshot.getName(), vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, parentSnapshotTo, quiesceVm);
- CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTosAndNewPaths, null, userVm.getState());
+ CreateDiskOnlyVmSnapshotCommand ccmd =
- new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), userVm.getUuid(), target, volumeTOs, null, userVm.getState(), isUefiVm(userVm));
++ new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), userVm.getUuid(), target, volumeTosAndNewPaths, null, userVm.getState(), isUefiVm(userVm));
logger.info("Sending disk-only VM snapshot creation of VM Snapshot [{}] command for host [{}].", vmSnapshot.getUuid(), hostId);
Answer answer = agentMgr.easySend(hostId, ccmd);
if (answer != null && answer.getResult()) {
CreateDiskOnlyVmSnapshotAnswer createDiskOnlyVMSnapshotAnswer = (CreateDiskOnlyVmSnapshotAnswer) answer;
- return processCreateVmSnapshotAnswer(vmSnapshot, volumeInfoToSnapshotObjectMap, createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO, virtualSize, parentSnapshotVo);
+ VMSnapshot createdVmSnapshot = processCreateVmSnapshotAnswer(vmSnapshot, volumeInfoToSnapshotObjectMap, createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO,
+ virtualSize, parentSnapshotVo);
+ notifyGuestRecoveryIssueIfNeeded(createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO);
+ return createdVmSnapshot;
}
- logger.error("Disk-only VM snapshot for VM [{}] failed{}.", userVm.getUuid(), answer != null ? " due to" + answer.getDetails() : "");
- throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed.", userVm.getUuid()));
+ String details = answer != null ? answer.getDetails() : String.format("No answer received from host [%s]. The host may be unreachable.", hostId);
+ logger.error("Disk-only VM snapshot for VM [{}] failed due to: {}.", userVm.getUuid(), details);
+ throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed due to: %s.", userVm.getUuid(), details));
}
/**
@@@ -723,6 -716,192 +788,192 @@@
return realSize;
}
+ protected boolean isUefiVm(UserVm userVm) {
+ return vmInstanceDetailsDao.findDetail(userVm.getId(), ApiConstants.BootType.UEFI.toString()) != null;
+ }
+
+ protected PrimaryDataStoreTO getRootVolumePrimaryDataStore(List<VolumeObjectTO> volumeTOs) {
+ return (PrimaryDataStoreTO) volumeTOs.stream()
+ .filter(volumeObjectTO -> Volume.Type.ROOT.equals(volumeObjectTO.getVolumeType()))
+ .findFirst()
+ .orElseThrow(() -> new CloudRuntimeException("Failed to locate the root volume while handling the VM snapshot."))
+ .getDataStore();
+ }
+
+ protected PrimaryDataStoreTO getRootVolumePrimaryDataStoreForCleanup(VMSnapshotVO vmSnapshot, List<VolumeObjectTO> volumeTOs) {
+ try {
+ return getRootVolumePrimaryDataStore(volumeTOs);
+ } catch (CloudRuntimeException e) {
+ logger.warn("Failed to locate the root volume while cleaning up the NVRAM sidecar for VM snapshot [{}].", vmSnapshot.getUuid(), e);
+ return null;
+ }
+ }
+
+ protected PrimaryDataStoreTO getPrimaryDataStoreForNvramCleanup(VMSnapshotVO vmSnapshot, List<VolumeObjectTO> volumeTOs) {
+ PrimaryDataStoreTO rootSnapshotPrimaryDataStore = getRootSnapshotPrimaryDataStoreForCleanup(vmSnapshot);
+ return rootSnapshotPrimaryDataStore != null ? rootSnapshotPrimaryDataStore : getRootVolumePrimaryDataStoreForCleanup(vmSnapshot, volumeTOs);
+ }
+
+ protected PrimaryDataStoreTO getRootSnapshotPrimaryDataStoreForCleanup(VMSnapshotVO vmSnapshot) {
+ try {
- return (PrimaryDataStoreTO) getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshot).stream()
++ return (PrimaryDataStoreTO) vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshot.getId()).stream()
+ .map(snapshotDataStoreVO -> (SnapshotObjectTO) snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(),
+ snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO())
+ .filter(snapshotObjectTO -> Volume.Type.ROOT.equals(snapshotObjectTO.getVolume().getVolumeType()))
+ .findFirst()
+ .orElseThrow(() -> new CloudRuntimeException("Failed to locate the root volume snapshot while handling the VM snapshot."))
+ .getDataStore();
+ } catch (CloudRuntimeException e) {
+ logger.warn("Failed to locate the root volume snapshot while cleaning up the NVRAM sidecar for VM snapshot [{}].", vmSnapshot.getUuid(), e);
+ return null;
+ }
+ }
+
+ protected String getNvramSnapshotPath(VMSnapshotVO vmSnapshot) {
+ VMSnapshotDetailsVO nvramDetail = vmSnapshotDetailsDao.findDetail(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT_NVRAM);
+ return nvramDetail != null ? nvramDetail.getValue() : null;
+ }
+
+ protected Long pickHostForUefiNvramAwareDiskOnlySnapshot(UserVm userVm, String operation) {
+ Long selectedHostId = vmSnapshotHelper.pickRunningHost(userVm.getId());
+ if (!isUefiVm(userVm)) {
+ return selectedHostId;
+ }
+
+ boolean isCreate = "create".equals(operation);
+ if (isCreate) {
+ validateUefiSnapshotCreateHostOwnsActiveNvram(userVm, selectedHostId);
+ }
+
+ return pickHostWithRequiredCapabilities(userVm, selectedHostId, operation, !isCreate,
+ List.of(Host.HOST_UEFI_ENABLE, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM));
+ }
+
+ protected void validateUefiSnapshotCreateHostOwnsActiveNvram(UserVm userVm, Long selectedHostId) {
+ if (VirtualMachine.State.Running.equals(userVm.getState())) {
+ return;
+ }
+
+ Long lastHostId = userVm.getLastHostId();
+ if (lastHostId == null || !Objects.equals(lastHostId, selectedHostId)) {
+ throw new CloudRuntimeException(String.format("Cannot create a disk-only snapshot for stopped UEFI VM [%s] on host [%s] because the active NVRAM "
+ + "state is expected on last host [%s]. Make the last host available or start the VM on a UEFI-capable KVM host before retrying.",
+ userVm.getUuid(), selectedHostId, lastHostId));
+ }
+ }
+
+ protected Long pickHostForNvramSidecarCleanup(VMSnapshotVO vmSnapshotVO, UserVm userVm, String operation) {
+ Long selectedHostId = vmSnapshotHelper.pickRunningHost(vmSnapshotVO.getVmId());
+ if (StringUtils.isBlank(getNvramSnapshotPath(vmSnapshotVO))) {
+ return selectedHostId;
+ }
+
+ return pickHostWithRequiredCapabilities(userVm, selectedHostId, operation, true, List.of(Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM));
+ }
+
+ protected Long pickHostWithRequiredCapabilities(UserVm userVm, Long selectedHostId, String operation, boolean canFallbackFromSelectedHost,
+ List<String> requiredCapabilities) {
+ if (hostSupportsCapabilities(selectedHostId, requiredCapabilities)) {
+ return selectedHostId;
+ }
+
+ if (VirtualMachine.State.Running.equals(userVm.getState()) || !canFallbackFromSelectedHost) {
+ return selectedHostId;
+ }
+
+ return listCandidateHostsForVmSnapshot(userVm).stream()
+ .filter(host -> hostSupportsCapabilities(host.getId(), requiredCapabilities))
+ .findFirst()
+ .map(HostVO::getId)
+ .orElseThrow(() -> new CloudRuntimeException(String.format("Cannot %s disk-only snapshot state for VM [%s] because no Up and Enabled host in the "
+ + "VM storage scope advertises [%s].", operation, userVm.getUuid(), String.join(", ", requiredCapabilities))));
+ }
+
+ protected List<HostVO> listCandidateHostsForVmSnapshot(UserVm userVm) {
+ List<VolumeVO> volumes = volumeDao.findByInstance(userVm.getId());
+ if (CollectionUtils.isEmpty(volumes)) {
+ throw new CloudRuntimeException(String.format("Cannot find a host for VM snapshot operation because VM [%s] has no volumes.", userVm.getUuid()));
+ }
+
+ VolumeVO volume = volumes.stream()
+ .filter(volumeVO -> Volume.Type.ROOT.equals(volumeVO.getVolumeType()))
+ .findFirst()
+ .orElse(volumes.get(0));
+ Long poolId = volume.getPoolId();
+ if (poolId == null) {
+ throw new CloudRuntimeException(String.format("Cannot find a host for VM snapshot operation because volume [%s] has no pool.", volume.getUuid()));
+ }
+
+ StoragePoolVO storagePoolVO = storagePool.findById(poolId);
+ if (storagePoolVO == null) {
+ throw new CloudRuntimeException(String.format("Cannot find a host for VM snapshot operation because storage pool [%s] was not found.", poolId));
+ }
+
+ List<HostVO> hosts = hostDao.listAllUpAndEnabledNonHAHosts(Host.Type.Routing, storagePoolVO.getClusterId(), storagePoolVO.getPodId(),
+ storagePoolVO.getDataCenterId(), null);
+ if (CollectionUtils.isEmpty(hosts)) {
+ throw new CloudRuntimeException(String.format("Cannot find a host for VM snapshot operation because no Up and Enabled host was found in storage pool [%s] scope.",
+ storagePoolVO.getUuid()));
+ }
+ return hosts;
+ }
+
+ protected boolean hostSupportsCapabilities(Long hostId, List<String> requiredCapabilities) {
+ if (hostId == null || CollectionUtils.isEmpty(requiredCapabilities)) {
+ return false;
+ }
+ return requiredCapabilities.stream().allMatch(capability -> isHostCapabilityEnabled(hostId, capability));
+ }
+
+ protected void validateHostSupportsUefiNvramAwareDiskOnlySnapshots(Long hostId, UserVm userVm, String operation) {
+ if (!isUefiVm(userVm)) {
+ return;
+ }
+
+ if (!isHostCapabilityEnabled(hostId, Host.HOST_UEFI_ENABLE)) {
+ throw new CloudRuntimeException(String.format("Cannot %s a disk-only snapshot for UEFI VM [%s] on host [%s] because the host does not advertise "
+ + "UEFI support. Ensure the host is configured with UEFI support and retry.", operation, userVm.getUuid(), hostId));
+ }
+
+ if (!isHostCapabilityEnabled(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)) {
+ throw new CloudRuntimeException(String.format("Cannot %s a disk-only snapshot for UEFI VM [%s] on host [%s] because the KVM agent does not advertise "
+ + "NVRAM-aware disk-only snapshot support. Upgrade the host and retry.", operation, userVm.getUuid(), hostId));
+ }
+ }
+
+ protected boolean isHostCapabilityEnabled(Long hostId, String capabilityName) {
+ DetailVO hostCapability = hostDetailsDao.findDetail(hostId, capabilityName);
+ return hostCapability != null && Boolean.parseBoolean(hostCapability.getValue());
+ }
+
+ protected void validateHostSupportsNvramSidecarCleanup(VMSnapshotVO vmSnapshotVO, Long hostId, String operation) {
+ if (StringUtils.isBlank(getNvramSnapshotPath(vmSnapshotVO))) {
+ return;
+ }
+
+ if (!isHostCapabilityEnabled(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)) {
+ throw new CloudRuntimeException(String.format("Cannot %s VM snapshot [%s] on host [%s] because the KVM agent does not advertise "
+ + "NVRAM-aware disk-only snapshot support and the snapshot has an NVRAM sidecar that must be cleaned up. Upgrade the host and retry.",
+ operation, vmSnapshotVO.getUuid(), hostId));
+ }
+ }
+
+ protected void notifyGuestRecoveryIssueIfNeeded(CreateDiskOnlyVmSnapshotAnswer answer, UserVm userVm, VMSnapshotVO vmSnapshot) {
+ if (StringUtils.isBlank(answer.getDetails())) {
+ return;
+ }
+
+ String subject = String.format("Disk-only VM snapshot [%s] completed with guest recovery warnings", vmSnapshot.getUuid());
+ String message = String.format("Disk-only VM snapshot [%s] for UEFI VM [%s] completed, but post-snapshot guest recovery reported: %s",
+ vmSnapshot.getUuid(), userVm.getUuid(), answer.getDetails());
+ logger.error(message);
+ try {
+ alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_VM_SNAPSHOT, userVm.getDataCenterId(), userVm.getPodIdToDeployIn(), subject, message);
+ } catch (Exception e) {
+ logger.warn("Failed to send post-snapshot guest recovery alert for VM snapshot [{}].", vmSnapshot.getUuid(), e);
+ }
+ }
+
/**
* Given a list of VM snapshots, will remove any that are part of the current direct backing chain (all the direct ancestors of the current vm snapshot).
* This is done because, when using <a href="https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockCommit">virDomainBlockCommit</a>}, Libvirt will maintain
diff --cc engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategyTest.java
index 00000000000,35b1f9ff65b..c1135d795e6
mode 000000,100644..100644
--- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategyTest.java
+++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategyTest.java
@@@ -1,0 -1,550 +1,550 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+ package org.apache.cloudstack.storage.vmsnapshot;
+
+ import static org.junit.Assert.assertEquals;
+ import static org.junit.Assert.assertNull;
+ import static org.junit.Assert.assertSame;
+ import static org.mockito.ArgumentMatchers.any;
+ import static org.mockito.ArgumentMatchers.anyLong;
+ import static org.mockito.ArgumentMatchers.anyString;
+ import static org.mockito.ArgumentMatchers.argThat;
+ import static org.mockito.ArgumentMatchers.eq;
+ import static org.mockito.Mockito.doNothing;
+ import static org.mockito.Mockito.inOrder;
+ import static org.mockito.Mockito.mock;
+ import static org.mockito.Mockito.never;
+ import static org.mockito.Mockito.verify;
+ import static org.mockito.Mockito.when;
+
+ import java.lang.reflect.Method;
+ import java.util.Collections;
+ import java.util.List;
+
+ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
+ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
+ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
+ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
+ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
+ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+ import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
+ import org.apache.cloudstack.storage.to.SnapshotObjectTO;
+ import org.apache.cloudstack.storage.to.VolumeObjectTO;
+ import org.apache.cloudstack.api.ApiConstants;
+ import org.junit.Before;
+ import org.junit.Test;
+ import org.mockito.ArgumentCaptor;
+ import org.mockito.InOrder;
+ import org.mockito.Mockito;
+
+ import com.cloud.agent.AgentManager;
+ import com.cloud.agent.api.Answer;
+ import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer;
+ import com.cloud.agent.api.storage.DeleteDiskOnlyVmSnapshotCommand;
+ import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotAnswer;
+ import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand;
+ import com.cloud.alert.AlertManager;
+ import com.cloud.host.DetailVO;
+ import com.cloud.host.Host;
+ import com.cloud.host.HostVO;
+ import com.cloud.host.dao.HostDao;
+ import com.cloud.host.dao.HostDetailsDao;
+ import com.cloud.storage.DataStoreRole;
+ import com.cloud.storage.Volume;
+ import com.cloud.storage.VolumeVO;
+ import com.cloud.storage.dao.VolumeDao;
+ import com.cloud.user.ResourceLimitService;
+ import com.cloud.uservm.UserVm;
+ import com.cloud.utils.exception.CloudRuntimeException;
+ import com.cloud.vm.VMInstanceDetailVO;
+ import com.cloud.vm.UserVmVO;
+ import com.cloud.vm.VirtualMachine;
+ import com.cloud.vm.dao.VMInstanceDetailsDao;
+ import com.cloud.vm.dao.UserVmDao;
+ import com.cloud.vm.snapshot.VMSnapshot;
+ import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
+ import com.cloud.vm.snapshot.VMSnapshotVO;
+ import com.cloud.vm.snapshot.dao.VMSnapshotDao;
+ import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
+
+ public class KvmFileBasedStorageVmSnapshotStrategyTest {
+
+ private KvmFileBasedStorageVmSnapshotStrategy strategy;
+ private VMSnapshotDetailsDao vmSnapshotDetailsDao;
+ private VMSnapshotDao vmSnapshotDao;
+ private VMSnapshotHelper vmSnapshotHelper;
+ private AgentManager agentMgr;
+ private SnapshotDataStoreDao snapshotDataStoreDao;
+ private HostDetailsDao hostDetailsDao;
+ private HostDao hostDao;
+ private PrimaryDataStoreDao storagePoolDao;
+
+ @Before
+ public void setup() {
+ strategy = Mockito.spy(new KvmFileBasedStorageVmSnapshotStrategy());
+ vmSnapshotDetailsDao = mock(VMSnapshotDetailsDao.class);
+ vmSnapshotDao = mock(VMSnapshotDao.class);
+ vmSnapshotHelper = mock(VMSnapshotHelper.class);
+ agentMgr = mock(AgentManager.class);
+ snapshotDataStoreDao = mock(SnapshotDataStoreDao.class);
+ hostDetailsDao = mock(HostDetailsDao.class);
+ hostDao = mock(HostDao.class);
+ storagePoolDao = mock(PrimaryDataStoreDao.class);
+
+ strategy.vmSnapshotDetailsDao = vmSnapshotDetailsDao;
+ strategy.vmSnapshotDao = vmSnapshotDao;
+ strategy.vmSnapshotHelper = vmSnapshotHelper;
+ strategy.agentMgr = agentMgr;
+ strategy.snapshotDataStoreDao = snapshotDataStoreDao;
+ strategy.hostDao = hostDao;
+ strategy.storagePool = storagePoolDao;
+ strategy.resourceLimitManager = mock(ResourceLimitService.class);
+ strategy.snapshotDataFactory = mock(SnapshotDataFactory.class);
+ strategy.userVmDao = mock(UserVmDao.class);
+ strategy.volumeDao = mock(VolumeDao.class);
+ strategy.vmInstanceDetailsDao = mock(VMInstanceDetailsDao.class);
+ strategy.hostDetailsDao = hostDetailsDao;
+ strategy.alertManager = mock(AlertManager.class);
+ doNothing().when(strategy).publishUsageEvent(anyString(), any(VMSnapshot.class), any(UserVm.class), anyLong(), anyLong());
+ doNothing().when(strategy).publishUsageEvent(anyString(), any(VMSnapshot.class), any(UserVm.class), any(VolumeObjectTO.class));
+ }
+
+ @Test
+ public void testProcessCreateVmSnapshotAnswerPersistsNvramPath() throws Exception {
+ VMSnapshot vmSnapshot = mock(VMSnapshot.class);
+ CreateDiskOnlyVmSnapshotAnswer answer = mock(CreateDiskOnlyVmSnapshotAnswer.class);
+ UserVm userVm = mock(UserVm.class);
+ VMSnapshotVO vmSnapshotVO = mock(VMSnapshotVO.class);
+
+ when(vmSnapshot.getId()).thenReturn(42L);
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
- when(answer.getMapVolumeToSnapshotSizeAndNewVolumePath()).thenReturn(Collections.emptyMap());
++ when(answer.getMapVolumeToSnapshotSize()).thenReturn(Collections.emptyMap());
+ when(answer.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+
+ Method method = KvmFileBasedStorageVmSnapshotStrategy.class.getDeclaredMethod("processCreateVmSnapshotAnswer", VMSnapshot.class, java.util.Map.class,
+ CreateDiskOnlyVmSnapshotAnswer.class, UserVm.class, VMSnapshotVO.class, long.class, VMSnapshotVO.class);
+ method.setAccessible(true);
+ method.invoke(strategy, vmSnapshot, Collections.emptyMap(), answer, userVm, vmSnapshotVO, 0L, null);
+
+ verify(vmSnapshotDetailsDao).addDetail(42L, "kvmFileBasedStorageSnapshotNvram", "nvram/42.fd", false);
+ }
+
+ @Test
+ public void testRevertVMSnapshotPassesNvramPathToAgentCommand() {
+ long vmId = 10L;
+ long snapshotId = 20L;
+ long dataStoreId = 30L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+ VMSnapshotVO currentVmSnapshot = mock(VMSnapshotVO.class);
+ SnapshotDataStoreVO snapshotDataStoreVO = mock(SnapshotDataStoreVO.class);
+ SnapshotInfo snapshotInfo = mock(SnapshotInfo.class);
+ SnapshotObjectTO snapshotObjectTO = mock(SnapshotObjectTO.class);
+ VMSnapshotDetailsVO volumeSnapshotDetail = new VMSnapshotDetailsVO(snapshotId, "kvmFileBasedStorageSnapshot", String.valueOf(snapshotId), true);
+ VMSnapshotDetailsVO nvramDetail = new VMSnapshotDetailsVO(snapshotId, "kvmFileBasedStorageSnapshotNvram", "nvram/42.fd", false);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(vmSnapshot.getId()).thenReturn(snapshotId);
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Stopped);
+ when(userVm.getName()).thenReturn("i-10-VM");
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getLastHostId()).thenReturn(39L);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_UEFI_ENABLE))
+ .thenReturn(new DetailVO(hostId, Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM))
+ .thenReturn(new DetailVO(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString()));
+ when(vmSnapshotDetailsDao.findDetails(snapshotId, "kvmFileBasedStorageSnapshot")).thenReturn(List.of(volumeSnapshotDetail));
+ when(vmSnapshotDetailsDao.findDetail(snapshotId, "kvmFileBasedStorageSnapshotNvram")).thenReturn(nvramDetail);
+ when(snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(snapshotId, DataStoreRole.Primary)).thenReturn(snapshotDataStoreVO);
+ when(snapshotDataStoreVO.getSnapshotId()).thenReturn(snapshotId);
+ when(snapshotDataStoreVO.getDataStoreId()).thenReturn(dataStoreId);
+ when(strategy.snapshotDataFactory.getSnapshot(snapshotId, dataStoreId, DataStoreRole.Primary)).thenReturn(snapshotInfo);
+ when(snapshotInfo.getTO()).thenReturn(snapshotObjectTO);
+ when(vmSnapshotDao.findCurrentSnapshotByVmId(vmId)).thenReturn(currentVmSnapshot);
+ when(agentMgr.easySend(eq(hostId), any())).thenAnswer(invocation ->
+ new RevertDiskOnlyVmSnapshotAnswer((RevertDiskOnlyVmSnapshotCommand) invocation.getArgument(1), Collections.emptyList()));
+
+ ArgumentCaptor<RevertDiskOnlyVmSnapshotCommand> commandCaptor = ArgumentCaptor.forClass(RevertDiskOnlyVmSnapshotCommand.class);
+
+ strategy.revertVMSnapshot(vmSnapshot);
+
+ verify(agentMgr).easySend(eq(hostId), commandCaptor.capture());
+ verify(userVm).setLastHostId(hostId);
+ verify(strategy.userVmDao).update(vmId, userVm);
+ assertEquals("vm-uuid", commandCaptor.getValue().getVmUuid());
+ assertEquals(true, commandCaptor.getValue().isUefiEnabled());
+ assertEquals("nvram/42.fd", commandCaptor.getValue().getNvramSnapshotPath());
+ }
+
+ @Test
+ public void testPickHostForUefiNvramAwareDiskOnlySnapshotUsesCapableCandidateForRevertWhenDefaultHostLacksSupport() {
+ long vmId = 10L;
+ long defaultHostId = 40L;
+ long capableHostId = 41L;
+ long poolId = 50L;
+ long clusterId = 60L;
+ long podId = 70L;
+ long dataCenterId = 80L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VolumeVO rootVolume = mock(VolumeVO.class);
+ StoragePoolVO storagePool = mock(StoragePoolVO.class);
+ HostVO defaultHost = mock(HostVO.class);
+ HostVO capableHost = mock(HostVO.class);
+
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Stopped);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(defaultHostId);
+ when(rootVolume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(rootVolume.getPoolId()).thenReturn(poolId);
+ when(strategy.volumeDao.findByInstance(vmId)).thenReturn(List.of(rootVolume));
+ when(storagePoolDao.findById(poolId)).thenReturn(storagePool);
+ when(storagePool.getClusterId()).thenReturn(clusterId);
+ when(storagePool.getPodId()).thenReturn(podId);
+ when(storagePool.getDataCenterId()).thenReturn(dataCenterId);
+ when(defaultHost.getId()).thenReturn(defaultHostId);
+ when(capableHost.getId()).thenReturn(capableHostId);
+ when(hostDao.listAllUpAndEnabledNonHAHosts(Host.Type.Routing, clusterId, podId, dataCenterId, null))
+ .thenReturn(List.of(defaultHost, capableHost));
+ when(hostDetailsDao.findDetail(defaultHostId, Host.HOST_UEFI_ENABLE)).thenReturn(null);
+ when(hostDetailsDao.findDetail(defaultHostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(null);
+ when(hostDetailsDao.findDetail(capableHostId, Host.HOST_UEFI_ENABLE))
+ .thenReturn(new DetailVO(capableHostId, Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
+ when(hostDetailsDao.findDetail(capableHostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM))
+ .thenReturn(new DetailVO(capableHostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString()));
+
+ Long hostId = strategy.pickHostForUefiNvramAwareDiskOnlySnapshot(userVm, "revert");
+
+ assertEquals(Long.valueOf(capableHostId), hostId);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testPickHostForUefiNvramAwareDiskOnlySnapshotFailsCreateWhenSelectedHostIsNotLastHost() {
+ long vmId = 10L;
+ long lastHostId = 39L;
+ long selectedHostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Stopped);
+ when(userVm.getLastHostId()).thenReturn(lastHostId);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(selectedHostId);
+ when(hostDetailsDao.findDetail(selectedHostId, Host.HOST_UEFI_ENABLE))
+ .thenReturn(new DetailVO(selectedHostId, Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
+ when(hostDetailsDao.findDetail(selectedHostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM))
+ .thenReturn(new DetailVO(selectedHostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString()));
+
+ strategy.pickHostForUefiNvramAwareDiskOnlySnapshot(userVm, "create");
+ }
+
+ @Test
+ public void testDeleteNvramSnapshotIfNeededPassesPrimaryDataStoreToAgentCommand() {
+ long hostId = 40L;
+
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+ VMSnapshotDetailsVO nvramDetail = new VMSnapshotDetailsVO(20L, "kvmFileBasedStorageSnapshotNvram", "nvram/42.fd", false);
+ PrimaryDataStoreTO primaryDataStore = mock(PrimaryDataStoreTO.class);
+
+ when(vmSnapshot.getId()).thenReturn(20L);
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
+ when(vmSnapshotDetailsDao.findDetail(20L, "kvmFileBasedStorageSnapshotNvram")).thenReturn(nvramDetail);
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM))
+ .thenReturn(new DetailVO(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString()));
+ when(agentMgr.easySend(eq(hostId), any())).thenReturn(new Answer(null, true, null));
+
+ strategy.deleteNvramSnapshotIfNeeded(vmSnapshot, hostId, primaryDataStore);
+
+ verify(agentMgr).easySend(eq(hostId), argThat(command -> {
+ if (!(command instanceof DeleteDiskOnlyVmSnapshotCommand)) {
+ return false;
+ }
+
+ DeleteDiskOnlyVmSnapshotCommand deleteCommand = (DeleteDiskOnlyVmSnapshotCommand) command;
+ return deleteCommand.getSnapshots().isEmpty()
+ && "nvram/42.fd".equals(deleteCommand.getNvramSnapshotPath())
+ && deleteCommand.getPrimaryDataStore() == primaryDataStore;
+ }));
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testValidateHostSupportsNvramSidecarCleanupFailsWhenHostLacksNvramAwareCleanupCapability() {
+ long hostId = 40L;
+
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+ VMSnapshotDetailsVO nvramDetail = new VMSnapshotDetailsVO(20L, "kvmFileBasedStorageSnapshotNvram", "nvram/42.fd", false);
+
+ when(vmSnapshot.getId()).thenReturn(20L);
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
+ when(vmSnapshotDetailsDao.findDetail(20L, "kvmFileBasedStorageSnapshotNvram")).thenReturn(nvramDetail);
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(null);
+
+ try {
+ strategy.validateHostSupportsNvramSidecarCleanup(vmSnapshot, hostId, "delete");
+ } finally {
+ verify(agentMgr, never()).easySend(eq(hostId), any());
+ }
+ }
+
+ @Test
+ public void testGetRootVolumePrimaryDataStoreForCleanupReturnsNullWhenRootVolumeIsMissing() {
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+ VolumeObjectTO dataVolume = mock(VolumeObjectTO.class);
+
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
+ when(dataVolume.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+
+ PrimaryDataStoreTO primaryDataStore = strategy.getRootVolumePrimaryDataStoreForCleanup(vmSnapshot, List.of(dataVolume));
+
+ assertNull(primaryDataStore);
+ }
+
+ @Test
+ public void testGetPrimaryDataStoreForNvramCleanupPrefersRootSnapshotPrimaryDataStore() {
+ long vmSnapshotId = 20L;
+ long rootSnapshotId = 30L;
+ long dataStoreId = 40L;
+
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+ VolumeObjectTO rootVolume = mock(VolumeObjectTO.class);
+ PrimaryDataStoreTO liveRootVolumePrimaryDataStore = mock(PrimaryDataStoreTO.class);
+ PrimaryDataStoreTO rootSnapshotPrimaryDataStore = mock(PrimaryDataStoreTO.class);
+ SnapshotDataStoreVO rootSnapshotDataStore = mock(SnapshotDataStoreVO.class);
+ SnapshotInfo rootSnapshotInfo = mock(SnapshotInfo.class);
+ SnapshotObjectTO rootSnapshotObjectTo = mock(SnapshotObjectTO.class);
+ VolumeObjectTO rootSnapshotVolume = mock(VolumeObjectTO.class);
+ VMSnapshotDetailsVO volumeSnapshotDetail = new VMSnapshotDetailsVO(vmSnapshotId, "kvmFileBasedStorageSnapshot", String.valueOf(rootSnapshotId), true);
+
+ when(vmSnapshot.getId()).thenReturn(vmSnapshotId);
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
+ when(vmSnapshotDetailsDao.findDetails(vmSnapshotId, "kvmFileBasedStorageSnapshot")).thenReturn(List.of(volumeSnapshotDetail));
+ when(snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(rootSnapshotId, DataStoreRole.Primary)).thenReturn(rootSnapshotDataStore);
+ when(rootSnapshotDataStore.getSnapshotId()).thenReturn(rootSnapshotId);
+ when(rootSnapshotDataStore.getDataStoreId()).thenReturn(dataStoreId);
+ when(strategy.snapshotDataFactory.getSnapshot(rootSnapshotId, dataStoreId, DataStoreRole.Primary)).thenReturn(rootSnapshotInfo);
+ when(rootSnapshotInfo.getTO()).thenReturn(rootSnapshotObjectTo);
+ when(rootSnapshotObjectTo.getVolume()).thenReturn(rootSnapshotVolume);
+ when(rootSnapshotVolume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(rootSnapshotObjectTo.getDataStore()).thenReturn(rootSnapshotPrimaryDataStore);
+ when(rootVolume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(rootVolume.getDataStore()).thenReturn(liveRootVolumePrimaryDataStore);
+
+ PrimaryDataStoreTO primaryDataStore = strategy.getPrimaryDataStoreForNvramCleanup(vmSnapshot, List.of(rootVolume));
+
+ assertSame(rootSnapshotPrimaryDataStore, primaryDataStore);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testTakeVmSnapshotInternalFailsWhenHostLacksNvramAwareSnapshotCapabilityForUefiVm() throws Exception {
+ long vmId = 10L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_UEFI_ENABLE))
+ .thenReturn(new DetailVO(hostId, Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(null);
+
+ strategy.takeVmSnapshotInternal(vmSnapshot, Collections.emptyMap());
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testTakeVMSnapshotMarksSnapshotFailedWhenHostCapabilityValidationFails() throws Exception {
+ long vmId = 10L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_UEFI_ENABLE))
+ .thenReturn(new DetailVO(hostId, Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(null);
+
+ try {
+ strategy.takeVMSnapshot(vmSnapshot);
+ } finally {
+ InOrder inOrder = inOrder(vmSnapshotHelper);
+ inOrder.verify(vmSnapshotHelper).vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.CreateRequested);
+ inOrder.verify(vmSnapshotHelper).vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed);
+ verify(agentMgr, never()).easySend(eq(hostId), any());
+ }
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testDeleteVMSnapshotFailsWhenHostLacksNvramAwareCleanupCapabilityForSidecarSnapshot() {
+ long vmId = 10L;
+ long vmSnapshotId = 20L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+ VMSnapshotDetailsVO nvramDetail = new VMSnapshotDetailsVO(vmSnapshotId, "kvmFileBasedStorageSnapshotNvram", "nvram/42.fd", false);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(vmSnapshot.getId()).thenReturn(vmSnapshotId);
+ when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ when(vmSnapshotDetailsDao.findDetail(vmSnapshotId, "kvmFileBasedStorageSnapshotNvram")).thenReturn(nvramDetail);
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(null);
+
+ strategy.deleteVMSnapshot(vmSnapshot);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testTakeVmSnapshotInternalFailsWhenHostLacksUefiCapabilityForUefiVm() throws Exception {
+ long vmId = 10L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_UEFI_ENABLE)).thenReturn(null);
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM))
+ .thenReturn(new DetailVO(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString()));
+
+ strategy.takeVmSnapshotInternal(vmSnapshot, Collections.emptyMap());
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testRevertVMSnapshotFailsWhenHostLacksNvramAwareSnapshotCapabilityForUefiVm() {
+ long vmId = 10L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Stopped);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ mockCandidateHostScope(vmId, hostId);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_UEFI_ENABLE))
+ .thenReturn(new DetailVO(hostId, Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(null);
+
+ strategy.revertVMSnapshot(vmSnapshot);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testRevertVMSnapshotFailsWhenHostLacksUefiCapabilityForUefiVm() {
+ long vmId = 10L;
+ long hostId = 40L;
+
+ UserVmVO userVm = mock(UserVmVO.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+
+ when(vmSnapshot.getVmId()).thenReturn(vmId);
+ when(userVm.getId()).thenReturn(vmId);
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getState()).thenReturn(VirtualMachine.State.Stopped);
+ when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
+ when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
+ mockCandidateHostScope(vmId, hostId);
+ when(strategy.vmInstanceDetailsDao.findDetail(vmId, ApiConstants.BootType.UEFI.toString()))
+ .thenReturn(new VMInstanceDetailVO(vmId, ApiConstants.BootType.UEFI.toString(), "SECURE", true));
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_UEFI_ENABLE)).thenReturn(null);
+ when(hostDetailsDao.findDetail(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM))
+ .thenReturn(new DetailVO(hostId, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString()));
+
+ strategy.revertVMSnapshot(vmSnapshot);
+ }
+
+ @Test
+ public void testNotifyGuestRecoveryIssueIfNeededSendsAlert() {
+ CreateDiskOnlyVmSnapshotAnswer answer = mock(CreateDiskOnlyVmSnapshotAnswer.class);
+ UserVm userVm = mock(UserVm.class);
+ VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
+
+ when(answer.getDetails()).thenReturn("VM could not be thawed");
+ when(userVm.getUuid()).thenReturn("vm-uuid");
+ when(userVm.getDataCenterId()).thenReturn(1L);
+ when(userVm.getPodIdToDeployIn()).thenReturn(2L);
+ when(vmSnapshot.getUuid()).thenReturn("snapshot-uuid");
+
+ strategy.notifyGuestRecoveryIssueIfNeeded(answer, userVm, vmSnapshot);
+
+ verify(strategy.alertManager).sendAlert(eq(AlertManager.AlertType.ALERT_TYPE_VM_SNAPSHOT), eq(1L), eq(2L), anyString(), anyString());
+ }
+
+ private void mockCandidateHostScope(long vmId, long hostId) {
+ long poolId = 50L;
+ long clusterId = 60L;
+ long podId = 70L;
+ long dataCenterId = 80L;
+
+ VolumeVO rootVolume = mock(VolumeVO.class);
+ StoragePoolVO storagePool = mock(StoragePoolVO.class);
+ HostVO host = mock(HostVO.class);
+
+ when(rootVolume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(rootVolume.getPoolId()).thenReturn(poolId);
+ when(strategy.volumeDao.findByInstance(vmId)).thenReturn(List.of(rootVolume));
+ when(storagePoolDao.findById(poolId)).thenReturn(storagePool);
+ when(storagePool.getClusterId()).thenReturn(clusterId);
+ when(storagePool.getPodId()).thenReturn(podId);
+ when(storagePool.getDataCenterId()).thenReturn(dataCenterId);
+ when(host.getId()).thenReturn(hostId);
+ when(hostDao.listAllUpAndEnabledNonHAHosts(Host.Type.Routing, clusterId, podId, dataCenterId, null)).thenReturn(List.of(host));
+ }
+ }
diff --cc plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
index 3fb96d6cfd9,a9ed0c62cab..a5df0b3347f
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
@@@ -16,8 -16,8 +16,9 @@@
// under the License.
package com.cloud.hypervisor.kvm.resource;
+import static com.cloud.host.Host.HOST_CDROM_MAX_COUNT;
import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION;
+ import static com.cloud.host.Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM;
import static com.cloud.host.Host.HOST_OVFTOOL_VERSION;
import static com.cloud.host.Host.HOST_VDDK_LIB_DIR;
import static com.cloud.host.Host.HOST_VDDK_SUPPORT;
diff --cc plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java
index 26265f82467,46a7da70c61..e6edf8e6e39
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java
@@@ -22,10 -24,38 +24,37 @@@ import com.cloud.agent.api.VMSnapshotTO
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer;
import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
+ import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
+ import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
- import com.cloud.utils.exception.BackupException;
+ import com.cloud.utils.Pair;
import com.cloud.vm.VirtualMachine;
+ import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
+ import org.apache.cloudstack.storage.to.VolumeObjectTO;
+ import org.apache.cloudstack.utils.qemu.QemuCommand;
+ import org.apache.cloudstack.utils.qemu.QemuImg;
+ import org.apache.cloudstack.utils.qemu.QemuImgException;
+ import org.apache.cloudstack.utils.qemu.QemuImgFile;
+ import org.apache.commons.lang3.StringUtils;
+ import org.libvirt.Connect;
+ import org.libvirt.Domain;
+ import org.libvirt.LibvirtException;
+ import com.google.gson.JsonElement;
+ import com.google.gson.JsonObject;
+ import com.google.gson.JsonParser;
+
+ import java.io.File;
+ import java.io.IOException;
+ import java.nio.file.Files;
+ import java.nio.file.Path;
+ import java.nio.file.StandardCopyOption;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
-import java.util.UUID;
+
+ import com.cloud.storage.Volume;
@ResourceWrapper(handles = CreateDiskOnlyVmSnapshotCommand.class)
public class LibvirtCreateDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper<CreateDiskOnlyVmSnapshotCommand, Answer, LibvirtComputingResource> {
@@@ -34,14 -77,354 +76,335 @@@
public Answer execute(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
VirtualMachine.State state = cmd.getVmState();
+ if (VirtualMachine.State.Running.equals(state)) {
+ return takeDiskOnlyVmSnapshotOfRunningVm(cmd, resource);
+ }
+
+ return takeDiskOnlyVmSnapshotOfStoppedVm(cmd, resource);
+ }
+
+ protected Answer takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
+ String vmName = cmd.getVmName();
+ logger.info("Taking disk-only VM snapshot of running VM [{}].", vmName);
+
+ Domain dm = null;
+ String nvramSnapshotPath = null;
+ boolean suspendedByThisWrapper = false;
+ boolean filesystemsFrozenByThisWrapper = false;
+ CreateDiskOnlyVmSnapshotAnswer answer = null;
+ String postSnapshotCleanupIssue = null;
+ try {
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper();
+ Connect conn = libvirtUtilitiesHelper.getConnection();
+ List<VolumeObjectTO> volumeObjectTOS = cmd.getVolumeTOs();
+ List<LibvirtVMDef.DiskDef> disks = resource.getDisks(conn, vmName);
+
+ dm = resource.getDomain(conn, vmName);
+
+ if (dm == null) {
+ answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, false,
+ String.format("Creation of disk-only VM Snapshot failed as we could not find the VM [%s].", vmName), null, null);
+ return answer;
+ }
+
+ VMSnapshotTO target = cmd.getTarget();
- Pair<String, Map<String, Pair<Long, String>>> snapshotXmlAndVolumeToNewPathMap = createSnapshotXmlAndNewVolumePathMap(volumeObjectTOS, disks, target, resource);
++ Pair<String, Map<String, Long>> snapshotXmlAndVolumeToNewPathMap = resource.createSnapshotXmlAndNewVolumePathMap(cmd.getVolumeTosAndNewPaths(), disks,
++ target.getSnapshotName());
+ if (shouldFreezeVmFilesystemsForSnapshot(cmd)) {
+ // The guest-agent freeze flushes guest filesystems; suspend below prevents concurrent UEFI NVRAM writes.
+ freezeVmFilesystems(dm, vmName);
+ filesystemsFrozenByThisWrapper = true;
+ verifyVmFilesystemsFrozen(dm, vmName);
+ }
+ if (shouldSuspendVmForSnapshot(cmd)) {
+ suspendedByThisWrapper = suspendVmIfNeeded(dm);
+ }
+ nvramSnapshotPath = backupNvramIfNeeded(cmd, resource);
+
+ dm.snapshotCreateXML(snapshotXmlAndVolumeToNewPathMap.first(), getFlagsToUseForRunningVmSnapshotCreation(target, filesystemsFrozenByThisWrapper));
+
+ postSnapshotCleanupIssue = recoverVmAfterSnapshot(dm, vmName, suspendedByThisWrapper, filesystemsFrozenByThisWrapper, postSnapshotCleanupIssue);
+ filesystemsFrozenByThisWrapper = false;
+ suspendedByThisWrapper = false;
+
+ answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, snapshotXmlAndVolumeToNewPathMap.second(), nvramSnapshotPath);
+ } catch (LibvirtException | IOException e) {
+ String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage());
+ logger.error(errorMsg, e);
+ cleanupNvramSnapshotIfNeeded(cmd, resource, nvramSnapshotPath);
+ if (StringUtils.contains(e.getMessage(), "QEMU guest agent is not connected")) {
+ errorMsg = "QEMU guest agent is not connected. If the VM has been recently started, it might connect soon. Otherwise the VM does not have the" +
+ " guest agent installed; thus the QuiesceVM parameter is not supported.";
+ answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null, null);
+ } else {
+ answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, false, e.getMessage(), null, null);
+ }
+ } finally {
+ if (dm != null) {
+ postSnapshotCleanupIssue = recoverVmAfterSnapshot(dm, vmName, suspendedByThisWrapper, filesystemsFrozenByThisWrapper, postSnapshotCleanupIssue);
+ try {
+ dm.free();
+ } catch (LibvirtException l) {
+ logger.trace("Ignoring libvirt error.", l);
+ }
+ }
+ }
+
+ if (answer != null && StringUtils.isNotBlank(postSnapshotCleanupIssue)) {
+ answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, answer.getResult(),
- appendSnapshotOperationIssue(answer.getDetails(), postSnapshotCleanupIssue), answer.getMapVolumeToSnapshotSizeAndNewVolumePath(), answer.getNvramSnapshotPath());
++ appendSnapshotOperationIssue(answer.getDetails(), postSnapshotCleanupIssue), answer.getMapVolumeToSnapshotSize(), answer.getNvramSnapshotPath());
+ }
+ return answer;
+ }
+
+ protected Answer takeDiskOnlyVmSnapshotOfStoppedVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
+ String vmName = cmd.getVmName();
+ logger.info("Taking disk-only VM snapshot of stopped VM [{}].", vmName);
+
- Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath = new HashMap<>();
++ Map<String, Long> mapVolumeToSnapshotSize = new HashMap<>();
+ String nvramSnapshotPath = null;
+
+ List<VolumeObjectTO> volumeObjectTos = cmd.getVolumeTOs();
+ KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr();
+ try {
+ nvramSnapshotPath = backupNvramIfNeeded(cmd, resource);
- for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
++ for (Pair<VolumeObjectTO, String> volumeObjectTOAndNewPath : cmd.getVolumeTosAndNewPaths()) {
++ VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first();
+ PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
+ KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
+
- String snapshotPath = UUID.randomUUID().toString();
++ String snapshotPath = volumeObjectTOAndNewPath.second();
+ String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotPath);
+ QemuImgFile newDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2);
+
+ String currentDeltaFullPath = kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath());
+ QemuImgFile currentDelta = new QemuImgFile(currentDeltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2);
+
+ QemuImg qemuImg = new QemuImg(0);
+
+ logger.debug("Creating new delta for volume [{}] as part of the disk-only VM snapshot process for VM [{}].", volumeObjectTO.getUuid(), vmName);
+ qemuImg.create(newDelta, currentDelta);
+
- mapVolumeToSnapshotSizeAndNewVolumePath.put(volumeObjectTO.getUuid(), new Pair<>(getFileSize(currentDeltaFullPath), snapshotPath));
++ mapVolumeToSnapshotSize.put(volumeObjectTO.getUuid(), getFileSize(currentDeltaFullPath));
+ }
+ } catch (LibvirtException | QemuImgException | IOException e) {
+ logger.error("Exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
- for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
- Pair<Long, String> volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid());
++ for (Pair<VolumeObjectTO, String> volumeObjectTOAndNewPath : cmd.getVolumeTosAndNewPaths()) {
++ VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first();
+ PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
+ KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
+
- if (volSizeAndNewPath == null) {
- continue;
- }
+ try {
- Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second())));
++ Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTOAndNewPath.second())));
+ } catch (IOException ex) {
- logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex);
++ logger.warn("Tried to delete leftover snapshot at [{}] failed.", volumeObjectTOAndNewPath.second(), ex);
+ }
+ }
+ cleanupNvramSnapshotIfNeeded(cmd, resource, nvramSnapshotPath);
+ return new Answer(cmd, e);
+ }
+
- return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSizeAndNewVolumePath, nvramSnapshotPath);
++ return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSize, nvramSnapshotPath);
+ }
+
+ protected int getFlagsToUseForRunningVmSnapshotCreation(VMSnapshotTO target, boolean filesystemsFrozenByThisWrapper) {
+ int flags = target.getQuiescevm() && !filesystemsFrozenByThisWrapper ? Domain.SnapshotCreateFlags.QUIESCE : 0;
+ flags += Domain.SnapshotCreateFlags.DISK_ONLY +
+ Domain.SnapshotCreateFlags.ATOMIC +
+ Domain.SnapshotCreateFlags.NO_METADATA;
+ return flags;
+ }
+
- protected Pair<String, Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS, List<LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- StringBuilder stringBuilder = new StringBuilder();
- Map<String, Pair<Long, String>> volumeObjectToNewPathMap = new HashMap<>();
-
- for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) {
- LibvirtVMDef.DiskDef diskdef = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO);
- String newPath = UUID.randomUUID().toString();
- stringBuilder.append(String.format(TAG_DISK_SNAPSHOT, diskdef.getDiskLabel(), resource.getSnapshotTemporaryPath(diskdef.getDiskPath(), newPath)));
-
- long snapSize = getFileSize(diskdef.getDiskPath());
-
- volumeObjectToNewPathMap.put(volumeObjectTO.getUuid(), new Pair<>(snapSize, newPath));
- }
-
- String snapshotXml = String.format(SNAPSHOT_XML, target.getSnapshotName(), stringBuilder);
- return new Pair<>(snapshotXml, volumeObjectToNewPathMap);
- }
-
+ protected long getFileSize(String path) {
+ return new File(path).length();
+ }
+
+ protected String backupNvramIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws IOException, LibvirtException {
+ if (!cmd.isUefiEnabled()) {
+ return null;
+ }
+
+ String activeNvramPath = resource.getUefiNvramPath(cmd.getVmUuid());
+ if (StringUtils.isBlank(activeNvramPath) || !Files.exists(Path.of(activeNvramPath))) {
+ throw new IOException(String.format("Unable to find the active UEFI NVRAM file for VM [%s].", cmd.getVmName()));
+ }
+
+ VolumeObjectTO rootVolume = getRootVolume(cmd.getVolumeTOs());
+ PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) rootVolume.getDataStore();
+ KVMStoragePool storagePool = resource.getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
+
+ String nvramSnapshotPath = getNvramSnapshotRelativePath(cmd.getTarget().getId());
+ Path targetPath = Path.of(storagePool.getLocalPathFor(nvramSnapshotPath));
+ Files.createDirectories(targetPath.getParent());
+ Files.copy(Path.of(activeNvramPath), targetPath, StandardCopyOption.REPLACE_EXISTING);
+ return nvramSnapshotPath;
+ }
+
+ protected void cleanupNvramSnapshotIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource, String nvramSnapshotPath) {
+ if (StringUtils.isBlank(nvramSnapshotPath)) {
+ return;
+ }
+
+ try {
+ VolumeObjectTO rootVolume = getRootVolume(cmd.getVolumeTOs());
+ PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) rootVolume.getDataStore();
+ KVMStoragePool storagePool = resource.getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
+ Files.deleteIfExists(Path.of(storagePool.getLocalPathFor(nvramSnapshotPath)));
+ } catch (Exception e) {
+ logger.warn("Failed to clean up temporary NVRAM snapshot [{}] for VM [{}].", nvramSnapshotPath, cmd.getVmName(), e);
+ }
+ }
+
+ protected String getNvramSnapshotRelativePath(Long vmSnapshotId) {
+ return String.format("%s/%s.fd", NVRAM_SNAPSHOT_DIR, vmSnapshotId);
+ }
+
+ protected VolumeObjectTO getRootVolume(List<VolumeObjectTO> volumeObjectTos) {
+ return volumeObjectTos.stream()
+ .filter(volumeObjectTO -> Volume.Type.ROOT.equals(volumeObjectTO.getVolumeType()))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Unable to locate the root volume while handling the VM snapshot."));
+ }
+
+ protected boolean shouldSuspendVmForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return cmd.isUefiEnabled();
+ }
+
+ protected boolean shouldFreezeVmFilesystemsForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return cmd.isUefiEnabled() && cmd.getTarget().getQuiescevm();
+ }
+
+ protected boolean suspendVmIfNeeded(Domain domain) throws LibvirtException {
+ if (domain.getInfo().state == org.libvirt.DomainInfo.DomainState.VIR_DOMAIN_PAUSED) {
+ return false;
+ }
+
+ domain.suspend();
+ return true;
+ }
+
+ protected void freezeVmFilesystems(Domain domain, String vmName) throws LibvirtException, IOException {
+ String result = getResultOfQemuCommand(FreezeThawVMCommand.FREEZE, domain);
+ if (isQemuAgentErrorResponse(result)) {
+ throw new IOException(String.format("Failed to freeze VM [%s] filesystems before taking the disk-only VM snapshot. Result: %s", vmName, result));
+ }
+ }
+
+ protected void verifyVmFilesystemsFrozen(Domain domain, String vmName) throws LibvirtException, IOException {
+ String status = getResultOfQemuCommand(FreezeThawVMCommand.STATUS, domain);
+ if (StringUtils.isBlank(status)) {
+ throw new IOException(String.format("Failed to verify VM [%s] filesystem freeze state before taking the disk-only VM snapshot. Result: %s", vmName, status));
+ }
+
+ JsonObject statusObject;
try {
- if (VirtualMachine.State.Running.equals(state)) {
- return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, resource.createDiskOnlyVmSnapshotForRunningVm(cmd.getVolumeTosAndNewPaths(), cmd.getVmName(),
- cmd.getTarget().getSnapshotName(), cmd.getTarget().getQuiescevm()));
+ JsonElement statusElement = new JsonParser().parse(status);
+ if (!statusElement.isJsonObject()) {
+ throw new IOException(String.format("Failed to verify VM [%s] filesystem freeze state before taking the disk-only VM snapshot. Result: %s", vmName, status));
}
- return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, resource.createDiskOnlyVMSnapshotOfStoppedVm(cmd.getVolumeTosAndNewPaths(), cmd.getVmName()));
- } catch (BackupException ex) {
- return new Answer(cmd, ex);
+ statusObject = statusElement.getAsJsonObject();
+ } catch (RuntimeException e) {
+ throw new IOException(String.format("Failed to verify VM [%s] filesystem freeze state before taking the disk-only VM snapshot. Result: %s", vmName, status), e);
+ }
+
+ if (statusObject.has("error")) {
+ throw new IOException(String.format("Failed to verify VM [%s] filesystem freeze state before taking the disk-only VM snapshot. Result: %s", vmName, status));
+ }
+
+ JsonElement returnElement = statusObject.get("return");
+ if (returnElement == null || !returnElement.isJsonPrimitive() || !returnElement.getAsJsonPrimitive().isString()) {
+ throw new IOException(String.format("Failed to verify VM [%s] filesystem freeze state before taking the disk-only VM snapshot. Result: %s", vmName, status));
+ }
+
+ String statusResult = returnElement.getAsString();
+ if (!FreezeThawVMCommand.FREEZE.equals(statusResult)) {
+ throw new IOException(String.format("Failed to freeze VM [%s] filesystems before taking the disk-only VM snapshot. Status: %s", vmName, statusResult));
+ }
+ }
+
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName, boolean filesystemsFrozenByThisWrapper) {
+ if (!filesystemsFrozenByThisWrapper) {
+ return true;
+ }
+ return thawVmFilesystemsIfNeeded(domain, vmName);
+ }
+
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName) {
+ try {
+ String result = getResultOfQemuCommand(FreezeThawVMCommand.THAW, domain);
+ if (isQemuAgentErrorResponse(result)) {
+ logger.warn("Failed to thaw VM [{}] filesystems after taking the disk-only VM snapshot. Result: {}", vmName, result);
+ return false;
+ }
+ return true;
+ } catch (LibvirtException e) {
+ logger.warn("Failed to thaw VM [{}] filesystems after taking the disk-only VM snapshot.", vmName, e);
+ return false;
+ }
+ }
+
+ protected boolean isQemuAgentErrorResponse(String result) {
+ if (StringUtils.isBlank(result) || result.startsWith("error")) {
+ return true;
+ }
+
+ try {
+ JsonElement resultElement = new JsonParser().parse(result);
+ return resultElement.isJsonObject() && resultElement.getAsJsonObject().has("error");
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ protected String getResultOfQemuCommand(String cmd, Domain domain) throws LibvirtException {
+ if (cmd.equals(FreezeThawVMCommand.FREEZE)) {
+ return domain.qemuAgentCommand(QemuCommand.buildQemuCommand(QemuCommand.AGENT_FREEZE, null), 10, 0);
+ } else if (cmd.equals(FreezeThawVMCommand.THAW)) {
+ return domain.qemuAgentCommand(QemuCommand.buildQemuCommand(QemuCommand.AGENT_THAW, null), 10, 0);
+ } else if (cmd.equals(FreezeThawVMCommand.STATUS)) {
+ return domain.qemuAgentCommand(QemuCommand.buildQemuCommand(QemuCommand.AGENT_FREEZE_STATUS, null), 10, 0);
+ }
+ return null;
+ }
+
+ protected boolean resumeVmIfNeeded(Domain domain, String vmName, boolean suspendedByThisWrapper) {
+ if (!suspendedByThisWrapper) {
+ return true;
+ }
+ return resumeVmIfNeeded(domain, vmName);
+ }
+
+ protected boolean resumeVmIfNeeded(Domain domain, String vmName) {
+ try {
+ if (domain.getInfo().state == org.libvirt.DomainInfo.DomainState.VIR_DOMAIN_PAUSED) {
+ domain.resume();
+ }
+ return true;
+ } catch (LibvirtException e) {
+ logger.warn("Failed to resume VM [{}] after taking the disk-only VM snapshot.", vmName, e);
+ return false;
+ }
+ }
+
+ protected String recoverVmAfterSnapshot(Domain domain, String vmName, boolean suspendedByThisWrapper, boolean filesystemsFrozenByThisWrapper, String currentIssue) {
+ if (suspendedByThisWrapper && !resumeVmIfNeeded(domain, vmName)) {
+ currentIssue = appendSnapshotOperationIssue(currentIssue,
+ String.format("VM [%s] could not be resumed after taking the disk-only snapshot. Guest may still be paused.", vmName));
+ }
+ if (filesystemsFrozenByThisWrapper && !thawVmFilesystemsIfNeeded(domain, vmName)) {
+ currentIssue = appendSnapshotOperationIssue(currentIssue,
+ String.format("VM [%s] filesystems could not be thawed after taking the disk-only snapshot. Guest may still be frozen.", vmName));
+ }
+ return currentIssue;
+ }
+
+ protected String appendSnapshotOperationIssue(String currentIssue, String newIssue) {
+ if (StringUtils.isBlank(newIssue)) {
+ return currentIssue;
+ }
+ if (StringUtils.isBlank(currentIssue)) {
+ return newIssue;
}
+ return currentIssue + " " + newIssue;
}
}
diff --cc plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java
index e88981f5b20,d8e5bed2ef0..1a562f1d330
--- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java
+++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/linux/KVMHostInfo.java
@@@ -140,12 -140,9 +140,12 @@@ public class KVMHostInfo
long speed = 0L;
LOGGER.info("Fetching CPU speed from command \"lscpu\".");
try {
- String command = "lscpu | grep -i 'Model name' | head -n 1 | grep -E -o '[[:digit:]].[[:digit:]]+GHz' | sed 's/GHz//g'";
+ String command = "lscpu | grep -i 'CPU max MHz' | head -n 1 | sed 's/^.*: //' | xargs";
+ if(isHostS390x()) {
+ command = "lscpu | grep 'CPU dynamic MHz' | cut -d ':' -f 2 | tr -d ' ' | awk '{printf \"%.1f\\n\", $1 / 1000}'";
+ }
String result = Script.runSimpleBashScript(command);
- speed = (long) (Float.parseFloat(result) * 1000);
+ speed = (long) (Float.parseFloat(result));
LOGGER.info(String.format("Command [%s] resulted in the value [%s] for CPU speed.", command, speed));
return speed;
} catch (NullPointerException | NumberFormatException e) {
diff --cc plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDiskOnlyVMSnapshotCommandWrapperTest.java
index 00000000000,e447e6f6cfd..35c8f6521fb
mode 000000,100644..100644
--- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDiskOnlyVMSnapshotCommandWrapperTest.java
+++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDiskOnlyVMSnapshotCommandWrapperTest.java
@@@ -1,0 -1,816 +1,821 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+ package com.cloud.hypervisor.kvm.resource.wrapper;
+
+ import static org.junit.Assert.assertEquals;
+ import static org.junit.Assert.assertFalse;
+ import static org.junit.Assert.assertTrue;
+ import static org.junit.Assert.fail;
++import static org.mockito.ArgumentMatchers.any;
+ import static org.mockito.ArgumentMatchers.anyInt;
+ import static org.mockito.ArgumentMatchers.anyString;
++import static org.mockito.Mockito.doReturn;
+ import static org.mockito.Mockito.doThrow;
+ import static org.mockito.Mockito.mock;
+ import static org.mockito.Mockito.never;
+ import static org.mockito.Mockito.verify;
+ import static org.mockito.Mockito.when;
+
+ import java.io.IOException;
+ import java.nio.file.Files;
+ import java.nio.file.Path;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.List;
+ import java.util.UUID;
+
+ import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
+ import org.apache.cloudstack.storage.to.SnapshotObjectTO;
+ import org.apache.cloudstack.storage.to.VolumeObjectTO;
+ import org.junit.Test;
+ import org.libvirt.Domain;
+ import org.libvirt.DomainInfo;
+
+ import com.cloud.agent.api.VMSnapshotTO;
+ import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand;
+ import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer;
+ import com.cloud.agent.api.storage.DeleteDiskOnlyVmSnapshotCommand;
+ import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand;
+ import com.cloud.agent.api.to.DataTO;
+ import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+ import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
+ import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
+ import com.cloud.storage.Storage;
+ import com.cloud.storage.Volume;
+ import com.cloud.utils.Pair;
+
+ public class LibvirtDiskOnlyVMSnapshotCommandWrapperTest {
+
+ @Test
+ public void testBackupNvramIfNeededCopiesActiveNvram() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO vmSnapshotTO = mock(VMSnapshotTO.class);
+ PrimaryDataStoreTO dataStoreTO = mock(PrimaryDataStoreTO.class);
+ VolumeObjectTO rootVolume = mock(VolumeObjectTO.class);
+
+ Path activeNvram = Files.createTempFile("active-", ".fd");
+ Files.writeString(activeNvram, "snapshot-nvram");
+ Path poolDirectory = Files.createTempDirectory("pool-");
+
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getTarget()).thenReturn(vmSnapshotTO);
+ when(vmSnapshotTO.getId()).thenReturn(42L);
+ when(command.getVolumeTOs()).thenReturn(List.of(rootVolume));
+ when(rootVolume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(rootVolume.getDataStore()).thenReturn(dataStoreTO);
+ when(dataStoreTO.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(dataStoreTO.getUuid()).thenReturn("pool-uuid");
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(activeNvram.toString());
+ when(resource.getStoragePoolMgr()).thenReturn(storagePoolManager);
+ when(storagePoolManager.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, "pool-uuid")).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor(anyString())).thenAnswer(invocation -> poolDirectory.resolve(invocation.getArgument(0, String.class)).toString());
+
+ String nvramSnapshotPath = wrapper.backupNvramIfNeeded(command, resource);
+
+ assertEquals(".cloudstack-vm-snapshot-nvram/42.fd", nvramSnapshotPath);
+ assertEquals("snapshot-nvram", Files.readString(poolDirectory.resolve(nvramSnapshotPath)));
+ }
+
+ @Test(expected = IOException.class)
+ public void testBackupNvramIfNeededFailsWhenUefiNvramIsMissing() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn("/tmp/" + UUID.randomUUID() + ".fd");
+
+ wrapper.backupNvramIfNeeded(command, resource);
+ }
+
+ @Test(expected = IOException.class)
+ public void testValidateNvramRevertStateFailsForLegacySnapshotsOnUefiVms() throws Exception {
+ LibvirtRevertDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtRevertDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ RevertDiskOnlyVmSnapshotCommand command = mock(RevertDiskOnlyVmSnapshotCommand.class);
+
+ Path activeNvram = Files.createTempFile("active-", ".fd");
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getNvramSnapshotPath()).thenReturn(null);
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(activeNvram.toString());
+
+ wrapper.validateNvramRevertState(command, resource, null, mock(KVMStoragePoolManager.class));
+ }
+
+ @Test(expected = IOException.class)
+ public void testValidateNvramRevertStateFailsForLegacySnapshotsOnFallbackHostsForUefiVms() throws Exception {
+ LibvirtRevertDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtRevertDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ RevertDiskOnlyVmSnapshotCommand command = mock(RevertDiskOnlyVmSnapshotCommand.class);
+
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getNvramSnapshotPath()).thenReturn(null);
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(Path.of("/tmp", UUID.randomUUID() + ".fd").toString());
+
+ wrapper.validateNvramRevertState(command, resource, null, mock(KVMStoragePoolManager.class));
+ }
+
+ @Test
+ public void testValidateNvramRevertStateAllowsFallbackHostsWithoutLocalNvram() throws Exception {
+ LibvirtRevertDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtRevertDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ RevertDiskOnlyVmSnapshotCommand command = mock(RevertDiskOnlyVmSnapshotCommand.class);
+ SnapshotObjectTO rootSnapshot = mock(SnapshotObjectTO.class);
+
+ Path poolDirectory = Files.createTempDirectory("pool-");
+ Path snapshotNvram = poolDirectory.resolve("nvram/42.fd");
+ Files.createDirectories(snapshotNvram.getParent());
+ Files.writeString(snapshotNvram, "snapshot");
+
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(poolDirectory.resolve("missing").resolve("vm-uuid.fd").toString());
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(rootSnapshot, storagePoolManager)).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor("nvram/42.fd")).thenReturn(snapshotNvram.toString());
+
+ wrapper.validateNvramRevertState(command, resource, rootSnapshot, storagePoolManager);
+ }
+
+ @Test
+ public void testRestoreNvramIfNeededRestoresSnapshotBytes() throws Exception {
+ LibvirtRevertDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtRevertDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ RevertDiskOnlyVmSnapshotCommand command = mock(RevertDiskOnlyVmSnapshotCommand.class);
+ SnapshotObjectTO rootSnapshot = mock(SnapshotObjectTO.class);
+
+ Path activeNvram = Files.createTempFile("active-", ".fd");
+ Files.writeString(activeNvram, "current");
+ Path poolDirectory = Files.createTempDirectory("pool-");
+ Path snapshotNvram = poolDirectory.resolve("nvram/42.fd");
+ Files.createDirectories(snapshotNvram.getParent());
+ Files.writeString(snapshotNvram, "snapshot");
+
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(activeNvram.toString());
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(rootSnapshot, storagePoolManager)).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor("nvram/42.fd")).thenReturn(snapshotNvram.toString());
+
+ wrapper.restoreNvramIfNeeded(command, resource, rootSnapshot, storagePoolManager);
+
+ assertEquals("snapshot", Files.readString(activeNvram));
+ }
+
+ @Test
+ public void testRestoreNvramIfNeededCreatesMissingActiveNvramFile() throws Exception {
+ LibvirtRevertDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtRevertDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ RevertDiskOnlyVmSnapshotCommand command = mock(RevertDiskOnlyVmSnapshotCommand.class);
+ SnapshotObjectTO rootSnapshot = mock(SnapshotObjectTO.class);
+
+ Path poolDirectory = Files.createTempDirectory("pool-");
+ Path snapshotNvram = poolDirectory.resolve("nvram/42.fd");
+ Path activeNvram = poolDirectory.resolve("target").resolve("vm-uuid.fd");
+ Files.createDirectories(snapshotNvram.getParent());
+ Files.writeString(snapshotNvram, "snapshot");
+
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(activeNvram.toString());
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(rootSnapshot, storagePoolManager)).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor("nvram/42.fd")).thenReturn(snapshotNvram.toString());
+
+ wrapper.restoreNvramIfNeeded(command, resource, rootSnapshot, storagePoolManager);
+
+ assertEquals("snapshot", Files.readString(activeNvram));
+ }
+
+ @Test
+ public void testRestoreNvramIfNeededPreservesActiveNvramWhenCopyFails() throws Exception {
+ LibvirtRevertDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtRevertDiskOnlyVMSnapshotCommandWrapper() {
+ @Override
+ protected void copyNvramSnapshotToTemporaryPath(Path snapshotNvramPath, Path temporaryNvramPath) throws IOException {
+ Files.writeString(temporaryNvramPath, "partial");
+ throw new IOException("copy failed");
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ RevertDiskOnlyVmSnapshotCommand command = mock(RevertDiskOnlyVmSnapshotCommand.class);
+ SnapshotObjectTO rootSnapshot = mock(SnapshotObjectTO.class);
+
+ Path activeNvram = Files.createTempFile("active-", ".fd");
+ Files.writeString(activeNvram, "current");
+ Path poolDirectory = Files.createTempDirectory("pool-");
+ Path snapshotNvram = poolDirectory.resolve("nvram/42.fd");
+ Files.createDirectories(snapshotNvram.getParent());
+ Files.writeString(snapshotNvram, "snapshot");
+
+ when(command.getVmUuid()).thenReturn("vm-uuid");
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+ when(resource.getUefiNvramPath("vm-uuid")).thenReturn(activeNvram.toString());
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(rootSnapshot, storagePoolManager)).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor("nvram/42.fd")).thenReturn(snapshotNvram.toString());
+
+ try {
+ wrapper.restoreNvramIfNeeded(command, resource, rootSnapshot, storagePoolManager);
+ fail("Expected restore to fail when the snapshot copy fails.");
+ } catch (IOException expected) {
+ assertEquals("current", Files.readString(activeNvram));
+ }
+ }
+
+ @Test
+ public void testDeleteNvramSnapshotIfNeededDeletesSidecar() throws Exception {
+ LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ DeleteDiskOnlyVmSnapshotCommand command = mock(DeleteDiskOnlyVmSnapshotCommand.class);
+ SnapshotObjectTO rootSnapshot = mock(SnapshotObjectTO.class);
+ VolumeObjectTO rootVolume = mock(VolumeObjectTO.class);
+
+ Path poolDirectory = Files.createTempDirectory("pool-");
+ Path snapshotNvram = poolDirectory.resolve("nvram/42.fd");
+ Files.createDirectories(snapshotNvram.getParent());
+ Files.writeString(snapshotNvram, "snapshot");
+
+ when(command.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+ when(rootSnapshot.getVolume()).thenReturn(rootVolume);
+ when(rootVolume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(rootSnapshot, storagePoolManager)).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor("nvram/42.fd")).thenReturn(snapshotNvram.toString());
+
+ wrapper.deleteNvramSnapshotIfNeeded(command, resource, storagePoolManager, List.<DataTO>of(rootSnapshot));
+
+ assertFalse(Files.exists(snapshotNvram));
+ assertTrue(Files.exists(poolDirectory));
+ }
+
+ @Test
+ public void testDeleteNvramSnapshotIfNeededDeletesSidecarUsingPrimaryDataStore() throws Exception {
+ LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper();
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ KVMStoragePoolManager storagePoolManager = mock(KVMStoragePoolManager.class);
+ KVMStoragePool storagePool = mock(KVMStoragePool.class);
+ DeleteDiskOnlyVmSnapshotCommand command = mock(DeleteDiskOnlyVmSnapshotCommand.class);
+ PrimaryDataStoreTO primaryDataStoreTO = mock(PrimaryDataStoreTO.class);
+
+ Path poolDirectory = Files.createTempDirectory("pool-");
+ Path snapshotNvram = poolDirectory.resolve("nvram/42.fd");
+ Files.createDirectories(snapshotNvram.getParent());
+ Files.writeString(snapshotNvram, "snapshot");
+
+ when(command.getNvramSnapshotPath()).thenReturn("nvram/42.fd");
+ when(command.getPrimaryDataStore()).thenReturn(primaryDataStoreTO);
+ when(primaryDataStoreTO.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(primaryDataStoreTO.getUuid()).thenReturn("pool-uuid");
+ when(storagePoolManager.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, "pool-uuid")).thenReturn(storagePool);
+ when(storagePool.getLocalPathFor("nvram/42.fd")).thenReturn(snapshotNvram.toString());
+
+ wrapper.deleteNvramSnapshotIfNeeded(command, resource, storagePoolManager, List.of());
+
+ assertFalse(Files.exists(snapshotNvram));
+ }
+
+ @Test
+ public void testResumeVmIfNeededOnlyResumesWhenWrapperSuspendedVm() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ Domain domain = mock(Domain.class);
+
+ wrapper.resumeVmIfNeeded(domain, "vm-name", false);
+
+ verify(domain, never()).resume();
+ verify(domain, never()).getInfo();
+ }
+
+ @Test
+ public void testSuspendVmIfNeededSkipsAlreadyPausedVm() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ Domain domain = mock(Domain.class);
+ DomainInfo domainInfo = new DomainInfo();
+ domainInfo.state = DomainInfo.DomainState.VIR_DOMAIN_PAUSED;
+ when(domain.getInfo()).thenReturn(domainInfo);
+
+ assertFalse(wrapper.suspendVmIfNeeded(domain));
+ verify(domain, never()).suspend();
+ }
+
+ @Test
+ public void testShouldSuspendVmForSnapshotWhenUefiAndNotQuiesced() {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getTarget()).thenReturn(target);
+ when(target.getQuiescevm()).thenReturn(false);
+
+ assertTrue(wrapper.shouldSuspendVmForSnapshot(command));
+ }
+
+ @Test
+ public void testShouldSuspendVmForSnapshotWhenQuiesceIsRequested() {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getTarget()).thenReturn(target);
+ when(target.getQuiescevm()).thenReturn(true);
+
+ assertTrue(wrapper.shouldSuspendVmForSnapshot(command));
+ }
+
+ @Test
+ public void testShouldFreezeVmFilesystemsForSnapshotWhenQuiesceIsRequested() {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(command.getTarget()).thenReturn(target);
+ when(target.getQuiescevm()).thenReturn(true);
+
+ assertTrue(wrapper.shouldFreezeVmFilesystemsForSnapshot(command));
+ }
+
+ @Test
+ public void testGetFlagsToUseForRunningVmSnapshotCreationOmitsLibvirtQuiesceWhenAlreadyFrozen() {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper();
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(target.getQuiescevm()).thenReturn(true);
+
+ int flags = wrapper.getFlagsToUseForRunningVmSnapshotCreation(target, true);
+
+ assertEquals(0, flags & Domain.SnapshotCreateFlags.QUIESCE);
+ assertTrue((flags & Domain.SnapshotCreateFlags.DISK_ONLY) != 0);
+ assertTrue((flags & Domain.SnapshotCreateFlags.ATOMIC) != 0);
+ assertTrue((flags & Domain.SnapshotCreateFlags.NO_METADATA) != 0);
+ }
+
+ @Test
+ public void testFreezeAndVerifyVmFilesystemsSucceedsWhenGuestAgentReportsFrozen() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
+ @Override
+ protected String getResultOfQemuCommand(String cmd, Domain domain) {
+ if ("status".equals(cmd)) {
+ return "{\"return\":\"frozen\"}";
+ }
+ return "{\"return\":0}";
+ }
+ };
+
+ Domain domain = mock(Domain.class);
+ wrapper.freezeVmFilesystems(domain, "vm-name");
+ wrapper.verifyVmFilesystemsFrozen(domain, "vm-name");
+ }
+
+ @Test
+ public void testFreezeVmFilesystemsFailsForQemuErrorResponse() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
+ @Override
+ protected String getResultOfQemuCommand(String cmd, Domain domain) {
+ return "{\"error\":{\"class\":\"GenericError\",\"desc\":\"guest agent failure\"}}";
+ }
+ };
+
+ try {
+ wrapper.freezeVmFilesystems(mock(Domain.class), "vm-name");
+ fail("QEMU guest agent error responses must be treated as freeze failures.");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Failed to freeze VM [vm-name] filesystems"));
+ }
+ }
+
+ @Test
+ public void testThawVmFilesystemsFailsForQemuErrorResponse() {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
+ @Override
+ protected String getResultOfQemuCommand(String cmd, Domain domain) {
+ return "{\"error\":{\"class\":\"GenericError\",\"desc\":\"guest agent failure\"}}";
+ }
+ };
+
+ assertFalse(wrapper.thawVmFilesystemsIfNeeded(mock(Domain.class), "vm-name"));
+ }
+
+ @Test
+ public void testTakeDiskOnlyVmSnapshotOfRunningVmThawsWhenFreezeVerificationFails() throws Exception {
+ final boolean[] thawCalled = {false};
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
- @Override
- protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
- List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
- }
++// @Override
++// protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
++// List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
++// return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
++// }
+
+ @Override
+ protected boolean shouldSuspendVmForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldFreezeVmFilesystemsForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return true;
+ }
+
+ @Override
+ protected void freezeVmFilesystems(Domain domain, String vmName) {
+ }
+
+ @Override
+ protected void verifyVmFilesystemsFrozen(Domain domain, String vmName) throws IOException {
+ throw new IOException("status verification failed");
+ }
+
+ @Override
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName) {
+ thawCalled[0] = true;
+ return true;
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ org.libvirt.Connect connect = mock(org.libvirt.Connect.class);
+ Domain domain = mock(Domain.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getVolumeTOs()).thenReturn(List.of());
+ when(command.getTarget()).thenReturn(target);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getConnection()).thenReturn(connect);
+ when(resource.getDisks(connect, "vm-name")).thenReturn(List.of());
+ when(resource.getDomain(connect, "vm-name")).thenReturn(domain);
+
+ CreateDiskOnlyVmSnapshotAnswer answer = (CreateDiskOnlyVmSnapshotAnswer) wrapper.takeDiskOnlyVmSnapshotOfRunningVm(command, resource);
+
+ assertFalse(answer.getResult());
+ assertTrue("Thaw must be attempted after a successful freeze followed by verification failure", thawCalled[0]);
+ }
+
+ @Test
+ public void testVerifyVmFilesystemsFrozenFailsForQemuErrorResponse() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
+ @Override
+ protected String getResultOfQemuCommand(String cmd, Domain domain) {
+ return "{\"error\":{\"class\":\"GenericError\",\"desc\":\"guest agent failure\"}}";
+ }
+ };
+
+ try {
+ wrapper.verifyVmFilesystemsFrozen(mock(Domain.class), "vm-name");
+ fail("QEMU guest agent error responses must be treated as IO failures.");
+ } catch (IOException e) {
+ assertTrue(e.getMessage().contains("Failed to verify VM [vm-name] filesystem freeze state"));
+ }
+ }
+
+ @Test
+ public void testTakeDiskOnlyVmSnapshotOfRunningVmReturnsFailureAnswerWhenFreezeStatusJsonIsMalformed() throws Exception {
+ final boolean[] thawCalled = {false};
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
- @Override
- protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
- List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
- }
++// @Override
++// protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
++// List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
++// return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
++// }
+
+ @Override
+ protected boolean shouldSuspendVmForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldFreezeVmFilesystemsForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return true;
+ }
+
+ @Override
+ protected void freezeVmFilesystems(Domain domain, String vmName) {
+ }
+
+ @Override
+ protected String getResultOfQemuCommand(String cmd, Domain domain) {
+ return "not-json";
+ }
+
+ @Override
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName) {
+ thawCalled[0] = true;
+ return true;
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ org.libvirt.Connect connect = mock(org.libvirt.Connect.class);
+ Domain domain = mock(Domain.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getVolumeTOs()).thenReturn(List.of());
+ when(command.getTarget()).thenReturn(target);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getConnection()).thenReturn(connect);
+ when(resource.getDisks(connect, "vm-name")).thenReturn(List.of());
+ when(resource.getDomain(connect, "vm-name")).thenReturn(domain);
+
+ CreateDiskOnlyVmSnapshotAnswer answer = (CreateDiskOnlyVmSnapshotAnswer) wrapper.takeDiskOnlyVmSnapshotOfRunningVm(command, resource);
+
+ assertFalse(answer.getResult());
+ assertTrue(answer.getDetails().contains("Failed to verify VM [vm-name] filesystem freeze state"));
+ assertTrue("Thaw must be attempted when freeze verification fails on malformed JSON", thawCalled[0]);
+ }
+
+ @Test
+ public void testTakeDiskOnlyVmSnapshotOfRunningVmSuspendsBeforeNvramCopyForQuiescedUefiSnapshots() throws Exception {
+ List<String> operations = new ArrayList<>();
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
- @Override
- protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
- List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
- }
++// @Override
++// protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
++// List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
++// return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
++// }
+
+ @Override
+ protected void freezeVmFilesystems(Domain domain, String vmName) {
+ operations.add("freeze");
+ }
+
+ @Override
+ protected void verifyVmFilesystemsFrozen(Domain domain, String vmName) {
+ operations.add("verify");
+ }
+
+ @Override
+ protected boolean suspendVmIfNeeded(Domain domain) {
+ operations.add("suspend");
+ return true;
+ }
+
+ @Override
+ protected String backupNvramIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
+ operations.add("backup");
+ return "nvram/42.fd";
+ }
+
+ @Override
+ protected void cleanupNvramSnapshotIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource, String nvramSnapshotPath) {
+ }
+
+ @Override
+ protected boolean resumeVmIfNeeded(Domain domain, String vmName) {
+ operations.add("resume");
+ return true;
+ }
+
+ @Override
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName) {
+ operations.add("thaw");
+ return true;
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ org.libvirt.Connect connect = mock(org.libvirt.Connect.class);
+ Domain domain = mock(Domain.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getVolumeTOs()).thenReturn(List.of());
+ when(command.getTarget()).thenReturn(target);
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(target.getQuiescevm()).thenReturn(true);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getConnection()).thenReturn(connect);
+ when(resource.getDisks(connect, "vm-name")).thenReturn(List.of());
+ when(resource.getDomain(connect, "vm-name")).thenReturn(domain);
+ doThrow(mock(org.libvirt.LibvirtException.class)).when(domain).snapshotCreateXML(anyString(), anyInt());
++ doReturn(new Pair<>("<domainsnapshot/>", Collections.emptyMap())).when(resource).createSnapshotXmlAndNewVolumePathMap(any(), any(), any());
+
+ CreateDiskOnlyVmSnapshotAnswer answer = (CreateDiskOnlyVmSnapshotAnswer) wrapper.takeDiskOnlyVmSnapshotOfRunningVm(command, resource);
+
+ assertFalse(answer.getResult());
+ assertEquals(List.of("freeze", "verify", "suspend", "backup", "resume", "thaw"), operations);
+ }
+
+ @Test
+ public void testTakeDiskOnlyVmSnapshotOfRunningVmReturnsSuccessWithWarningWhenThawFails() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
- @Override
- protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
- List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
- }
++// @Override
++// protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
++// List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
++// return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
++// }
+
+ @Override
+ protected boolean shouldSuspendVmForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldFreezeVmFilesystemsForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return true;
+ }
+
+ @Override
+ protected void freezeVmFilesystems(Domain domain, String vmName) {
+ }
+
+ @Override
+ protected void verifyVmFilesystemsFrozen(Domain domain, String vmName) {
+ }
+
+ @Override
+ protected String backupNvramIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
+ return null;
+ }
+
+ @Override
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName) {
+ return false;
+ }
+
+ @Override
+ protected boolean thawVmFilesystemsIfNeeded(Domain domain, String vmName, boolean frozen) {
+ return false;
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ org.libvirt.Connect connect = mock(org.libvirt.Connect.class);
+ Domain domain = mock(Domain.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getVolumeTOs()).thenReturn(List.of());
+ when(command.getTarget()).thenReturn(target);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getConnection()).thenReturn(connect);
+ when(resource.getDisks(connect, "vm-name")).thenReturn(List.of());
+ when(resource.getDomain(connect, "vm-name")).thenReturn(domain);
++ doReturn(new Pair<>("<domainsnapshot/>", Collections.emptyMap())).when(resource).createSnapshotXmlAndNewVolumePathMap(any(), any(), any());
+
+ CreateDiskOnlyVmSnapshotAnswer answer = (CreateDiskOnlyVmSnapshotAnswer) wrapper.takeDiskOnlyVmSnapshotOfRunningVm(command, resource);
+
+ assertTrue("Snapshot metadata must still be returned when thaw fails after snapshot creation", answer.getResult());
+ assertTrue(answer.getDetails().contains("could not be thawed"));
+ }
+
+ @Test
+ public void testTakeDiskOnlyVmSnapshotOfRunningVmReturnsSuccessWithWarningWhenResumeFails() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
- @Override
- protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
- List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
- }
++// @Override
++// protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
++// List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
++// return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
++// }
+
+ @Override
+ protected boolean shouldSuspendVmForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return true;
+ }
+
+ @Override
+ protected boolean suspendVmIfNeeded(Domain domain) {
+ return true;
+ }
+
+ @Override
+ protected boolean shouldFreezeVmFilesystemsForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return false;
+ }
+
+ @Override
+ protected String backupNvramIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) {
+ return null;
+ }
+
+ @Override
+ protected boolean resumeVmIfNeeded(Domain domain, String vmName) {
+ return false;
+ }
+
+ @Override
+ protected boolean resumeVmIfNeeded(Domain domain, String vmName, boolean suspended) {
+ return false;
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ org.libvirt.Connect connect = mock(org.libvirt.Connect.class);
+ Domain domain = mock(Domain.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getVolumeTOs()).thenReturn(List.of());
+ when(command.getTarget()).thenReturn(target);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getConnection()).thenReturn(connect);
+ when(resource.getDisks(connect, "vm-name")).thenReturn(List.of());
+ when(resource.getDomain(connect, "vm-name")).thenReturn(domain);
++ doReturn(new Pair<>("<domainsnapshot/>", Collections.emptyMap())).when(resource).createSnapshotXmlAndNewVolumePathMap(any(), any(), any());
+
+ CreateDiskOnlyVmSnapshotAnswer answer = (CreateDiskOnlyVmSnapshotAnswer) wrapper.takeDiskOnlyVmSnapshotOfRunningVm(command, resource);
+
+ assertTrue("Snapshot metadata must still be returned when resume fails after snapshot creation", answer.getResult());
+ assertTrue(answer.getDetails().contains("could not be resumed"));
+ }
+
+ @Test
+ public void testTakeDiskOnlyVmSnapshotOfRunningVmHandlesNullErrorMessage() throws Exception {
+ LibvirtCreateDiskOnlyVMSnapshotCommandWrapper wrapper = new LibvirtCreateDiskOnlyVMSnapshotCommandWrapper() {
- @Override
- protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
- List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
- return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
- }
++// @Override
++// protected Pair<String, java.util.Map<String, Pair<Long, String>>> createSnapshotXmlAndNewVolumePathMap(List<VolumeObjectTO> volumeObjectTOS,
++// List<com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef> disks, VMSnapshotTO target, LibvirtComputingResource resource) {
++// return new Pair<>("<domainsnapshot/>", Collections.emptyMap());
++// }
+
+ @Override
+ protected boolean shouldSuspendVmForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldFreezeVmFilesystemsForSnapshot(CreateDiskOnlyVmSnapshotCommand cmd) {
+ return false;
+ }
+
+ @Override
+ protected String backupNvramIfNeeded(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws IOException {
+ throw new IOException();
+ }
+ };
+ LibvirtComputingResource resource = mock(LibvirtComputingResource.class);
+ LibvirtUtilitiesHelper libvirtUtilitiesHelper = mock(LibvirtUtilitiesHelper.class);
+ org.libvirt.Connect connect = mock(org.libvirt.Connect.class);
+ Domain domain = mock(Domain.class);
+ CreateDiskOnlyVmSnapshotCommand command = mock(CreateDiskOnlyVmSnapshotCommand.class);
+ VMSnapshotTO target = mock(VMSnapshotTO.class);
+
+ when(command.getVmName()).thenReturn("vm-name");
+ when(command.getVolumeTOs()).thenReturn(List.of());
+ when(command.getTarget()).thenReturn(target);
+ when(command.isUefiEnabled()).thenReturn(true);
+ when(resource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
+ when(libvirtUtilitiesHelper.getConnection()).thenReturn(connect);
+ when(resource.getDisks(connect, "vm-name")).thenReturn(List.of());
+ when(resource.getDomain(connect, "vm-name")).thenReturn(domain);
+
+ CreateDiskOnlyVmSnapshotAnswer answer = (CreateDiskOnlyVmSnapshotAnswer) wrapper.takeDiskOnlyVmSnapshotOfRunningVm(command, resource);
+
+ assertFalse(answer.getResult());
+ }
+ }
diff --cc plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java
index 0a58dfc79b0,11d508d1646..ff10c443512
--- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java
+++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java
@@@ -502,168 -510,70 +512,235 @@@ public class KVMStorageProcessorTest
Assert.assertEquals("vda", result);
}
+ /**
+ * Wires a mocked Ceph stack for {@link KVMStorageProcessor#takeRbdVolumeSnapshotOfStoppedVm} and returns the
+ * mocked disk. The Rbd instance is created inside the method under test, so it is mocked by construction.
+ */
+ private KVMPhysicalDisk prepareRbdSnapshotMocks(Rados radosMock, IoCTX ioCtxMock) throws Exception {
+ KVMPhysicalDisk diskMock = Mockito.mock(KVMPhysicalDisk.class);
+ Mockito.lenient().doReturn(RBD_IMAGE_NAME).when(diskMock).getName();
+
+ Mockito.lenient().doReturn(RBD_POOL_NAME).when(kvmStoragePoolMock).getSourceDir();
+ Mockito.lenient().doReturn("10.0.0.1").when(kvmStoragePoolMock).getSourceHost();
+ Mockito.lenient().doReturn("cloudstack").when(kvmStoragePoolMock).getAuthUserName();
+ Mockito.lenient().doReturn("secret").when(kvmStoragePoolMock).getAuthSecret();
+
+ Mockito.doReturn(radosMock).when(storageProcessorSpy).radosConnect(kvmStoragePoolMock);
+ Mockito.doReturn(ioCtxMock).when(radosMock).ioCtxCreate(RBD_POOL_NAME);
+ Mockito.lenient().doReturn(SNAPSHOT_SIZE).when(storageProcessorSpy).getRbdSnapshotSize(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
+
+ return diskMock;
+ }
+
+ /**
+ * A duplicated snapCreate call used to throw "snapshot already exists" on every single RBD snapshot, which then
+ * skipped the cleanup below and leaked the image's exclusive-lock.
+ */
+ @Test
+ public void takeRbdVolumeSnapshotOfStoppedVmTestCreatesSnapshotExactlyOnce() throws Exception {
+ Rados radosMock = Mockito.mock(Rados.class);
+ IoCTX ioCtxMock = Mockito.mock(IoCTX.class);
+ RbdImage rbdImageMock = Mockito.mock(RbdImage.class);
+ KVMPhysicalDisk diskMock = prepareRbdSnapshotMocks(radosMock, ioCtxMock);
+
+ try (MockedConstruction<Rbd> rbd = Mockito.mockConstruction(Rbd.class, ((mock, context) ->
+ Mockito.doReturn(rbdImageMock).when(mock).open(RBD_IMAGE_NAME)))) {
+
+ Long result = storageProcessorSpy.takeRbdVolumeSnapshotOfStoppedVm(kvmStoragePoolMock, diskMock, SNAPSHOT_NAME);
+
+ Assert.assertEquals(Long.valueOf(SNAPSHOT_SIZE), result);
+ Mockito.verify(rbdImageMock, Mockito.times(1)).snapCreate(SNAPSHOT_NAME);
+ Mockito.verify(rbd.constructed().get(0)).close(rbdImageMock);
+ Mockito.verify(radosMock).ioCtxDestroy(ioCtxMock);
+ }
+ }
+
+ /**
+ * While the image stays open this client holds the RBD exclusive-lock, and a later 'rbd snap rollback'
+ * (revertSnapshot) from another host fails with EROFS. The handles must be released even when the snapshot fails.
+ */
+ @Test
+ public void takeRbdVolumeSnapshotOfStoppedVmTestReleasesHandlesWhenSnapshotFails() throws Exception {
+ Rados radosMock = Mockito.mock(Rados.class);
+ IoCTX ioCtxMock = Mockito.mock(IoCTX.class);
+ RbdImage rbdImageMock = Mockito.mock(RbdImage.class);
+ KVMPhysicalDisk diskMock = prepareRbdSnapshotMocks(radosMock, ioCtxMock);
+ Mockito.doThrow(new RbdException("Failed to create snapshot")).when(rbdImageMock).snapCreate(SNAPSHOT_NAME);
+
+ try (MockedConstruction<Rbd> rbd = Mockito.mockConstruction(Rbd.class, ((mock, context) ->
+ Mockito.doReturn(rbdImageMock).when(mock).open(RBD_IMAGE_NAME)))) {
+
+ Long result = storageProcessorSpy.takeRbdVolumeSnapshotOfStoppedVm(kvmStoragePoolMock, diskMock, SNAPSHOT_NAME);
+
+ Assert.assertNull(result);
+ Mockito.verify(rbd.constructed().get(0)).close(rbdImageMock);
+ Mockito.verify(radosMock).ioCtxDestroy(ioCtxMock);
+ }
+ }
++
+ @Test
+ public void testParseClvmSnapshotPath_ValidPath() {
+ String snapshotPath = "/dev/vg-storage/volume-uuid-123/snapshot-uuid-456";
+ Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM_NG;
+
+ try {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "parseClvmSnapshotPath", String.class, Storage.StoragePoolType.class);
+ method.setAccessible(true);
+ String[] result = (String[]) method.invoke(storageProcessorSpy, snapshotPath, poolType);
+
+ Assert.assertNotNull("Should return parsed array for valid path", result);
+ Assert.assertEquals("Should return 4 elements", 4, result.length);
+ Assert.assertEquals("VG name should be vg-storage", "vg-storage", result[0]);
+ Assert.assertEquals("Volume UUID should be volume-uuid-123", "volume-uuid-123", result[1]);
+ Assert.assertEquals("Snapshot UUID should be snapshot-uuid-456", "snapshot-uuid-456", result[2]);
+ Assert.assertNotNull("MD5 hash should be computed", result[3]);
+ } catch (Exception e) {
+ Assert.fail("Failed to test parseClvmSnapshotPath: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testParseClvmSnapshotPath_InvalidPathFormat() {
+ String snapshotPath = "/dev/vg-storage/invalid";
+ Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM;
+
+ try {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "parseClvmSnapshotPath", String.class, Storage.StoragePoolType.class);
+ method.setAccessible(true);
+ String[] result = (String[]) method.invoke(storageProcessorSpy, snapshotPath, poolType);
+
+ Assert.assertNull("Should return null for invalid path format", result);
+ } catch (Exception e) {
+ Assert.fail("Failed to test parseClvmSnapshotPath: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDeleteClvmSnapshot_SuccessfulDeletion() {
+ String snapshotPath = "/dev/vg-storage/volume-uuid/snapshot-uuid";
+ Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM_NG;
+
+ try (MockedConstruction<Script> scriptConstruction = Mockito.mockConstruction(Script.class, (mock, context) -> {
+ Mockito.when(mock.execute()).thenReturn(null);
+ })) {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, true);
+
+ Assert.assertTrue("Should return true for successful deletion", result);
+ } catch (Exception e) {
+ Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDeleteClvmSnapshot_SnapshotAlreadyDeleted() {
+ String snapshotPath = "/dev/vg-storage/volume-uuid/snapshot-uuid";
+ Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM;
+
+ try (MockedConstruction<Script> scriptConstruction = Mockito.mockConstruction(Script.class, (mock, context) -> {
+ Mockito.when(mock.execute()).thenReturn("Error: snapshot does not exist");
+ })) {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, true);
+
+ Assert.assertTrue("Should return true when snapshot already deleted", result);
+ } catch (Exception e) {
+ Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDeleteClvmSnapshot_DeletionFailedWithoutCheck() {
+ String snapshotPath = "/dev/vg-storage/volume-uuid/snapshot-uuid";
+ Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM_NG;
+
+ try (MockedConstruction<Script> scriptConstruction = Mockito.mockConstruction(Script.class, (mock, context) -> {
+ Mockito.when(mock.execute()).thenReturn("Error: some other error");
+ })) {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, false);
+
+ Assert.assertFalse("Should return false when deletion fails", result);
+ } catch (Exception e) {
+ Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDeleteClvmSnapshot_InvalidPath() {
+ String snapshotPath = "/invalid/path";
+ Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM;
+
+ try {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, true);
+
+ Assert.assertFalse("Should return false for invalid path", result);
+ } catch (Exception e) {
+ Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testComputeMd5Hash_ValidInput() {
+ String input = "snapshot-uuid-123";
+
+ try {
+ Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "computeMd5Hash", String.class);
+ method.setAccessible(true);
+ String result = (String) method.invoke(storageProcessorSpy, input);
+
+ Assert.assertNotNull("Should return non-null hash", result);
+ Assert.assertEquals("Hash should be 32 characters long (MD5)", 32, result.length());
+ Assert.assertTrue("Hash should contain only hex characters", result.matches("[0-9a-f]+"));
+ } catch (Exception e) {
+ Assert.fail("Failed to test computeMd5Hash: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testComputeMd5Hash_EmptyInput() {
+ String input = "";
+
+ try {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "computeMd5Hash", String.class);
+ method.setAccessible(true);
+ String result = (String) method.invoke(storageProcessorSpy, input);
+
+ Assert.assertNotNull("Should return non-null hash even for empty input", result);
+ Assert.assertEquals("Hash should be 32 characters long (MD5)", 32, result.length());
+ } catch (Exception e) {
+ Assert.fail("Failed to test computeMd5Hash: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testComputeMd5Hash_ConsistentResults() {
+ String input = "snapshot-uuid-456";
+
+ try {
+ java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
+ "computeMd5Hash", String.class);
+ method.setAccessible(true);
+ String result1 = (String) method.invoke(storageProcessorSpy, input);
+ String result2 = (String) method.invoke(storageProcessorSpy, input);
+
+ Assert.assertEquals("Same input should produce same hash", result1, result2);
+ } catch (Exception e) {
+ Assert.fail("Failed to test computeMd5Hash: " + e.getMessage());
+ }
+ }
}
diff --cc plugins/storage/sharedfs/storagevm/src/test/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycleTest.java
index 82d055b9a35,dd7f873998e..f5b97a7d758
--- a/plugins/storage/sharedfs/storagevm/src/test/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycleTest.java
+++ b/plugins/storage/sharedfs/storagevm/src/test/java/org/apache/cloudstack/storage/sharedfs/lifecycle/StorageVmSharedFSLifeCycleTest.java
@@@ -273,6 -274,46 +274,46 @@@ public class StorageVmSharedFSLifeCycle
Assert.assertEquals(Optional.ofNullable(result.second()), Optional.ofNullable(s_vmId));
}
+ @Test
+ public void testDeploySharedFSContinuesWhenTemplateIsMissingForNonLastHypervisor() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException, IOException, OperationTimedoutException {
+ SharedFS sharedFS = prepareDeploySharedFS();
+ when(resourceMgr.getSupportedHypervisorTypes(s_zoneId, false, null)).thenReturn(new ArrayList<>(List.of(Hypervisor.HypervisorType.External, Hypervisor.HypervisorType.KVM)) {
+ @Override
+ public Hypervisor.HypervisorType set(int index, Hypervisor.HypervisorType element) {
+ // Keep the test order stable while exercising the production shuffle call.
+ return get(index);
+ }
+ });
+ when(templateDao.findSystemVMReadyTemplate(s_zoneId, Hypervisor.HypervisorType.External, ResourceManager.SystemVmPreferredArchitecture.defaultValue())).thenReturn(null);
+
+ Account owner = mock(Account.class);
+ when(owner.getId()).thenReturn(s_ownerId);
+ when(accountMgr.getActiveAccountById(s_ownerId)).thenReturn(owner);
+
+ UserVm vm = mock(UserVm.class);
+ when(vm.getId()).thenReturn(s_vmId);
+ when(userVmService.createAdvancedVirtualMachine(
+ any(DataCenter.class), any(ServiceOffering.class), any(VirtualMachineTemplate.class), anyList(), any(Account.class), anyString(),
+ anyString(), anyLong(), anyLong(), any(), isNull(), any(Hypervisor.HypervisorType.class), any(BaseCmd.HTTPMethod.class), anyString(),
+ isNull(), isNull(), anyList(), isNull(), any(Network.IpAddresses.class), isNull(), isNull(), isNull(),
+ anyMap(), isNull(), isNull(), isNull(), isNull(),
- anyBoolean(), anyString(), isNull(), isNull(), isNull())).thenReturn(vm);
++ anyBoolean(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(vm);
+
+ VolumeVO rootVol = mock(VolumeVO.class);
+ when(rootVol.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(rootVol.getName()).thenReturn("ROOT-1");
+ VolumeVO dataVol = mock(VolumeVO.class);
+ when(dataVol.getId()).thenReturn(s_volumeId);
+ when(dataVol.getName()).thenReturn("DATA-1");
+ when(dataVol.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeDao.findByInstance(s_vmId)).thenReturn(List.of(rootVol, dataVol));
+
+ Pair<Long, Long> result = lifeCycle.deploySharedFS(sharedFS, s_networkId, s_diskOfferingId, s_size, s_minIops, s_maxIops);
+
+ Assert.assertEquals(Optional.ofNullable(result.first()), Optional.ofNullable(s_volumeId));
+ Assert.assertEquals(Optional.ofNullable(result.second()), Optional.ofNullable(s_vmId));
+ }
+
@Test(expected = CloudRuntimeException.class)
public void testDeploySharedFSHypervisorNotFound() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException, IOException, OperationTimedoutException {
SharedFS sharedFS = mock(SharedFS.class);
diff --cc server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java
index cfa0e2edaae,040f050fd45..282d892600b
--- a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java
+++ b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java
@@@ -17,6 -17,33 +17,36 @@@
package com.cloud.network.lb;
+ import static org.mockito.ArgumentMatchers.anyLong;
+ import static org.mockito.Mockito.never;
+ import static org.mockito.Mockito.times;
+ import static org.mockito.Mockito.when;
+
+ import java.util.ArrayList;
+ import java.util.Arrays;
++import java.util.Collections;
++import java.util.HashMap;
+ import java.util.List;
++import java.util.Map;
+ import java.util.UUID;
+
+ import org.apache.cloudstack.acl.SecurityChecker;
+ import org.apache.cloudstack.api.ApiConstants;
+ import org.apache.cloudstack.api.ServerApiException;
+ import org.apache.cloudstack.api.command.user.loadbalancer.UpdateLoadBalancerRuleCmd;
+ import org.apache.cloudstack.context.CallContext;
+ import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+ import org.junit.Assert;
+ import org.junit.Test;
+ import org.junit.runner.RunWith;
+ import org.mockito.InjectMocks;
+ import org.mockito.Mock;
+ import org.mockito.Mockito;
+ import org.mockito.Spy;
+ import org.mockito.junit.MockitoJUnitRunner;
+ import org.springframework.test.util.ReflectionTestUtils;
+
+ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
@@@ -27,7 -54,7 +57,8 @@@ import com.cloud.network.dao.LoadBalanc
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.SslCertVO;
+import com.cloud.network.vpc.VpcManager;
+ import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
@@@ -38,35 -64,6 +69,7 @@@ import com.cloud.uservm.UserVm
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
+import com.cloud.vm.Nic;
- import org.apache.cloudstack.acl.SecurityChecker;
- import org.apache.cloudstack.api.ApiConstants;
- import org.apache.cloudstack.api.ServerApiException;
- import org.apache.cloudstack.api.command.user.loadbalancer.UpdateLoadBalancerRuleCmd;
- import org.apache.cloudstack.context.CallContext;
- import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
- import org.junit.Assert;
- import org.junit.Test;
- import org.junit.runner.RunWith;
- import org.mockito.InjectMocks;
- import org.mockito.Mock;
- import org.mockito.Mockito;
- import org.mockito.Spy;
- import org.mockito.junit.MockitoJUnitRunner;
- import org.springframework.test.util.ReflectionTestUtils;
-
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.UUID;
-
- import static org.mockito.ArgumentMatchers.anyLong;
- import static org.mockito.Mockito.never;
- import static org.mockito.Mockito.times;
- import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LoadBalancingRulesManagerImplTest{
@@@ -95,9 -92,9 +98,12 @@@
@Mock
NetworkOfferingServiceMapDao _networkOfferingServiceDao;
+ @Mock
+ NetworkVO networkMock;
+
+ @Mock
+ VpcManager vpcManager;
+
@Spy
@InjectMocks
LoadBalancingRulesManagerImpl lbr = new LoadBalancingRulesManagerImpl();
@@@ -318,46 -312,18 +321,61 @@@
Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.SSL_PROTO);
}
+ @Test
+ public void testGetVmNicInLoadBalancerDefaultCase() {
+ UserVm userVm = Mockito.mock(UserVm.class);
+ LoadBalancerVO loadBalancer = Mockito.mock(LoadBalancerVO.class);
+ Network loadBalancerNetwork = Mockito.mock(Network.class);
+ Account owner = Mockito.mock(Account.class);
+
+ when(vpcManager.isNetworkOnVpcEnabledConserveMode(Mockito.eq(loadBalancerNetwork))).thenReturn(false);
+
+ when(loadBalancer.getNetworkId()).thenReturn(networkId);
+ Nic nic = Mockito.mock(Nic.class);
+ when(nic.getNetworkId()).thenReturn(networkId);
+ List<? extends Nic> nics = Collections.singletonList(nic);
+ Mockito.doReturn(nics).when(_networkModel).getNics(anyLong());
+ Nic nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, null, owner);
+ Assert.assertEquals(nic, nicInLb);
+ }
+
+ @Test
+ public void testGetVmNicInLoadBalancerVPCConserveMode() {
+ long vmId = 30L;
+ UserVm userVm = Mockito.mock(UserVm.class);
+ when(userVm.getId()).thenReturn(vmId);
+ LoadBalancerVO loadBalancer = Mockito.mock(LoadBalancerVO.class);
+ Network loadBalancerNetwork = Mockito.mock(Network.class);
+ Account owner = Mockito.mock(Account.class);
+
+ long networkTier2Id = 20L;
+ NetworkVO networkTier2 = Mockito.mock(NetworkVO.class);
+ Map<Long, Long> vmIdNetworkIdMap = new HashMap<>();
+ vmIdNetworkIdMap.put(vmId, networkTier2Id);
+
+ when(vpcManager.isNetworkOnVpcEnabledConserveMode(Mockito.eq(loadBalancerNetwork))).thenReturn(true);
+ when(_networkDao.findById(networkTier2Id)).thenReturn(networkTier2);
+ when(networkTier2.getVpcId()).thenReturn(10L);
+ when(loadBalancerNetwork.getVpcId()).thenReturn(10L);
+ Nic nic = Mockito.mock(Nic.class);
+ when(_networkModel.getNicInNetwork(Mockito.eq(vmId), Mockito.eq(networkTier2Id))).thenReturn(nic);
+
+ Nic nicInLb = lbr.getVmNicInLoadBalancer(userVm, loadBalancer, loadBalancerNetwork, vmIdNetworkIdMap, owner);
+ Assert.assertEquals(nic, nicInLb);
+ }
++
+ @Test(expected = InvalidParameterValueException.class)
+ public void createPublicLoadBalancerRuleWithDnsPortAndNoIpDoesNotNpe() throws Exception {
+ long lbOwnerId = accountId;
+ long networkOfferingId = 7L;
+ when(_accountMgr.getAccount(lbOwnerId)).thenReturn(Mockito.mock(Account.class));
+ when(_networkModel.getNetwork(networkId)).thenReturn(networkMock);
+ when(networkMock.getNetworkOfferingId()).thenReturn(networkOfferingId);
+ NetworkOffering off = Mockito.mock(NetworkOffering.class);
+ when(_entityMgr.findById(NetworkOffering.class, networkOfferingId)).thenReturn(off);
+ when(off.isElasticLb()).thenReturn(false);
+
+ lbr.createPublicLoadBalancerRule("xid", "name", "desc", 53, 53, 53, 53,
+ null, "tcp", "roundrobin", networkId, lbOwnerId, false, "tcp", null, null);
+ }
}
diff --cc server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index 82b9e7f0e23,cc2dc1cc9ef..4d631b293c1
--- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
+++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
@@@ -141,8 -136,10 +139,9 @@@ import com.cloud.exception.ResourceAllo
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
+ import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
-import com.cloud.kubernetes.cluster.KubernetesServiceHelper;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.as.AutoScaleManager;
@@@ -3348,10 -3293,10 +3326,10 @@@ public class UserVmManagerImplTest
Mockito.any());
Assert.assertThrows(CloudRuntimeException.class, () -> userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock,
- userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock, 1L));
+ userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock));
- Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
+ Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any());
Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong());
}
}
@@@ -3371,10 -3316,10 +3349,10 @@@
Mockito.any());
Assert.assertThrows(CloudRuntimeException.class, () -> userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock,
- userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock, 1L));
+ userVmVoMock, serviceOfferingVoMock, volumes, virtualMachineTemplateMock));
- Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
+ Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any());
Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong());
}
}
@@@ -3393,13 -3338,13 +3371,13 @@@
configureDoNothingForMethodsThatWeDoNotWantToTest();
userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes,
- virtualMachineTemplateMock, 1L);
+ virtualMachineTemplateMock);
- Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
+ Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any());
Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong());
Mockito.verify(userVmManagerImpl).updateVmNetwork(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.verify(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.verify(userVmManagerImpl).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
}
}
@@@ -3416,13 -3361,13 +3394,13 @@@
configureDoNothingForMethodsThatWeDoNotWantToTest();
userVmManagerImpl.executeStepsToChangeOwnershipOfVm(assignVmCmdMock, callerAccount, accountMock, accountMock, userVmVoMock, serviceOfferingVoMock, volumes,
- virtualMachineTemplateMock, 1L);
+ virtualMachineTemplateMock);
- Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.verify(userVmManagerImpl).resourceCountDecrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
+ Mockito.verify(userVmManagerImpl).updateVmOwner(Mockito.any(), Mockito.any());
Mockito.verify(userVmManagerImpl).updateVolumesOwner(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyLong());
Mockito.verify(userVmManagerImpl).updateVmNetwork(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.verify(userVmManagerImpl, Mockito.never()).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.verify(userVmManagerImpl, Mockito.never()).resourceCountIncrement(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
}
}
diff --cc services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
index b0a2a1fca76,0cb1c4805da..3b584dafd66
--- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
+++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java
@@@ -172,23 -227,11 +232,29 @@@ public class ConsoleProxy
LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize);
}
+ s = conf.getProperty("session_reconnection_window");
+ if (s != null) {
+ sessionReconnectionWindowMs = Long.parseLong(s);
+ LOGGER.info("Setting sessionReconnectionWindowMs=" + sessionReconnectionWindowMs);
+ }
++
+ // Read consoleproxy.session.timeout in milliseconds.
+ s = conf.getProperty("consoleproxy.session.timeout");
+ if (s != null) {
+ try {
+ int parsedTimeout = Integer.parseInt(s);
+ if (parsedTimeout < 1000) {
+ LOGGER.warn("Invalid value for consoleproxy.session.timeout: " + s
+ + " ms, must be >= 1000 ms, keeping default " + sessionTimeoutMillis + " ms");
+ } else {
+ sessionTimeoutMillis = parsedTimeout;
+ LOGGER.info("Setting consoleproxy.session.timeout=" + sessionTimeoutMillis + " ms");
+ }
+ } catch (NumberFormatException e) {
+ LOGGER.warn("Invalid value for consoleproxy.session.timeout: " + s
+ + ", keeping default " + sessionTimeoutMillis + " ms", e);
+ }
+ }
}
public static ConsoleProxyServerFactory getHttpServerFactory() {
diff --cc ui/src/components/offering/ComputeOfferingForm.vue
index c621324f108,00000000000..0e293895f3f
mode 100644,000000..100644
--- a/ui/src/components/offering/ComputeOfferingForm.vue
+++ b/ui/src/components/offering/ComputeOfferingForm.vue
@@@ -1,812 -1,0 +1,812 @@@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+ <a-form
+ ref="internalFormRef"
+ :model="form"
+ :rules="rules"
+ @finish="onInternalSubmit"
+ layout="vertical">
+ <a-form-item name="name" ref="name">
+ <template #label>
+ <tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
+ </template>
+ <a-input
+ v-focus="true"
+ v-model:value="form.name"
+ :placeholder="$t('label.name')"/>
+ </a-form-item>
+ <a-form-item name="displaytext" ref="displaytext">
+ <template #label>
+ <tooltip-label :title="$t('label.displaytext')" :tooltip="apiParams.displaytext.description"/>
+ </template>
+ <a-input
+ v-model:value="form.displaytext"
+ :placeholder="$t('label.displaytext')"/>
+ </a-form-item>
+ <a-form-item name="systemvmtype" ref="systemvmtype" v-if="isSystem">
+ <template #label>
+ <tooltip-label :title="$t('label.systemvmtype')" :tooltip="apiParams.systemvmtype.description"/>
+ </template>
+ <a-select
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.systemvmtype"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ :placeholder="apiParams.systemvmtype.description">
+ <a-select-option key="domainrouter" :label="$t('label.domain.router')">{{ $t('label.domain.router') }}</a-select-option>
+ <a-select-option key="consoleproxy" :label="$t('label.console.proxy')">{{ $t('label.console.proxy') }}</a-select-option>
+ <a-select-option key="secondarystoragevm" :label="$t('label.secondary.storage.vm')">{{ $t('label.secondary.storage.vm') }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item name="offeringtype" ref="offeringtype" :label="$t('label.offeringtype')" v-show="!isSystem">
+ <a-radio-group
+ v-model:value="form.offeringtype"
+ @change="selected => { handleComputeOfferingTypeChange(selected.target.value) }"
+ buttonStyle="solid">
+ <a-radio-button value="fixed">{{ $t('label.fixed') }}</a-radio-button>
+ <a-radio-button value="customconstrained">{{ $t('label.customconstrained') }}</a-radio-button>
+ <a-radio-button value="customunconstrained">{{ $t('label.customunconstrained') }}</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+
+ <a-row :gutter="12">
+ <a-col :md="8" :lg="8" v-if="offeringType === 'fixed'">
+ <a-form-item name="cpunumber" ref="cpunumber">
+ <template #label>
+ <tooltip-label :title="$t('label.cpunumber')" :tooltip="apiParams.cpunumber.description"/>
+ </template>
+ <a-input v-model:value="form.cpunumber" :placeholder="apiParams.cpunumber.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="8" :lg="8" v-if="offeringType !== 'customunconstrained'">
+ <a-form-item name="cpuspeed" ref="cpuspeed">
+ <template #label>
+ <tooltip-label :title="$t('label.cpuspeed')" :tooltip="apiParams.cpuspeed.description"/>
+ </template>
+ <a-input v-model:value="form.cpuspeed" :placeholder="apiParams.cpuspeed.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="8" :lg="8" v-if="offeringType === 'fixed'">
+ <a-form-item name="memory" ref="memory">
+ <template #label>
+ <tooltip-label :title="$t('label.memory.mb')" :tooltip="apiParams.memory.description"/>
+ </template>
+ <a-input v-model:value="form.memory" :placeholder="apiParams.memory.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <!-- The rest of the form fields (storage, QoS, GPU, tags, etc.) were copied
+ from AddComputeOffering.vue to ensure identical behavior. -->
+
+ <a-row :gutter="12" v-if="offeringType === 'customconstrained'">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="mincpunumber" ref="mincpunumber">
+ <template #label>
+ <tooltip-label :title="$t('label.mincpunumber')" :tooltip="apiParams.mincpunumber.description"/>
+ </template>
+ <a-input v-model:value="form.mincpunumber" :placeholder="apiParams.mincpunumber.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="maxcpunumber" ref="maxcpunumber">
+ <template #label>
+ <tooltip-label :title="$t('label.maxcpunumber')" :tooltip="apiParams.maxcpunumber.description"/>
+ </template>
+ <a-input v-model:value="form.maxcpunumber" :placeholder="apiParams.maxcpunumber.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-row :gutter="12" v-if="offeringType === 'customconstrained'">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="minmemory" ref="minmemory">
+ <template #label>
+ <tooltip-label :title="$t('label.minmemory')" :tooltip="apiParams.minmemory.description"/>
+ </template>
+ <a-input v-model:value="form.minmemory" :placeholder="apiParams.minmemory.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="maxmemory" ref="maxmemory">
+ <template #label>
+ <tooltip-label :title="$t('label.maxmemory')" :tooltip="apiParams.maxmemory.description"/>
+ </template>
+ <a-input v-model:value="form.maxmemory" :placeholder="apiParams.maxmemory.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-row :gutter="12">
+ <a-col :md="12" :lg="12">
+ <a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="hosttags" ref="hosttags">
+ <template #label>
+ <tooltip-label :title="$t('label.hosttags')" :tooltip="apiParams.hosttags.description"/>
+ </template>
+ <a-input v-model:value="form.hosttags" :placeholder="apiParams.hosttags.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="networkrate" ref="networkrate">
+ <template #label>
+ <tooltip-label :title="$t('label.networkrate')" :tooltip="apiParams.networkrate.description"/>
+ </template>
+ <a-input v-model:value="form.networkrate" :placeholder="apiParams.networkrate.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-row :gutter="12">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="offerha" ref="offerha">
+ <template #label>
+ <tooltip-label :title="$t('label.offerha')" :tooltip="apiParams.offerha.description"/>
+ </template>
+ <a-switch v-model:checked="form.offerha" />
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="dynamicscalingenabled" ref="dynamicscalingenabled">
+ <template #label>
+ <tooltip-label :title="$t('label.dynamicscalingenabled')" :tooltip="apiParams.dynamicscalingenabled.description"/>
+ </template>
+ <a-switch v-model:checked="form.dynamicscalingenabled" @change="val => { dynamicscalingenabled = val }"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-row :gutter="12">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="limitcpuuse" ref="limitcpuuse">
+ <template #label>
+ <tooltip-label :title="$t('label.limitcpuuse')" :tooltip="apiParams.limitcpuuse.description"/>
+ </template>
+ <a-switch v-model:checked="form.limitcpuuse" />
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item v-if="!isSystem" name="isvolatile" ref="isvolatile">
+ <template #label>
+ <tooltip-label :title="$t('label.isvolatile')" :tooltip="apiParams.isvolatile.description"/>
+ </template>
+ <a-switch v-model:checked="form.isvolatile" />
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-form-item name="deploymentplanner" ref="deploymentplanner" v-if="!isSystem && isAdmin()">
+ <template #label>
+ <tooltip-label :title="$t('label.deploymentplanner')" :tooltip="apiParams.deploymentplanner.description"/>
+ </template>
+ <a-select
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.deploymentplanner"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ :loading="deploymentPlannerLoading"
+ :placeholder="apiParams.deploymentplanner.description"
+ @change="val => { handleDeploymentPlannerChange(val) }">
+ <a-select-option v-for="(opt) in deploymentPlanners" :key="opt.name" :label="opt.name || opt.description || ''">{{ opt.name || opt.description }}</a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item name="plannermode" ref="plannermode" :label="$t('label.plannermode')" v-if="plannerModeVisible">
+ <a-radio-group v-model:value="form.plannermode" buttonStyle="solid">
+ <a-radio-button value="">{{ $t('label.none') }}</a-radio-button>
+ <a-radio-button value="strict">{{ $t('label.strict') }}</a-radio-button>
+ <a-radio-button value="Preferred">{{ $t('label.preferred') }}</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+
+ <a-form-item name="gpucardid" ref="gpucardid" :label="$t('label.gpu.card')" v-if="!isSystem">
+ <a-select
+ v-model:value="form.gpucardid"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ :loading="gpuCardLoading"
+ :placeholder="$t('label.gpu.card')"
+ @change="handleGpuCardChange">
+ <a-select-option v-for="(opt, optIndex) in gpuCards" :key="optIndex" :value="opt.id" :label="opt.name || opt.description || ''">{{ opt.description || opt.name || '' }}</a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item name="vgpuprofile" ref="vgpuprofile" :label="$t('label.vgpu.profile')" v-if="!isSystem && form.gpucardid && vgpuProfiles.length > 0">
+ <a-select
+ v-model:value="form.vgpuprofile"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ :loading="vgpuProfileLoading"
+ :placeholder="$t('label.vgpu.profile')">
+ <a-select-option v-for="(vgpu, vgpuIndex) in vgpuProfiles" :key="vgpuIndex" :value="vgpu.id" :label="vgpu.vgpuprofile || ''">{{ vgpu.name }} {{ getVgpuProfileDetails(vgpu) }}</a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-row :gutter="12" v-if="!isSystem && form.gpucardid">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="gpucount" ref="gpucount">
+ <template #label>
+ <tooltip-label :title="$t('label.gpu.count')" :tooltip="apiParams.gpucount.description"/>
+ </template>
+ <a-input v-model:value="form.gpucount" type="number" min="1" max="16" :placeholder="$t('label.gpu.count')"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="gpudisplay" ref="gpudisplay">
+ <template #label>
- <tooltip-label :title="$t('label.gpu.display')" :tooltip="apiParams.gpudisplay.description"/>
++ <tooltip-label :title="$t('label.gpudisplay')" :tooltip="apiParams.gpudisplay.description"/>
+ </template>
+ <a-switch v-model:checked="form.gpudisplay" />
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-form-item name="ispublic" ref="ispublic" :label="$t('label.ispublic')" v-show="isAdmin()">
+ <a-switch v-model:checked="form.ispublic" />
+ </a-form-item>
+
+ <a-form-item name="domainid" ref="domainid" v-if="!form.ispublic">
+ <template #label>
+ <tooltip-label :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/>
+ </template>
+ <a-select
+ mode="multiple"
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.domainid"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ :loading="domainLoading"
+ :placeholder="apiParams.domainid.description">
+ <a-select-option v-for="(opt, optIndex) in domains" :key="optIndex" :label="opt.path || opt.name || opt.description">
+ <span>
+ <resource-icon v-if="opt && opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
+ <block-outlined v-else style="margin-right: 5px" />
+ {{ opt.path || opt.name || opt.description }}
+ </span>
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item name="zoneid" ref="zoneid" v-if="!isSystem">
+ <template #label>
+ <tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/>
+ </template>
+ <a-select
+ id="zone-selection"
+ mode="multiple"
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.zoneid"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ @select="val => fetchvSphereStoragePolicies(val)"
+ :loading="zoneLoading"
+ :placeholder="apiParams.zoneid.description">
+ <a-select-option v-for="(opt, optIndex) in zones" :key="optIndex" :label="opt.name || opt.description">
+ <span>
+ <resource-icon v-if="opt.icon" :image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
+ <global-outlined v-else style="margin-right: 5px"/>
+ {{ opt.name || opt.description }}
+ </span>
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item name="storagepolicy" ref="storagepolicy" v-if="'listVsphereStoragePolicies' in $store.getters.apis && storagePolicies !== null">
+ <template #label>
+ <tooltip-label :title="$t('label.vmware.storage.policy')" :tooltip="apiParams.storagepolicy.description"/>
+ </template>
+ <a-select
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.storagepolicy"
+ :placeholder="apiParams.storagepolicy.description"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }">
+ <a-select-option v-for="policy in storagePolicies" :key="policy.id" :label="policy.name || policy.id || ''">{{ policy.name || policy.id }}</a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item name="purgeresources" ref="purgeresources">
+ <template #label>
+ <tooltip-label :title="$t('label.purgeresources')" :tooltip="apiParams.purgeresources.description"/>
+ </template>
+ <a-switch v-model:checked="form.purgeresources"/>
+ </a-form-item>
+
+ <a-form-item name="showLeaseOptions" ref="showLeaseOptions" v-if="isLeaseFeatureEnabled">
+ <template #label>
+ <tooltip-label :title="$t('label.lease.enable')" :tooltip="$t('label.lease.enable.tooltip')" />
+ </template>
+ <a-switch v-model:checked="showLeaseOptions" @change="onToggleLeaseData"/>
+ </a-form-item>
+
+ <a-row :gutter="12" v-if="isLeaseFeatureEnabled && showLeaseOptions">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="leaseduration" ref="leaseduration">
+ <template #label>
+ <tooltip-label :title="$t('label.leaseduration')"/>
+ </template>
+ <a-input v-model:value="form.leaseduration" :placeholder="$t('label.instance.lease.placeholder')"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="leaseexpiryaction" ref="leaseexpiryaction" v-if="form.leaseduration > 0">
+ <template #label>
+ <tooltip-label :title="$t('label.leaseexpiryaction')" />
+ </template>
+ <a-select v-model:value="form.leaseexpiryaction" :defaultValue="expiryActions">
+ <a-select-option v-for="action in expiryActions" :key="action" :label="action"/>
+ </a-select>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-form-item name="computeonly" ref="computeonly">
+ <template #label>
+ <tooltip-label :title="$t('label.computeonly.offering')" :tooltip="$t('label.computeonly.offering.tooltip')"/>
+ </template>
+ <a-switch v-model:checked="form.computeonly" :checked="computeonly" @change="val => { computeonly = val }"/>
+ </a-form-item>
+
+ <a-card style="margin-bottom: 10px;">
+ <span v-if="computeonly">
+ <a-form-item name="storagetype" ref="storagetype">
+ <template #label>
+ <tooltip-label :title="$t('label.storagetype')" :tooltip="apiParams.storagetype.description"/>
+ </template>
+ <a-radio-group v-model:value="form.storagetype" buttonStyle="solid" @change="selected => { handleStorageTypeChange(selected.target.value) }">
+ <a-radio-button value="shared">{{ $t('label.shared') }}</a-radio-button>
+ <a-radio-button value="local">{{ $t('label.local') }}</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+ <a-form-item name="provisioningtype" ref="provisioningtype">
+ <template #label>
+ <tooltip-label :title="$t('label.provisioningtype')" :tooltip="apiParams.provisioningtype.description"/>
+ </template>
+ <a-radio-group v-model:value="form.provisioningtype" buttonStyle="solid" @change="selected => { handleProvisioningTypeChange(selected.target.value) }">
+ <a-radio-button value="thin">{{ $t('label.provisioningtype.thin') }}</a-radio-button>
+ <a-radio-button value="sparse">{{ $t('label.provisioningtype.sparse') }}</a-radio-button>
+ <a-radio-button value="fat">{{ $t('label.provisioningtype.fat') }}</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+ <a-form-item name="cachemode" ref="cachemode">
+ <template #label>
+ <tooltip-label :title="$t('label.cachemode')" :tooltip="apiParams.cachemode.description"/>
+ </template>
+ <a-radio-group v-model:value="form.cachemode" buttonStyle="solid" @change="selected => { handleCacheModeChange(selected.target.value) }">
+ <a-radio-button value="none">{{ $t('label.nodiskcache') }}</a-radio-button>
+ <a-radio-button value="writeback">{{ $t('label.writeback') }}</a-radio-button>
+ <a-radio-button value="writethrough">{{ $t('label.writethrough') }}</a-radio-button>
+ <a-radio-button value="hypervisor_default">{{ $t('label.hypervisor.default') }}</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+ <a-form-item :label="$t('label.qostype')" name="qostype" ref="qostype">
+ <a-radio-group v-model:value="form.qostype" buttonStyle="solid" @change="selected => { handleQosTypeChange(selected.target.value) }">
+ <a-radio-button value="">{{ $t('label.none') }}</a-radio-button>
+ <a-radio-button value="hypervisor">{{ $t('label.hypervisor') }}</a-radio-button>
+ <a-radio-button value="storage">{{ $t('label.storage') }}</a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+
+ <a-row :gutter="12" v-if="qosType === 'hypervisor'">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="diskbytesreadrate" ref="diskbytesreadrate">
+ <template #label>
+ <tooltip-label :title="$t('label.diskbytesreadrate')" :tooltip="apiParams.bytesreadrate.description"/>
+ </template>
+ <a-input v-model:value="form.diskbytesreadrate" :placeholder="apiParams.bytesreadrate.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="diskbyteswriterate" ref="diskbyteswriterate">
+ <template #label>
+ <tooltip-label :title="$t('label.diskbyteswriterate')" :tooltip="apiParams.byteswriterate.description"/>
+ </template>
+ <a-input v-model:value="form.diskbyteswriterate" :placeholder="apiParams.byteswriterate.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-row :gutter="12" v-if="qosType === 'hypervisor'">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="diskiopsreadrate" ref="diskiopsreadrate">
+ <template #label>
+ <tooltip-label :title="$t('label.diskiopsreadrate')" :tooltip="apiParams.iopsreadrate.description"/>
+ </template>
+ <a-input v-model:value="form.diskiopsreadrate" :placeholder="apiParams.iopsreadrate.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="diskiopswriterate" ref="diskiopswriterate">
+ <template #label>
+ <tooltip-label :title="$t('label.diskiopswriterate')" :tooltip="apiParams.iopswriterate.description"/>
+ </template>
+ <a-input v-model:value="form.diskiopswriterate" :placeholder="apiParams.iopswriterate.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-form-item v-if="!isSystem && qosType === 'storage'" name="iscustomizeddiskiops" ref="iscustomizeddiskiops">
+ <template #label>
+ <tooltip-label :title="$t('label.iscustomizeddiskiops')" :tooltip="apiParams.customizediops.description"/>
+ </template>
+ <a-switch v-model:checked="form.iscustomizeddiskiops" :checked="isCustomizedDiskIops" @change="val => { isCustomizedDiskIops = val }" />
+ </a-form-item>
+
+ <a-row :gutter="12" v-if="qosType === 'storage' && !isCustomizedDiskIops">
+ <a-col :md="12" :lg="12">
+ <a-form-item name="diskiopsmin" ref="diskiopsmin">
+ <template #label>
+ <tooltip-label :title="$t('label.diskiopsmin')" :tooltip="apiParams.miniops.description"/>
+ </template>
+ <a-input v-model:value="form.diskiopsmin" :placeholder="apiParams.miniops.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item name="diskiopsmax" ref="diskiopsmax">
+ <template #label>
+ <tooltip-label :title="$t('label.diskiopsmax')" :tooltip="apiParams.maxiops.description"/>
+ </template>
+ <a-input v-model:value="form.diskiopsmax" :placeholder="apiParams.maxiops.description"/>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-form-item v-if="!isSystem && qosType === 'storage'" name="hypervisorsnapshotreserve" ref="hypervisorsnapshotreserve">
+ <template #label>
+ <tooltip-label :title="$t('label.hypervisorsnapshotreserve')" :tooltip="apiParams.hypervisorsnapshotreserve.description"/>
+ </template>
+ <a-input v-model:value="form.hypervisorsnapshotreserve" :placeholder="apiParams.hypervisorsnapshotreserve.description"/>
+ </a-form-item>
+
+ <a-row :gutter="12">
+ <a-col :md="12" :lg="12">
+ <a-form-item v-if="apiParams.rootdisksize" name="rootdisksize" ref="rootdisksize">
+ <template #label>
+ <tooltip-label :title="$t('label.root.disk.size')" :tooltip="apiParams.rootdisksize.description"/>
+ </template>
+ <a-input v-model:value="form.rootdisksize" :placeholder="apiParams.rootdisksize.description"/>
+ </a-form-item>
+ </a-col>
+ <a-col :md="12" :lg="12">
+ <a-form-item v-if="isAdmin() || isDomainAdminAllowedToInformTags" name="storagetags" ref="storagetags">
+ <template #label>
+ <tooltip-label :title="$t('label.storagetags')" :tooltip="apiParams.tags.description"/>
+ </template>
+ <a-select
+ mode="tags"
+ :getPopupContainer="(trigger) => trigger.parentNode"
+ v-model:value="form.storagetags"
+ showSearch
+ optionFilterProp="value"
+ :filterOption="(input, option) => { return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"
+ :loading="storageTagLoading"
+ :placeholder="apiParams.tags.description"
+ v-if="isAdmin() || isDomainAdminAllowedToInformTags">
+ <a-select-option v-for="opt in storageTags" :key="opt">{{ opt }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ </a-col>
+ </a-row>
+
+ <a-form-item name="encryptdisk" ref="encryptdisk">
+ <template #label>
+ <tooltip-label :title="$t('label.encrypt')" :tooltip="apiParams.encryptroot.description" />
+ </template>
+ <a-switch v-model:checked="form.encryptdisk" :checked="encryptdisk" @change="val => { encryptdisk = val }" />
+ </a-form-item>
+ </span>
+ <span v-if="!computeonly">
+ <a-form-item>
+ <a-button type="primary" @click="addDiskOffering()"> {{ $t('label.add.disk.offering') }} </a-button>
+ <a-modal
+ :visible="showDiskOfferingModal"
+ :title="$t('label.add.disk.offering')"
+ :footer="null"
+ centered
+ :closable="true"
+ @cancel="closeDiskOfferingModal"
+ width="auto">
+ <add-disk-offering @close-action="closeDiskOfferingModal()" @publish-disk-offering-id="($event) => updateSelectedDiskOffering($event)"/>
+ </a-modal>
+ <br /><br />
+ <a-form-item :label="$t('label.disk.offerings')" name="diskofferingid" ref="diskofferingid">
+ <a-select :getPopupContainer="(trigger) => trigger.parentNode" v-model:value="form.diskofferingid" :loading="loading" :placeholder="$t('label.diskoffering')">
+ <a-select-option v-for="(offering, index) in diskOfferings" :value="offering.id" :key="index">{{ offering.displaytext || offering.name }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ </a-form-item>
+ </span>
+ <a-form-item name="diskofferingstrictness" ref="diskofferingstrictness">
+ <template #label>
+ <tooltip-label :title="$t('label.diskofferingstrictness')" :tooltip="apiParams.diskofferingstrictness.description"/>
+ </template>
+ <a-switch v-model:checked="form.diskofferingstrictness" :checked="diskofferingstrictness" @change="val => { diskofferingstrictness = val }"/>
+ </a-form-item>
+ </a-card>
+
+ <a-form-item name="externaldetails" ref="externaldetails">
+ <template #label>
+ <tooltip-label :title="$t('label.externaldetails')" :tooltip="apiParams.externaldetails.description"/>
+ </template>
+ <a-switch v-model:checked="externalDetailsEnabled" @change="onExternalDetailsEnabledChange"/>
+ <a-card v-if="externalDetailsEnabled" style="margin-top: 10px">
+ <div style="margin-bottom: 10px">{{ $t('message.add.orchestrator.resource.details') }}</div>
+ <details-input v-model:value="form.externaldetails" />
+ </a-card>
+ </a-form-item>
+
+ <slot name="form-actions"></slot>
+ </a-form>
+</template>
+
+<script>
+import { reactive, toRaw } from 'vue'
+import { getAPI } from '@/api'
+import AddDiskOffering from '@/views/offering/AddDiskOffering'
+import { isAdmin } from '@/role'
+import { mixinForm } from '@/utils/mixin'
+import ResourceIcon from '@/components/view/ResourceIcon'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+import DetailsInput from '@/components/widgets/DetailsInput'
+import store from '@/store'
+
+export default {
+ name: 'ComputeOfferingForm',
+ mixins: [mixinForm],
+ components: {
+ AddDiskOffering,
+ ResourceIcon,
+ TooltipLabel,
+ DetailsInput
+ },
+ props: {
+ initialValues: {
+ type: Object,
+ default: () => ({})
+ },
+ apiParams: {
+ type: Object,
+ default: () => ({})
+ },
+ isSystem: {
+ type: Boolean,
+ default: false
+ },
+ isAdmin: {
+ type: Function,
+ default: () => false
+ }
+ },
+ data () {
+ return {
+ internalFormRef: null,
+ form: reactive(Object.assign({
+ systemvmtype: 'domainrouter',
+ offeringtype: 'fixed',
+ ispublic: true,
+ dynamicscalingenabled: true,
+ plannermode: '',
+ gpucardid: '',
+ vgpuprofile: '',
+ gpucount: '1',
+ gpudisplay: false,
+ computeonly: true,
+ storagetype: 'shared',
+ provisioningtype: 'thin',
+ cachemode: 'none',
+ qostype: '',
+ iscustomizeddiskiops: false,
+ diskofferingid: null,
+ diskofferingstrictness: false,
+ encryptdisk: false,
+ leaseduration: undefined,
+ leaseexpiryaction: undefined
+ }, this.initialValues || {})),
+ rules: reactive({}),
+ // other UI state copied
+ storageType: 'shared',
+ provisioningType: 'thin',
+ cacheMode: 'none',
+ offeringType: 'fixed',
+ isCustomizedDiskIops: false,
+ isPublic: true,
+ domains: [],
+ domainLoading: false,
+ zones: [],
+ zoneLoading: false,
+ selectedDeploymentPlanner: null,
+ storagePolicies: null,
+ storageTags: [],
+ storageTagLoading: false,
+ deploymentPlanners: [],
+ deploymentPlannerLoading: false,
+ plannerModeVisible: false,
+ plannerMode: '',
+ selectedGpuCard: '',
+ showDiskOfferingModal: false,
+ gpuCardLoading: false,
+ gpuCards: [],
+ loading: false,
+ dynamicscalingenabled: true,
+ diskofferingstrictness: false,
+ encryptdisk: false,
+ computeonly: true,
+ diskOfferingLoading: false,
+ diskOfferings: [],
+ selectedDiskOfferingId: '',
+ qosType: '',
+ isDomainAdminAllowedToInformTags: false,
+ isLeaseFeatureEnabled: this.$store.getters.features.instanceleaseenabled,
+ showLeaseOptions: false,
+ expiryActions: ['STOP', 'DESTROY'],
+ defaultLeaseDuration: 90,
+ defaultLeaseExpiryAction: 'STOP',
+ leaseduration: undefined,
+ leaseexpiryaction: undefined,
+ vgpuProfiles: [],
+ vgpuProfileLoading: false,
+ externalDetailsEnabled: false
+ }
+ },
+ created () {
+ this.zones = [{ id: null, name: this.$t('label.all.zone') }]
+ this.initForm()
+ this.fetchData()
+ this.isPublic = isAdmin()
+ this.form.ispublic = this.isPublic
+ },
+ methods: {
+ initForm () {
+ this.formRef = this.$refs.internalFormRef
+ this.rules = reactive({
+ name: [{ required: true, message: this.$t('message.error.required.input') }]
+ })
+ },
+ fetchData () {
+ this.fetchDomainData()
+ this.fetchZoneData()
+ this.fetchGPUCards()
+ if (isAdmin()) {
+ this.fetchStorageTagData()
+ this.fetchDeploymentPlannerData()
+ } else if (this.isDomainAdmin()) {
+ this.checkIfDomainAdminIsAllowedToInformTag()
+ if (this.isDomainAdminAllowedToInformTags) {
+ this.fetchStorageTagData()
+ }
+ }
+ this.fetchDiskOfferings()
+ },
+ fetchGPUCards () {
+ this.gpuCardLoading = true
+ getAPI('listGpuCards', {
+ }).then(json => {
+ this.gpuCards = json.listgpucardsresponse.gpucard || []
+ this.gpuCards.unshift({ id: '', name: this.$t('label.none') })
+ }).finally(() => {
+ this.gpuCardLoading = false
+ })
+ },
+ addDiskOffering () { this.showDiskOfferingModal = true },
+ fetchDiskOfferings () {
+ this.diskOfferingLoading = true
+ getAPI('listDiskOfferings', { listall: true }).then(json => {
+ this.diskOfferings = json.listdiskofferingsresponse.diskoffering || []
+ if (this.selectedDiskOfferingId === '') {
+ this.selectedDiskOfferingId = this.diskOfferings[0]?.id || ''
+ }
+ }).finally(() => { this.diskOfferingLoading = false })
+ },
+ updateSelectedDiskOffering (id) { if (id) this.selectedDiskOfferingId = id },
+ closeDiskOfferingModal () { this.fetchDiskOfferings(); this.showDiskOfferingModal = false },
+ isDomainAdmin () {
+ return ['DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
+ },
+ getVgpuProfileDetails (vgpuProfile) {
+ let output = '('
+ if (vgpuProfile?.videoram) output += `${vgpuProfile.videoram} MB`
+ if (vgpuProfile?.maxresolutionx && vgpuProfile?.maxresolutiony) {
+ if (output !== '(') output += ', '
+ output += `${vgpuProfile.maxresolutionx}x${vgpuProfile.maxresolutiony}`
+ }
+ output += ')'
+ return output === '()' ? '' : output
+ },
+ checkIfDomainAdminIsAllowedToInformTag () {
+ const params = { id: store.getters.userInfo.accountid }
+ getAPI('isAccountAllowedToCreateOfferingsWithTags', params).then(json => {
+ this.isDomainAdminAllowedToInformTags = json.isaccountallowedtocreateofferingswithtagsresponse.isallowed.isallowed
+ })
+ },
+ arrayHasItems (array) { return array !== null && array !== undefined && Array.isArray(array) && array.length > 0 },
+ fetchDomainData () {
+ const params = { listAll: true, showicon: true, details: 'min' }
+ this.domainLoading = true
+ getAPI('listDomains', params).then(json => {
+ const listDomains = json.listdomainsresponse.domain
+ this.domains = this.domains.concat(listDomains)
+ }).finally(() => { this.domainLoading = false })
+ },
+ fetchZoneData () {
+ const params = { showicon: true }
+ this.zoneLoading = true
+ getAPI('listZones', params).then(json => {
+ const listZones = json.listzonesresponse.zone
+ if (listZones) this.zones = this.zones.concat(listZones)
+ }).finally(() => { this.zoneLoading = false })
+ },
+ fetchStorageTagData () {
+ this.storageTagLoading = true
+ this.storageTags = []
+ getAPI('listStorageTags').then(json => {
+ const tags = json.liststoragetagsresponse.storagetag || []
+ for (const tag of tags) if (!this.storageTags.includes(tag.name)) this.storageTags.push(tag.name)
+ }).finally(() => { this.storageTagLoading = false })
+ },
+ fetchDeploymentPlannerData () {
+ this.deploymentPlannerLoading = true
+ getAPI('listDeploymentPlanners').then(json => {
+ const planners = json.listdeploymentplannersresponse.deploymentPlanner
+ this.deploymentPlanners = this.deploymentPlanners.concat(planners)
+ this.deploymentPlanners.unshift({ name: '' })
+ this.form.deploymentplanner = this.deploymentPlanners.length > 0 ? this.deploymentPlanners[0].name : ''
+ }).finally(() => { this.deploymentPlannerLoading = false })
+ },
+ fetchvSphereStoragePolicies (zoneIndex) {
+ if (zoneIndex === 0 || this.form.zoneid.length > 1) { this.storagePolicies = null; return }
+ const zoneid = this.zones[zoneIndex].id
+ if ('importVsphereStoragePolicies' in this.$store.getters.apis) {
+ this.storagePolicies = []
+ getAPI('listVsphereStoragePolicies', { zoneid }).then(response => { this.storagePolicies = response.listvspherestoragepoliciesresponse.StoragePolicy || [] })
+ }
+ },
+ handleStorageTypeChange (val) { this.storageType = val },
+ handleProvisioningTypeChange (val) { this.provisioningType = val },
+ handleCacheModeChange (val) { this.cacheMode = val },
+ handleComputeOfferingTypeChange (val) { this.offeringType = val },
+ handleQosTypeChange (val) { this.qosType = val },
+ handleDeploymentPlannerChange (planner) { this.selectedDeploymentPlanner = planner; this.plannerModeVisible = false; if (this.selectedDeploymentPlanner === 'ImplicitDedicationPlanner') this.plannerModeVisible = isAdmin() },
+ handleGpuCardChange (cardId) { this.selectedGpuCard = cardId; this.form.vgpuprofile = ''; if (cardId && cardId !== '') this.fetchVgpuProfiles(cardId); else { this.vgpuProfiles = []; this.form.gpucount = '1' } },
+ fetchVgpuProfiles (gpuCardId) { this.vgpuProfileLoading = true; this.vgpuProfiles = []; getAPI('listVgpuProfiles', { gpucardid: gpuCardId }).then(json => { this.vgpuProfiles = json.listvgpuprofilesresponse.vgpuprofile || []; this.form.vgpuprofile = this.vgpuProfiles.length > 0 ? this.vgpuProfiles[0].id : '' }).catch(() => { this.vgpuProfiles = [] }).finally(() => { this.vgpuProfileLoading = false }) },
+ onExternalDetailsEnabledChange (val) { if (val || !this.form.externaldetails) return; this.form.externaldetails = undefined },
+ onToggleLeaseData () { if (this.showLeaseOptions === false) { this.leaseduration = undefined; this.leaseexpiryaction = undefined } else { this.leaseduration = this.leaseduration !== undefined ? this.leaseduration : this.defaultLeaseDuration; this.leaseexpiryaction = this.leaseexpiryaction !== undefined ? this.leaseexpiryaction : this.defaultLeaseExpiryAction } this.form.leaseduration = this.leaseduration; this.form.leaseexpiryaction = this.leaseexpiryaction },
+
+ validate () {
+ return this.$refs.internalFormRef.validate().then(() => {
+ const formRaw = toRaw(this.form)
+ const values = this.handleRemoveFields(formRaw)
+ return values
+ })
+ },
+ onInternalSubmit (e) {
+ // When internal form triggers submit, validate and emit
+ this.validate().then(values => this.$emit('submit', values))
+ }
+ }
+}
+</script>
+
+<style scoped>
+</style>
diff --cc ui/src/components/view/ListView.vue
index 9272617900f,ceb5ad7884c..9a7d874fef3
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@@ -814,9 -787,10 +815,10 @@@
<status :text="record.enabled ? record.enabled.toString() : 'false'" />
{{ record.enabled ? 'Enabled' : 'Disabled' }}
</template>
- <template
- v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate', 'allocated', 'startdate', 'enddate'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)"
- >
+ <template v-if="column.key === 'egressdefaultpolicy'">
+ <span> {{ record.egressdefaultpolicy ? 'Allow' : 'Deny' }} </span>
+ </template>
- <template v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate', 'allocated'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)">
++ <template v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate', 'allocated', 'startdate', 'enddate'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)">
{{ text && $toLocaleDate(text) }}
</template>
<template
diff --cc ui/src/views/compute/DeployVM.vue
index deecdd71a2e,921fd8cb124..4fa5c14d86e
--- a/ui/src/views/compute/DeployVM.vue
+++ b/ui/src/views/compute/DeployVM.vue
@@@ -2073,12 -2023,18 +2073,24 @@@ export default
this.formRef.value.resetFields()
this.fetchData()
},
+ updateRootKmsKey (value) {
+ this.form.rootkmskeyid = value
+ },
+ updateDataKmsKey (value) {
+ this.form.datakmskeyid = value
+ },
+ updateSelectedTemplateHypervisor () {
+ if (!this.form.templateid) {
+ return
+ }
+ for (const entry of Object.values(this.options.templates)) {
+ const template = entry?.template.find(option => option.id === this.form.templateid)
+ if (template?.hypervisor) {
+ this.form.hypervisor = template.hypervisor
+ return
+ }
+ }
+ },
updateFieldValue (name, value) {
if (name === 'templateid') {
this.imageType = 'templateid'