Skip to content

Commit 10d3e11

Browse files
Kunalbehbudkunal.behbudzade
andauthored
KVM: fix UEFI disk-only instance snapshot NVRAM handling (#13020)
Signed-off-by: kunal.behbudzade <kunal.behbudzade@btsgrp.com> Co-authored-by: kunal.behbudzade <kunal.behbudzade@btsgrp.com>
1 parent a9b2f33 commit 10d3e11

20 files changed

Lines changed: 2174 additions & 30 deletions

File tree

PendingReleaseNotes

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,17 @@ example.ver.1 > example.ver.2:
3939
which can now be attached to Instances. This is to prevent the Secondary
4040
Storage to grow to enormous sizes as Linux Distributions keep growing in
4141
size while a stripped down Linux should fit on a 2.88MB floppy.
42+
43+
4.22.0.0 > 4.22.0.1:
44+
* Disk-only instance snapshots for KVM UEFI VMs now include a sidecar copy of
45+
the active NVRAM state so revert operations restore both disk and firmware
46+
boot state consistently.
47+
48+
* UEFI disk-only instance snapshots taken before this change do not contain an
49+
NVRAM sidecar and cannot be safely reverted. Take a new snapshot after
50+
upgrading before relying on revert for UEFI VMs.
51+
52+
* Taking a disk-only instance snapshot for KVM UEFI VMs now briefly suspends
53+
the guest while the NVRAM sidecar is copied, so that the captured firmware
54+
state is consistent with the disk snapshot. Non-UEFI VMs are unaffected and
55+
continue to snapshot live.

api/src/main/java/com/cloud/host/Host.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public static String[] toStrings(Host.Type... types) {
5555
}
5656

5757
String HOST_UEFI_ENABLE = "host.uefi.enable";
58+
String HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM = "host.kvm.diskonlyvmsnapshot.nvram";
5859
String HOST_VOLUME_ENCRYPTION = "host.volume.encryption";
5960
String HOST_INSTANCE_CONVERSION = "host.instance.conversion";
6061
String HOST_VDDK_SUPPORT = "host.vddk.support";

api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
import com.cloud.vm.VirtualMachine;
3838
import com.cloud.vm.snapshot.VMSnapshot;
3939

40-
@APICommand(name = "createVMSnapshot", description = "Creates Snapshot for an Instance.", responseObject = VMSnapshotResponse.class, since = "4.2.0", entityType = {VMSnapshot.class},
40+
@APICommand(name = "createVMSnapshot", description = "Creates Snapshot for an Instance. Running KVM UEFI disk-only snapshots briefly suspend the Instance while copying NVRAM state.",
41+
responseObject = VMSnapshotResponse.class, since = "4.2.0", entityType = {VMSnapshot.class},
4142
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
4243
public class CreateVMSnapshotCmd extends BaseAsyncCreateCmd {
4344

core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,24 @@
2727
public class CreateDiskOnlyVmSnapshotAnswer extends Answer {
2828

2929
protected Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath;
30+
private String nvramSnapshotPath;
3031

3132
public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath) {
33+
this(command, success, details, mapVolumeToSnapshotSizeAndNewVolumePath, null);
34+
}
35+
36+
public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath,
37+
String nvramSnapshotPath) {
3238
super(command, success, details);
3339
this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath;
40+
this.nvramSnapshotPath = nvramSnapshotPath;
3441
}
3542

3643
public Map<String, Pair<Long, String>> getMapVolumeToSnapshotSizeAndNewVolumePath() {
3744
return mapVolumeToSnapshotSizeAndNewVolumePath;
3845
}
46+
47+
public String getNvramSnapshotPath() {
48+
return nvramSnapshotPath;
49+
}
3950
}

core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,30 @@
2929
public class CreateDiskOnlyVmSnapshotCommand extends VMSnapshotBaseCommand {
3030

3131
protected VirtualMachine.State vmState;
32+
private final String vmUuid;
33+
private final boolean uefiEnabled;
3234

3335
public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List<VolumeObjectTO> volumeTOs, String guestOSType, VirtualMachine.State vmState) {
36+
this(vmName, null, snapshot, volumeTOs, guestOSType, vmState, false);
37+
}
38+
39+
public CreateDiskOnlyVmSnapshotCommand(String vmName, String vmUuid, VMSnapshotTO snapshot, List<VolumeObjectTO> volumeTOs, String guestOSType,
40+
VirtualMachine.State vmState, boolean uefiEnabled) {
3441
super(vmName, snapshot, volumeTOs, guestOSType);
42+
this.vmUuid = vmUuid;
3543
this.vmState = vmState;
44+
this.uefiEnabled = uefiEnabled;
3645
}
3746

3847
public VirtualMachine.State getVmState() {
3948
return vmState;
4049
}
50+
51+
public String getVmUuid() {
52+
return vmUuid;
53+
}
54+
55+
public boolean isUefiEnabled() {
56+
return uefiEnabled;
57+
}
4158
}

core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,43 @@
1919
package com.cloud.agent.api.storage;
2020

2121
import com.cloud.agent.api.Command;
22-
2322
import com.cloud.agent.api.to.DataTO;
24-
23+
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
2524

2625
import java.util.List;
2726

2827
public class DeleteDiskOnlyVmSnapshotCommand extends Command {
2928

30-
List<DataTO> snapshots;
29+
private final List<DataTO> snapshots;
30+
private final String nvramSnapshotPath;
31+
private final PrimaryDataStoreTO primaryDataStore;
3132

3233
public DeleteDiskOnlyVmSnapshotCommand(List<DataTO> snapshots) {
34+
this(snapshots, null);
35+
}
36+
37+
public DeleteDiskOnlyVmSnapshotCommand(List<DataTO> snapshots, String nvramSnapshotPath) {
38+
this(snapshots, nvramSnapshotPath, null);
39+
}
40+
41+
public DeleteDiskOnlyVmSnapshotCommand(List<DataTO> snapshots, String nvramSnapshotPath, PrimaryDataStoreTO primaryDataStore) {
3342
this.snapshots = snapshots;
43+
this.nvramSnapshotPath = nvramSnapshotPath;
44+
this.primaryDataStore = primaryDataStore;
3445
}
3546

3647
public List<DataTO> getSnapshots() {
3748
return snapshots;
3849
}
3950

51+
public String getNvramSnapshotPath() {
52+
return nvramSnapshotPath;
53+
}
54+
55+
public PrimaryDataStoreTO getPrimaryDataStore() {
56+
return primaryDataStore;
57+
}
58+
4059
@Override
4160
public boolean executeInSequence() {
4261
return false;

core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,21 @@ public class RevertDiskOnlyVmSnapshotCommand extends Command {
2727

2828
private List<SnapshotObjectTO> snapshotObjectTos;
2929
private String vmName;
30+
private final String vmUuid;
31+
private final boolean uefiEnabled;
32+
private final String nvramSnapshotPath;
3033

3134
public RevertDiskOnlyVmSnapshotCommand(List<SnapshotObjectTO> snapshotObjectTos, String vmName) {
35+
this(snapshotObjectTos, vmName, null, false, null);
36+
}
37+
38+
public RevertDiskOnlyVmSnapshotCommand(List<SnapshotObjectTO> snapshotObjectTos, String vmName, String vmUuid, boolean uefiEnabled, String nvramSnapshotPath) {
3239
super();
3340
this.snapshotObjectTos = snapshotObjectTos;
3441
this.vmName = vmName;
42+
this.vmUuid = vmUuid;
43+
this.uefiEnabled = uefiEnabled;
44+
this.nvramSnapshotPath = nvramSnapshotPath;
3545
}
3646

3747
public List<SnapshotObjectTO> getSnapshotObjectTos() {
@@ -42,6 +52,18 @@ public String getVmName() {
4252
return vmName;
4353
}
4454

55+
public String getVmUuid() {
56+
return vmUuid;
57+
}
58+
59+
public boolean isUefiEnabled() {
60+
return uefiEnabled;
61+
}
62+
63+
public String getNvramSnapshotPath() {
64+
return nvramSnapshotPath;
65+
}
66+
4567
@Override
4668
public boolean executeInSequence() {
4769
return false;

engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,12 @@
108108
import com.cloud.exception.UnsupportedVersionException;
109109
import com.cloud.ha.HighAvailabilityManager;
110110
import com.cloud.host.Host;
111+
import com.cloud.host.DetailVO;
111112
import com.cloud.host.HostVO;
112113
import com.cloud.host.Status;
113114
import com.cloud.host.Status.Event;
114115
import com.cloud.host.dao.HostDao;
116+
import com.cloud.host.dao.HostDetailsDao;
115117
import com.cloud.hypervisor.Hypervisor.HypervisorType;
116118
import com.cloud.hypervisor.HypervisorGuruManager;
117119
import com.cloud.org.Cluster;
@@ -167,6 +169,8 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl
167169
@Inject
168170
protected HostDao _hostDao = null;
169171
@Inject
172+
protected HostDetailsDao _hostDetailsDao = null;
173+
@Inject
170174
private ManagementServerHostDao _mshostDao;
171175
@Inject
172176
protected OutOfBandManagementDao outOfBandManagementDao;
@@ -802,18 +806,24 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi
802806
ReadyAnswer readyAnswer = (ReadyAnswer)answer;
803807
Map<String, String> detailsMap = readyAnswer.getDetailsMap();
804808
if (detailsMap != null) {
809+
_hostDao.loadDetails(host);
810+
if (host.getDetails() == null) {
811+
host.setDetails(new HashMap<>());
812+
}
805813
String uefiEnabled = detailsMap.get(Host.HOST_UEFI_ENABLE);
814+
String diskOnlyVmSnapshotNvramSupport = detailsMap.get(Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM);
806815
String virtv2vVersion = detailsMap.get(Host.HOST_VIRTV2V_VERSION);
807816
String ovftoolVersion = detailsMap.get(Host.HOST_OVFTOOL_VERSION);
808817
String vddkSupport = detailsMap.get(Host.HOST_VDDK_SUPPORT);
809818
String vddkLibDir = detailsMap.get(Host.HOST_VDDK_LIB_DIR);
810819
String vddkVersion = detailsMap.get(Host.HOST_VDDK_VERSION);
811820
logger.debug("Got HOST_UEFI_ENABLE [{}] for host [{}]:", uefiEnabled, host);
812-
if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) {
813-
_hostDao.loadDetails(host);
821+
if (ObjectUtils.anyNotNull(uefiEnabled, diskOnlyVmSnapshotNvramSupport, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) {
814822
boolean updateNeeded = false;
815-
if (StringUtils.isNotBlank(uefiEnabled) && !uefiEnabled.equals(host.getDetails().get(Host.HOST_UEFI_ENABLE))) {
816-
host.getDetails().put(Host.HOST_UEFI_ENABLE, uefiEnabled);
823+
if (syncBooleanHostCapability(host, Host.HOST_UEFI_ENABLE, uefiEnabled)) {
824+
updateNeeded = true;
825+
}
826+
if (syncBooleanHostCapability(host, Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, diskOnlyVmSnapshotNvramSupport)) {
817827
updateNeeded = true;
818828
}
819829
if (StringUtils.isNotBlank(virtv2vVersion) && !virtv2vVersion.equals(host.getDetails().get(Host.HOST_VIRTV2V_VERSION))) {
@@ -856,6 +866,26 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi
856866
return attache;
857867
}
858868

869+
protected boolean syncBooleanHostCapability(HostVO host, String capabilityName, String advertisedValue) {
870+
if (StringUtils.isNotBlank(advertisedValue)) {
871+
if (!advertisedValue.equals(host.getDetails().get(capabilityName))) {
872+
host.getDetails().put(capabilityName, advertisedValue);
873+
return true;
874+
}
875+
return false;
876+
}
877+
878+
if (host.getDetails().containsKey(capabilityName)) {
879+
host.getDetails().remove(capabilityName);
880+
DetailVO hostDetail = _hostDetailsDao.findDetail(host.getId(), capabilityName);
881+
if (hostDetail != null) {
882+
_hostDetailsDao.remove(hostDetail.getId());
883+
}
884+
return true;
885+
}
886+
return false;
887+
}
888+
859889
@Override
860890
public boolean start() {
861891
ManagementServerHostVO msHost = _mshostDao.findByMsid(_nodeId);

engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@
1818

1919
import com.cloud.agent.Listener;
2020
import com.cloud.agent.api.Answer;
21+
import com.cloud.agent.api.ReadyAnswer;
2122
import com.cloud.agent.api.ReadyCommand;
2223
import com.cloud.agent.api.StartupCommand;
2324
import com.cloud.agent.api.StartupRoutingCommand;
2425
import com.cloud.exception.ConnectionException;
26+
import com.cloud.host.DetailVO;
2527
import com.cloud.host.Host;
2628
import com.cloud.host.HostVO;
2729
import com.cloud.host.Status;
2830
import com.cloud.host.dao.HostDao;
31+
import com.cloud.host.dao.HostDetailsDao;
2932
import com.cloud.hypervisor.Hypervisor;
3033
import com.cloud.utils.Pair;
3134
import org.junit.Assert;
@@ -34,10 +37,13 @@
3437
import org.mockito.Mockito;
3538

3639
import java.util.ArrayList;
40+
import java.util.HashMap;
41+
import java.util.Map;
3742

3843
public class AgentManagerImplTest {
3944

4045
private HostDao hostDao;
46+
private HostDetailsDao hostDetailsDao;
4147
private Listener storagePoolMonitor;
4248
private AgentAttache attache;
4349
private AgentManagerImpl mgr = Mockito.spy(new AgentManagerImpl());
@@ -46,15 +52,18 @@ public class AgentManagerImplTest {
4652

4753
@Before
4854
public void setUp() throws Exception {
49-
host = new HostVO("some-Uuid");
55+
host = Mockito.spy(new HostVO("some-Uuid"));
56+
Mockito.when(host.getId()).thenReturn(1L);
5057
host.setDataCenterId(1L);
5158
cmds = new StartupCommand[]{new StartupRoutingCommand()};
5259
attache = new ConnectedAgentAttache(null, 1L, "uuid", "kvm-attache", Hypervisor.HypervisorType.KVM, null, false);
5360

5461
hostDao = Mockito.mock(HostDao.class);
62+
hostDetailsDao = Mockito.mock(HostDetailsDao.class);
5563
storagePoolMonitor = Mockito.mock(Listener.class);
5664

5765
mgr._hostDao = hostDao;
66+
mgr._hostDetailsDao = hostDetailsDao;
5867
mgr._hostMonitors = new ArrayList<>();
5968
mgr._hostMonitors.add(new Pair<>(0, storagePoolMonitor));
6069
}
@@ -86,6 +95,32 @@ public void testNotifyMonitorsOfConnectionWhenStoragePoolConnectionHostFailure()
8695
Mockito.verify(mgr, Mockito.times(1)).handleDisconnectWithoutInvestigation(Mockito.any(attache.getClass()), Mockito.eq(Status.Event.AgentDisconnected), Mockito.eq(true), Mockito.eq(true));
8796
}
8897

98+
@Test
99+
public void testNotifyMonitorsOfConnectionClearsStaleNvramCapabilityOnReconnect() throws ConnectionException {
100+
DetailVO staleNvramCapability = Mockito.mock(DetailVO.class);
101+
ReadyAnswer readyAnswer = Mockito.mock(ReadyAnswer.class);
102+
host.setDetails(new HashMap<>(Map.of(Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString(),
103+
Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM, Boolean.TRUE.toString())));
104+
105+
Mockito.when(staleNvramCapability.getId()).thenReturn(11L);
106+
Mockito.when(hostDao.findById(Mockito.anyLong())).thenReturn(host);
107+
Mockito.doNothing().when(hostDao).loadDetails(host);
108+
Mockito.when(hostDetailsDao.findDetail(host.getId(), Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM)).thenReturn(staleNvramCapability);
109+
Mockito.doNothing().when(storagePoolMonitor).processConnect(Mockito.eq(host), Mockito.eq(cmds[0]), Mockito.eq(false));
110+
Mockito.doReturn(true).when(mgr).handleDisconnectWithoutInvestigation(Mockito.any(attache.getClass()), Mockito.any(Status.Event.class), Mockito.anyBoolean(), Mockito.anyBoolean());
111+
Mockito.when(readyAnswer.getResult()).thenReturn(true);
112+
Mockito.when(readyAnswer.getDetailsMap()).thenReturn(Map.of(Host.HOST_UEFI_ENABLE, Boolean.TRUE.toString()));
113+
Mockito.doReturn(readyAnswer).when(mgr).easySend(Mockito.anyLong(), Mockito.any(ReadyCommand.class));
114+
Mockito.doReturn(true).when(mgr).agentStatusTransitTo(Mockito.eq(host), Mockito.eq(Status.Event.Ready), Mockito.anyLong());
115+
116+
final AgentAttache agentAttache = mgr.notifyMonitorsOfConnection(attache, cmds, false);
117+
118+
Assert.assertTrue(agentAttache.isReady());
119+
Assert.assertFalse(host.getDetails().containsKey(Host.HOST_KVM_DISK_ONLY_VM_SNAPSHOT_NVRAM));
120+
Mockito.verify(hostDetailsDao).remove(11L);
121+
Mockito.verify(hostDao).saveDetails(host);
122+
}
123+
89124
@Test
90125
public void testGetTimeoutWithPositiveTimeout() {
91126
Commands commands = Mockito.mock(Commands.class);

0 commit comments

Comments
 (0)