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'