2828import com .cloud .agent .api .storage .RevertDiskOnlyVmSnapshotCommand ;
2929import com .cloud .agent .api .storage .SnapshotMergeTreeTO ;
3030import com .cloud .agent .api .to .DataTO ;
31+ import com .cloud .alert .AlertManager ;
3132import com .cloud .configuration .Resource ;
3233import com .cloud .event .EventTypes ;
3334import com .cloud .event .UsageEventUtils ;
35+ import com .cloud .host .DetailVO ;
36+ import com .cloud .host .Host ;
37+ import com .cloud .host .dao .HostDetailsDao ;
3438import com .cloud .hypervisor .Hypervisor ;
3539import com .cloud .storage .DataStoreRole ;
3640import com .cloud .storage .Snapshot ;
4650import com .cloud .utils .fsm .NoTransitionException ;
4751import com .cloud .vm .UserVmVO ;
4852import com .cloud .vm .VirtualMachine ;
53+ import com .cloud .vm .dao .VMInstanceDetailsDao ;
4954import com .cloud .vm .snapshot .VMSnapshot ;
5055import com .cloud .vm .snapshot .VMSnapshotDetailsVO ;
5156import com .cloud .vm .snapshot .VMSnapshotVO ;
57+ import org .apache .cloudstack .api .ApiConstants ;
5258import org .apache .cloudstack .backup .BackupOfferingVO ;
5359import org .apache .cloudstack .backup .dao .BackupOfferingDao ;
5460import org .apache .cloudstack .engine .subsystem .api .storage .ObjectInDataStoreStateMachine ;
5965import org .apache .cloudstack .storage .datastore .db .SnapshotDataStoreDao ;
6066import org .apache .cloudstack .storage .datastore .db .SnapshotDataStoreVO ;
6167import org .apache .cloudstack .storage .datastore .db .StoragePoolVO ;
68+ import org .apache .cloudstack .storage .to .PrimaryDataStoreTO ;
6269import org .apache .cloudstack .storage .snapshot .SnapshotObject ;
6370import org .apache .cloudstack .storage .to .SnapshotObjectTO ;
6471import org .apache .cloudstack .storage .to .VolumeObjectTO ;
6572import org .apache .commons .collections .CollectionUtils ;
73+ import org .apache .commons .lang3 .StringUtils ;
6674
6775import javax .inject .Inject ;
6876import java .util .ArrayList ;
7684public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStrategy {
7785
7886 private static final List <Storage .StoragePoolType > supportedStoragePoolTypes = List .of (Storage .StoragePoolType .Filesystem , Storage .StoragePoolType .NetworkFilesystem , Storage .StoragePoolType .SharedMountPoint );
87+ private static final String KVM_FILE_BASED_STORAGE_SNAPSHOT_NVRAM = "kvmFileBasedStorageSnapshotNvram" ;
7988
8089 @ Inject
8190 protected SnapshotDataStoreDao snapshotDataStoreDao ;
@@ -86,6 +95,15 @@ public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStra
8695 @ Inject
8796 protected BackupOfferingDao backupOfferingDao ;
8897
98+ @ Inject
99+ protected VMInstanceDetailsDao vmInstanceDetailsDao ;
100+
101+ @ Inject
102+ protected HostDetailsDao hostDetailsDao ;
103+
104+ @ Inject
105+ protected AlertManager alertManager ;
106+
89107 @ Override
90108 public VMSnapshot takeVMSnapshot (VMSnapshot vmSnapshot ) {
91109 Map <VolumeInfo , SnapshotObject > volumeInfoToSnapshotObjectMap = new HashMap <>();
@@ -117,13 +135,15 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) {
117135 UserVmVO userVm = userVmDao .findById (vmSnapshot .getVmId ());
118136 VMSnapshotVO vmSnapshotBeingDeleted = (VMSnapshotVO ) vmSnapshot ;
119137 Long hostId = vmSnapshotHelper .pickRunningHost (vmSnapshotBeingDeleted .getVmId ());
138+ validateHostSupportsNvramSidecarCleanup (vmSnapshotBeingDeleted , hostId , "delete" );
120139 long virtualSize = 0 ;
121140 boolean isCurrent = vmSnapshotBeingDeleted .getCurrent ();
122141
123142 transitStateWithoutThrow (vmSnapshotBeingDeleted , VMSnapshot .Event .ExpungeRequested );
124143
125144 List <VolumeObjectTO > volumeTOs = vmSnapshotHelper .getVolumeTOList (vmSnapshotBeingDeleted .getVmId ());
126145 List <VMSnapshotVO > snapshotChildren = vmSnapshotDao .listByParentAndStateIn (vmSnapshotBeingDeleted .getId (), VMSnapshot .State .Ready , VMSnapshot .State .Hidden );
146+ PrimaryDataStoreTO nvramPrimaryDataStore = getPrimaryDataStoreForNvramCleanup (vmSnapshotBeingDeleted , volumeTOs );
127147
128148 long realSize = getVMSnapshotRealSize (vmSnapshotBeingDeleted );
129149 int numberOfChildren = snapshotChildren .size ();
@@ -157,6 +177,8 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) {
157177 return true ;
158178 }
159179
180+ deleteNvramSnapshotIfNeeded (vmSnapshotBeingDeleted , hostId , nvramPrimaryDataStore );
181+
160182 transitStateWithoutThrow (vmSnapshotBeingDeleted , VMSnapshot .Event .OperationSucceeded );
161183
162184 vmSnapshotDetailsDao .removeDetails (vmSnapshotBeingDeleted .getId ());
@@ -176,6 +198,7 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {
176198
177199 VMSnapshotVO vmSnapshotBeingReverted = (VMSnapshotVO ) vmSnapshot ;
178200 Long hostId = vmSnapshotHelper .pickRunningHost (vmSnapshotBeingReverted .getVmId ());
201+ validateHostSupportsUefiNvramAwareDiskOnlySnapshots (hostId , userVm , "revert" );
179202
180203 transitStateWithoutThrow (vmSnapshotBeingReverted , VMSnapshot .Event .RevertRequested );
181204
@@ -184,7 +207,9 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {
184207 .map (snapshot -> (SnapshotObjectTO ) snapshotDataFactory .getSnapshot (snapshot .getSnapshotId (), snapshot .getDataStoreId (), DataStoreRole .Primary ).getTO ())
185208 .collect (Collectors .toList ());
186209
187- RevertDiskOnlyVmSnapshotCommand revertDiskOnlyVMSnapshotCommand = new RevertDiskOnlyVmSnapshotCommand (volumeSnapshotTos , userVm .getName ());
210+ RevertDiskOnlyVmSnapshotCommand revertDiskOnlyVMSnapshotCommand =
211+ new RevertDiskOnlyVmSnapshotCommand (volumeSnapshotTos , userVm .getName (), userVm .getUuid (), isUefiVm (userVm ),
212+ getNvramSnapshotPath (vmSnapshotBeingReverted ));
188213 Answer answer = agentMgr .easySend (hostId , revertDiskOnlyVMSnapshotCommand );
189214
190215 if (answer == null || !answer .getResult ()) {
@@ -204,6 +229,11 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {
204229 publishUsageEvent (EventTypes .EVENT_VM_SNAPSHOT_REVERT , vmSnapshotBeingReverted , userVm , volumeObjectTo );
205230 }
206231
232+ if (isUefiVm (userVm )) {
233+ userVm .setLastHostId (hostId );
234+ userVmDao .update (userVm .getId (), userVm );
235+ }
236+
207237 transitStateWithoutThrow (vmSnapshotBeingReverted , VMSnapshot .Event .OperationSucceeded );
208238
209239 VMSnapshotVO currentVmSnapshot = vmSnapshotDao .findCurrentSnapshotByVmId (userVm .getId ());
@@ -248,6 +278,8 @@ private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParen
248278 return ;
249279 }
250280
281+ validateHostSupportsNvramSidecarCleanup (oldParent , hostId , "clean up" );
282+ PrimaryDataStoreTO nvramPrimaryDataStore = getPrimaryDataStoreForNvramCleanup (oldParent , volumeTOs );
251283 List <SnapshotVO > snapshotVos ;
252284
253285 if (oldParent .getCurrent ()) {
@@ -276,6 +308,8 @@ private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParen
276308 snapshotDao .update (snapshotVO .getId (), snapshotVO );
277309 }
278310
311+ deleteNvramSnapshotIfNeeded (oldParent , hostId , nvramPrimaryDataStore );
312+
279313 vmSnapshotDetailsDao .removeDetails (oldParent .getId ());
280314
281315 oldParent .setRemoved (DateUtil .now ());
@@ -347,12 +381,13 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe
347381 }
348382
349383 private List <SnapshotVO > deleteSnapshot (VMSnapshotVO vmSnapshotVO , Long hostId ) {
384+ validateHostSupportsNvramSidecarCleanup (vmSnapshotVO , hostId , "delete" );
350385 List <SnapshotDataStoreVO > volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot (vmSnapshotVO );
351386 List <DataTO > volumeSnapshotTOList = volumeSnapshots .stream ()
352387 .map (snapshotDataStoreVO -> snapshotDataFactory .getSnapshot (snapshotDataStoreVO .getSnapshotId (), snapshotDataStoreVO .getDataStoreId (), DataStoreRole .Primary ).getTO ())
353388 .collect (Collectors .toList ());
354389
355- DeleteDiskOnlyVmSnapshotCommand deleteSnapshotCommand = new DeleteDiskOnlyVmSnapshotCommand (volumeSnapshotTOList );
390+ DeleteDiskOnlyVmSnapshotCommand deleteSnapshotCommand = new DeleteDiskOnlyVmSnapshotCommand (volumeSnapshotTOList , getNvramSnapshotPath ( vmSnapshotVO ) );
356391 Answer answer = agentMgr .easySend (hostId , deleteSnapshotCommand );
357392 if (answer == null || !answer .getResult ()) {
358393 logger .error ("Failed to delete VM snapshot [{}] due to {}." , vmSnapshotVO .getUuid (), answer != null ? answer .getDetails () : "Communication failure" );
@@ -368,6 +403,21 @@ private List<SnapshotVO> deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId)
368403 return snapshotVOList ;
369404 }
370405
406+ protected void deleteNvramSnapshotIfNeeded (VMSnapshotVO vmSnapshotVO , Long hostId , PrimaryDataStoreTO primaryDataStore ) {
407+ String nvramSnapshotPath = getNvramSnapshotPath (vmSnapshotVO );
408+ if (StringUtils .isBlank (nvramSnapshotPath ) || primaryDataStore == null ) {
409+ return ;
410+ }
411+
412+ validateHostSupportsNvramSidecarCleanup (vmSnapshotVO , hostId , "delete" );
413+ DeleteDiskOnlyVmSnapshotCommand deleteSnapshotCommand = new DeleteDiskOnlyVmSnapshotCommand (List .of (), nvramSnapshotPath , primaryDataStore );
414+ Answer answer = agentMgr .easySend (hostId , deleteSnapshotCommand );
415+ if (answer == null || !answer .getResult ()) {
416+ logger .warn ("Failed to delete the NVRAM sidecar of VM snapshot [{}] due to {}." , vmSnapshotVO .getUuid (),
417+ answer != null ? answer .getDetails () : "communication failure" );
418+ }
419+ }
420+
371421 private List <SnapshotVO > mergeSnapshots (VMSnapshotVO vmSnapshotVO , VMSnapshotVO childSnapshot , UserVmVO userVm , List <VolumeObjectTO > volumeObjectTOS , Long hostId ) {
372422 logger .debug ("Merging VM snapshot [{}] with its child [{}]." , vmSnapshotVO .getUuid (), childSnapshot .getUuid ());
373423
@@ -471,6 +521,7 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map<VolumeInf
471521 logger .info ("Starting disk-only VM snapshot process for VM [{}]." , userVm .getUuid ());
472522
473523 Long hostId = vmSnapshotHelper .pickRunningHost (vmSnapshot .getVmId ());
524+ validateHostSupportsUefiNvramAwareDiskOnlySnapshots (hostId , userVm , "create" );
474525 VMSnapshotVO vmSnapshotVO = (VMSnapshotVO ) vmSnapshot ;
475526 List <VolumeObjectTO > volumeTOs = vmSnapshotHelper .getVolumeTOList (userVm .getId ());
476527
@@ -493,14 +544,18 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map<VolumeInf
493544
494545 VMSnapshotTO target = new VMSnapshotTO (vmSnapshot .getId (), vmSnapshot .getName (), vmSnapshot .getType (), null , vmSnapshot .getDescription (), false , parentSnapshotTo , quiesceVm );
495546
496- CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand (userVm .getInstanceName (), target , volumeTOs , null , userVm .getState ());
547+ CreateDiskOnlyVmSnapshotCommand ccmd =
548+ new CreateDiskOnlyVmSnapshotCommand (userVm .getInstanceName (), userVm .getUuid (), target , volumeTOs , null , userVm .getState (), isUefiVm (userVm ));
497549
498550 logger .info ("Sending disk-only VM snapshot creation of VM Snapshot [{}] command for host [{}]." , vmSnapshot .getUuid (), hostId );
499551 Answer answer = agentMgr .easySend (hostId , ccmd );
500552
501553 if (answer != null && answer .getResult ()) {
502554 CreateDiskOnlyVmSnapshotAnswer createDiskOnlyVMSnapshotAnswer = (CreateDiskOnlyVmSnapshotAnswer ) answer ;
503- return processCreateVmSnapshotAnswer (vmSnapshot , volumeInfoToSnapshotObjectMap , createDiskOnlyVMSnapshotAnswer , userVm , vmSnapshotVO , virtualSize , parentSnapshotVo );
555+ VMSnapshot createdVmSnapshot = processCreateVmSnapshotAnswer (vmSnapshot , volumeInfoToSnapshotObjectMap , createDiskOnlyVMSnapshotAnswer , userVm , vmSnapshotVO ,
556+ virtualSize , parentSnapshotVo );
557+ notifyGuestRecoveryIssueIfNeeded (createDiskOnlyVMSnapshotAnswer , userVm , vmSnapshotVO );
558+ return createdVmSnapshot ;
504559 }
505560
506561 logger .error ("Disk-only VM snapshot for VM [{}] failed{}." , userVm .getUuid (), answer != null ? " due to" + answer .getDetails () : "" );
@@ -541,6 +596,14 @@ private VMSnapshot processCreateVmSnapshotAnswer(VMSnapshot vmSnapshot, Map<Volu
541596 publishUsageEvent (EventTypes .EVENT_VM_SNAPSHOT_CREATE , vmSnapshot , userVm , (VolumeObjectTO ) volumeInfo .getTO ());
542597 }
543598
599+ if (StringUtils .isNotBlank (answer .getNvramSnapshotPath ())) {
600+ vmSnapshotDetailsDao .addDetail (vmSnapshot .getId (), KVM_FILE_BASED_STORAGE_SNAPSHOT_NVRAM , answer .getNvramSnapshotPath (), false );
601+ } else if (isUefiVm (userVm )) {
602+ logger .warn ("Disk-only snapshot [{}] for UEFI VM [{}] was created without an NVRAM sidecar and cannot be safely reverted. "
603+ + "Upgrade the KVM agent and take a new snapshot." ,
604+ vmSnapshot .getUuid (), userVm .getUuid ());
605+ }
606+
544607 vmSnapshotVO .setCurrent (true );
545608 vmSnapshotDao .persist (vmSnapshotVO );
546609
@@ -651,6 +714,101 @@ private long getVMSnapshotRealSize(VMSnapshotVO vmSnapshot) {
651714 return realSize ;
652715 }
653716
717+ protected boolean isUefiVm (UserVm userVm ) {
718+ return vmInstanceDetailsDao .findDetail (userVm .getId (), ApiConstants .BootType .UEFI .toString ()) != null ;
719+ }
720+
721+ protected PrimaryDataStoreTO getRootVolumePrimaryDataStore (List <VolumeObjectTO > volumeTOs ) {
722+ return (PrimaryDataStoreTO ) volumeTOs .stream ()
723+ .filter (volumeObjectTO -> Volume .Type .ROOT .equals (volumeObjectTO .getVolumeType ()))
724+ .findFirst ()
725+ .orElseThrow (() -> new CloudRuntimeException ("Failed to locate the root volume while handling the VM snapshot." ))
726+ .getDataStore ();
727+ }
728+
729+ protected PrimaryDataStoreTO getRootVolumePrimaryDataStoreForCleanup (VMSnapshotVO vmSnapshot , List <VolumeObjectTO > volumeTOs ) {
730+ try {
731+ return getRootVolumePrimaryDataStore (volumeTOs );
732+ } catch (CloudRuntimeException e ) {
733+ logger .warn ("Failed to locate the root volume while cleaning up the NVRAM sidecar for VM snapshot [{}]." , vmSnapshot .getUuid (), e );
734+ return null ;
735+ }
736+ }
737+
738+ protected PrimaryDataStoreTO getPrimaryDataStoreForNvramCleanup (VMSnapshotVO vmSnapshot , List <VolumeObjectTO > volumeTOs ) {
739+ PrimaryDataStoreTO rootSnapshotPrimaryDataStore = getRootSnapshotPrimaryDataStoreForCleanup (vmSnapshot );
740+ return rootSnapshotPrimaryDataStore != null ? rootSnapshotPrimaryDataStore : getRootVolumePrimaryDataStoreForCleanup (vmSnapshot , volumeTOs );
741+ }
742+
743+ protected PrimaryDataStoreTO getRootSnapshotPrimaryDataStoreForCleanup (VMSnapshotVO vmSnapshot ) {
744+ try {
745+ return (PrimaryDataStoreTO ) getVolumeSnapshotsAssociatedWithVmSnapshot (vmSnapshot ).stream ()
746+ .map (snapshotDataStoreVO -> (SnapshotObjectTO ) snapshotDataFactory .getSnapshot (snapshotDataStoreVO .getSnapshotId (),
747+ snapshotDataStoreVO .getDataStoreId (), DataStoreRole .Primary ).getTO ())
748+ .filter (snapshotObjectTO -> Volume .Type .ROOT .equals (snapshotObjectTO .getVolume ().getVolumeType ()))
749+ .findFirst ()
750+ .orElseThrow (() -> new CloudRuntimeException ("Failed to locate the root volume snapshot while handling the VM snapshot." ))
751+ .getDataStore ();
752+ } catch (CloudRuntimeException e ) {
753+ logger .warn ("Failed to locate the root volume snapshot while cleaning up the NVRAM sidecar for VM snapshot [{}]." , vmSnapshot .getUuid (), e );
754+ return null ;
755+ }
756+ }
757+
758+ protected String getNvramSnapshotPath (VMSnapshotVO vmSnapshot ) {
759+ VMSnapshotDetailsVO nvramDetail = vmSnapshotDetailsDao .findDetail (vmSnapshot .getId (), KVM_FILE_BASED_STORAGE_SNAPSHOT_NVRAM );
760+ return nvramDetail != null ? nvramDetail .getValue () : null ;
761+ }
762+
763+ protected void validateHostSupportsUefiNvramAwareDiskOnlySnapshots (Long hostId , UserVm userVm , String operation ) {
764+ if (!isUefiVm (userVm )) {
765+ return ;
766+ }
767+
768+ if (!isHostCapabilityEnabled (hostId , Host .HOST_UEFI_ENABLE )) {
769+ throw new CloudRuntimeException (String .format ("Cannot %s a disk-only snapshot for UEFI VM [%s] on host [%s] because the host does not advertise "
770+ + "UEFI support. Ensure the host is configured with UEFI support and retry." , operation , userVm .getUuid (), hostId ));
771+ }
772+
773+ if (!isHostCapabilityEnabled (hostId , Host .HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM )) {
774+ 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 "
775+ + "NVRAM-aware disk-only snapshot support. Upgrade the host and retry." , operation , userVm .getUuid (), hostId ));
776+ }
777+ }
778+
779+ protected boolean isHostCapabilityEnabled (Long hostId , String capabilityName ) {
780+ DetailVO hostCapability = hostDetailsDao .findDetail (hostId , capabilityName );
781+ return hostCapability != null && Boolean .parseBoolean (hostCapability .getValue ());
782+ }
783+
784+ protected void validateHostSupportsNvramSidecarCleanup (VMSnapshotVO vmSnapshotVO , Long hostId , String operation ) {
785+ if (StringUtils .isBlank (getNvramSnapshotPath (vmSnapshotVO ))) {
786+ return ;
787+ }
788+
789+ if (!isHostCapabilityEnabled (hostId , Host .HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM )) {
790+ throw new CloudRuntimeException (String .format ("Cannot %s VM snapshot [%s] on host [%s] because the KVM agent does not advertise "
791+ + "NVRAM-aware disk-only snapshot support and the snapshot has an NVRAM sidecar that must be cleaned up. Upgrade the host and retry." ,
792+ operation , vmSnapshotVO .getUuid (), hostId ));
793+ }
794+ }
795+
796+ protected void notifyGuestRecoveryIssueIfNeeded (CreateDiskOnlyVmSnapshotAnswer answer , UserVm userVm , VMSnapshotVO vmSnapshot ) {
797+ if (StringUtils .isBlank (answer .getDetails ())) {
798+ return ;
799+ }
800+
801+ String subject = String .format ("Disk-only VM snapshot [%s] completed with guest recovery warnings" , vmSnapshot .getUuid ());
802+ String message = String .format ("Disk-only VM snapshot [%s] for UEFI VM [%s] completed, but post-snapshot guest recovery reported: %s" ,
803+ vmSnapshot .getUuid (), userVm .getUuid (), answer .getDetails ());
804+ logger .error (message );
805+ try {
806+ alertManager .sendAlert (AlertManager .AlertType .ALERT_TYPE_VM_SNAPSHOT , userVm .getDataCenterId (), userVm .getPodIdToDeployIn (), subject , message );
807+ } catch (Exception e ) {
808+ logger .warn ("Failed to send post-snapshot guest recovery alert for VM snapshot [{}]." , vmSnapshot .getUuid (), e );
809+ }
810+ }
811+
654812 /**
655813 * 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).
656814 * This is done because, when using <a href="https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockCommit">virDomainBlockCommit</a>}, Libvirt will maintain
0 commit comments