Skip to content

Commit be61c04

Browse files
committed
preserve restore vm uuid
A new UUID for the restored VM changes the serial for VM h/w info. This may prevent cloud-init from re-running on restore.
1 parent c916518 commit be61c04

8 files changed

Lines changed: 114 additions & 44 deletions

File tree

api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public abstract class BaseAsyncCreateCustomIdCmd extends BaseAsyncCreateCmd {
2020
@Parameter(name = ApiConstants.CUSTOM_ID,
2121
type = CommandType.STRING,
2222
description = "An optional field, in case you want to set a custom id to the resource. Allowed to Root Admins only")
23-
private String customId;
23+
protected String customId;
2424

2525
public String getCustomId() {
2626
return customId;

api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,8 @@ public void setBlankInstance(boolean blankInstance) {
9090
public void setInstanceType(String instanceType) {
9191
this.instanceType = instanceType;
9292
}
93+
94+
public void setCustomId(String customId) {
95+
this.customId = customId;
96+
}
9397
}

plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@
214214
import com.cloud.utils.component.ComponentContext;
215215
import com.cloud.utils.component.ManagerBase;
216216
import com.cloud.utils.db.Filter;
217+
import com.cloud.utils.db.Transaction;
218+
import com.cloud.utils.db.TransactionCallbackWithException;
217219
import com.cloud.utils.exception.CloudRuntimeException;
218220
import com.cloud.vm.NicVO;
219221
import com.cloud.vm.UserVmManager;
@@ -377,7 +379,7 @@ protected void waitForJobCompletion(long jobId) {
377379
final long deadline = System.nanoTime() + timeoutNanos;
378380
long sleepMillis = 500;
379381
while (true) {
380-
AsyncJobVO job = asyncJobDao.findById(jobId);
382+
AsyncJobVO job = asyncJobDao.findByIdIncludingRemoved(jobId);
381383
if (job == null) {
382384
logger.warn("Async job with ID {} not found", jobId);
383385
return;
@@ -568,8 +570,8 @@ protected ServiceOffering getServiceOfferingIdForVmCreation(com.cloud.dc.DataCen
568570
return offering;
569571
}
570572

571-
protected GuestOS getGuestOsForInstance(Vm request, boolean isWorkerVm) {
572-
if (isWorkerVm) {
573+
protected GuestOS getGuestOsForInstance(Vm request) {
574+
if (request.isWorkerVm()) {
573575
GuestOS os = guestOSDao.findOneByDisplayName(WORKER_VM_GUEST_OS);
574576
if (os == null) {
575577
logger.warn("Guest OS with name {} for worker VM not found, VM will be created with default guest OS",
@@ -719,11 +721,48 @@ protected String getValidatedInstanceType(Vm request) {
719721
return instanceType;
720722
}
721723

724+
protected UserVm createVmWithForRestoreIfNeeded(DeployVMCmdByAdmin cmd, Vm request) {
725+
return Transaction.execute((TransactionCallbackWithException<UserVm, CloudRuntimeException>) status -> {
726+
final String uuid = request.getInstanceId();
727+
if (StringUtils.isNotBlank(uuid)) {
728+
logger.debug("UUID for new VM needs to be {}", uuid);
729+
updateOldInstanceUuid(uuid);
730+
cmd.setCustomId(uuid);
731+
}
732+
try {
733+
return userVmManager.createVirtualMachine(cmd);
734+
} catch (InsufficientCapacityException | ResourceUnavailableException |
735+
ResourceAllocationException | CloudRuntimeException e) {
736+
throw new CloudRuntimeException("Failed to create VM: " + e.getMessage(), e);
737+
}
738+
});
739+
}
740+
741+
private void updateOldInstanceUuid(String uuid) {
742+
UserVmVO oldVM = userVmDao.findByUuidIncludingRemoved(uuid);
743+
if (oldVM == null) {
744+
return;
745+
}
746+
if (oldVM.getRemoved() == null) {
747+
logger.error("Found existing VM with the UUID from request data and it is not removed. Cannot " +
748+
"proceed with UUID update. VM with conflicting UUID: {}", oldVM);
749+
throw new CloudRuntimeException("VM with ID " + uuid + " already exists and is not removed. Cannot proceed with VM creation for restore.");
750+
}
751+
logger.debug("Found removed VM with UUID from request data. Updating its UUID to a new random " +
752+
"value to free up the requested UUID for the new VM. Old VM: {}", uuid, oldVM);
753+
UserVmVO updateObj = userVmDao.createForUpdate(oldVM.getId());
754+
updateObj.setUuid(UUID.randomUUID().toString());
755+
userVmDao.update(updateObj.getId(), updateObj);
756+
}
757+
722758
protected Pair<Vm, UserVm> createInstance(com.cloud.dc.DataCenter zone, Long clusterId, Account owner, Long domainId,
723759
String accountName, Long projectId, String name, String displayName, String serviceOfferingUuid,
724760
int cpu, int memory, String templateUuid, GuestOS guestOs, String userdata, ApiConstants.BootType bootType,
725-
ApiConstants.BootMode bootMode, String affinityGroupId, String userDataId, String sshKeyPairNames,
726-
String instanceType, Map<String, String> details) {
761+
ApiConstants.BootMode bootMode, Vm request) {
762+
final String affinityGroupId = request.getAffinityGroupId();
763+
final String userDataId = request.getUserDataId();
764+
final String sshKeyPairNames = request.getSshKeyPairNames();
765+
final Map<String, String> details = request.getDetails();
727766
Account account = owner != null ? owner : CallContext.current().getCallingAccount();
728767
ServiceOffering serviceOffering = getServiceOfferingIdForVmCreation(zone, account, serviceOfferingUuid, cpu,
729768
memory);
@@ -773,30 +812,27 @@ protected Pair<Vm, UserVm> createInstance(com.cloud.dc.DataCenter zone, Long clu
773812
if (StringUtils.isNotBlank(sshKeyPairNames)) {
774813
cmd.setSshKeyPairNames(getValidatedSshKeyPairNames(sshKeyPairNames, owner));
775814
}
815+
final String instanceType = getValidatedInstanceType(request);
776816
cmd.setInstanceType(StringUtils.trimToNull(instanceType));
777817
cmd.setHypervisor(Hypervisor.HypervisorType.KVM.name());
778-
Map<String, String> instanceDetails = getDetailsForInstanceCreation(userdata, serviceOffering, details);
818+
Map<String, String> instanceDetails = getDetailsForInstanceCreation(request, serviceOffering, details);
779819
if (MapUtils.isNotEmpty(instanceDetails)) {
780820
Map<Integer, Map<String, String>> map = new HashMap<>();
781821
map.put(0, instanceDetails);
782822
cmd.setDetails(map);
783823
}
784824
cmd.setBlankInstance(true);
785-
try {
786-
UserVm vm = userVmManager.createVirtualMachine(cmd);
787-
vm = userVmManager.finalizeCreateVirtualMachine(vm.getId());
788-
UserVmJoinVO vo = userVmJoinDao.findById(vm.getId());
789-
Vm vmObj = UserVmJoinVOToVmConverter.toVm(vo, this::getHostById, this::getDetailsByInstanceId,
790-
this::listTagsByInstanceId, this::listDiskAttachmentsByInstanceId, this::listNicsByInstance,
791-
null, null, false);
792-
return new Pair<>(vmObj, vm);
793-
} catch (InsufficientCapacityException | ResourceUnavailableException | ResourceAllocationException | CloudRuntimeException e) {
794-
throw new CloudRuntimeException("Failed to create VM: " + e.getMessage(), e);
795-
}
825+
UserVm vm = createVmWithForRestoreIfNeeded(cmd, request);
826+
vm = userVmManager.finalizeCreateVirtualMachine(vm.getId());
827+
UserVmJoinVO vo = userVmJoinDao.findById(vm.getId());
828+
Vm vmObj = UserVmJoinVOToVmConverter.toVm(vo, this::getHostById, this::getDetailsByInstanceId,
829+
this::listTagsByInstanceId, this::listDiskAttachmentsByInstanceId, this::listNicsByInstance,
830+
null, null, false);
831+
return new Pair<>(vmObj, vm);
796832
}
797833

798834
@NotNull
799-
protected static Map<String, String> getDetailsForInstanceCreation(String userdata, ServiceOffering serviceOffering,
835+
protected static Map<String, String> getDetailsForInstanceCreation(Vm request, ServiceOffering serviceOffering,
800836
Map<String, String> existingDetails) {
801837
Map<String, String> details = new HashMap<>();
802838
List<String> detailsTobeSkipped = List.of(
@@ -810,7 +846,7 @@ protected static Map<String, String> getDetailsForInstanceCreation(String userda
810846
details.put(entry.getKey(), entry.getValue());
811847
}
812848
}
813-
if (StringUtils.isNotEmpty(userdata)) {
849+
if (request.isWorkerVm()) {
814850
// Assumption: Only worker VM will have userdata and it needs CPU mode
815851
details.put(VmDetailConstants.GUEST_CPU_MODE, WORKER_VM_GUEST_CPU_MODE);
816852
}
@@ -1277,8 +1313,9 @@ public Vm getInstance(String uuid, boolean includeTags, boolean includeDisks, bo
12771313
@ApiAccess(command = DeployVMCmd.class)
12781314
public Vm createInstance(Vm request) {
12791315
if (request == null) {
1280-
throw new InvalidParameterValueException("Request disk data is empty");
1316+
throw new InvalidParameterValueException("Request VM data is empty");
12811317
}
1318+
logger.debug("VM create request [worker-vm: {}]", request.isWorkerVm());
12821319
OvfXmlUtil.updateFromConfiguration(request);
12831320
String name = request.getName();
12841321
if (StringUtils.isBlank(name)) {
@@ -1334,13 +1371,11 @@ public Vm createInstance(Vm request) {
13341371
if (request.getTemplate() != null && StringUtils.isNotEmpty(request.getTemplate().getId())) {
13351372
templateUuid = request.getTemplate().getId();
13361373
}
1337-
GuestOS guestOs = getGuestOsForInstance(request, request.isWorkerVm());
1338-
String instanceType = getValidatedInstanceType(request);
1374+
GuestOS guestOs = getGuestOsForInstance(request);
13391375
Pair<Vm, UserVm> result = createInstance(zone, clusterId, owner, ownerDetails.first(), ownerDetails.second(),
13401376
ownerDetails.third(), name, displayName, serviceOfferingUuid, cpu, memoryMB, templateUuid, guestOs,
1341-
userdata, bootOptions.first(), bootOptions.second(), request.getAffinityGroupId(),
1342-
request.getUserDataId(), request.getSshKeyPairNames(), instanceType, request.getDetails());
1343-
saveInstanceAdditionalDetails(request, result.second());
1377+
userdata, bootOptions.first(), bootOptions.second(), request);
1378+
saveInstanceRestoreConfig(request, result.second());
13441379
return result.first();
13451380
}
13461381

plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtil.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,10 +504,13 @@ protected static String getVmConfigurationData(Vm vm) {
504504
if (initialization == null) {
505505
return null;
506506
}
507-
Vm.Initialization.Configuration configuration = vm.getInitialization().getConfiguration();
507+
Vm.Initialization.Configuration configuration = initialization.getConfiguration();
508508
if (configuration == null) {
509509
return null;
510510
}
511+
if (!"ovf".equalsIgnoreCase(configuration.getType())) {
512+
return null;
513+
}
511514
return configuration.getData();
512515
}
513516

@@ -803,11 +806,11 @@ private static void updateFromXmlCloudStackMetadataSection(Vm vm, Node metadataS
803806
return;
804807
}
805808
String instanceId = xpathString(xpath, metadataSection, ".//*[local-name()='InstanceId']/text()");
806-
if (StringUtils.isNotBlank(instanceId)) {
809+
if (StringUtils.isNotBlank(instanceId) && vm.isSameLocationRestore()) {
807810
vm.setInstanceId(instanceId);
808811
}
809812
String instanceName = xpathString(xpath, metadataSection, ".//*[local-name()='InstanceName']/text()");
810-
if (StringUtils.isNotBlank(instanceName)) {
813+
if (StringUtils.isNotBlank(instanceName) && vm.isSameLocationRestore()) {
811814
vm.setInstanceName(instanceName);
812815
}
813816
String instanceGroupId = xpathString(xpath, metadataSection, ".//*[local-name()='InstanceGroupId']/text()");

plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Vm.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ public static Bios getBiosFromOrdinal(String bootTypeStr) {
513513
if (type == Type.q35_ovmf.ordinal()) {
514514
bios.setType(Type.q35_ovmf.name());
515515
} else if (type == Type.q35_secure_boot.ordinal()) {
516-
bios.setType(Type.q35_secure_boot.name());
516+
bios.setType(Type.q35_secure_boot.name());
517517
}
518518
return bios;
519519
}
@@ -629,6 +629,14 @@ public boolean isWorkerVm() {
629629
return initialization != null && StringUtils.isNotEmpty(initialization.getCustomScript());
630630
}
631631

632+
/**
633+
* Whether restoring same VM. We need to preserve IDs in that case
634+
*/
635+
@JsonIgnore
636+
public boolean isSameLocationRestore() {
637+
return initialization != null && !Boolean.TRUE.equals(initialization.isRegenerateIds());
638+
}
639+
632640
public static Vm of(String href, String id) {
633641
return withHrefAndId(new Vm(), href, id);
634642
}

plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -197,21 +197,25 @@ public void testGetProvisionedSizeInGb_Negative_Throws() {
197197

198198

199199
@Test
200-
public void testGetDetailsForInstanceCreation_WithUserdata_AddsCpuMode() {
200+
public void testGetDetailsForInstanceCreation_WorkerVm_AddsCpuMode() {
201201
ServiceOfferingVO offering = mock(ServiceOfferingVO.class);
202202
when(offering.isCustomized()).thenReturn(false);
203+
Vm request = mock(Vm.class);
204+
when(request.isWorkerVm()).thenReturn(true);
203205

204-
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation("#!/bin/bash", offering, null);
206+
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(request, offering, null);
205207

206208
assertEquals("host-passthrough", result.get(VmDetailConstants.GUEST_CPU_MODE));
207209
}
208210

209211
@Test
210-
public void testGetDetailsForInstanceCreation_NoUserdata_NoCpuMode() {
212+
public void testGetDetailsForInstanceCreation_NotWorkerVm_NoCpuMode() {
211213
ServiceOfferingVO offering = mock(ServiceOfferingVO.class);
212214
when(offering.isCustomized()).thenReturn(false);
215+
Vm request = mock(Vm.class);
216+
when(request.isWorkerVm()).thenReturn(false);
213217

214-
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(null, offering, null);
218+
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(request, offering, null);
215219

216220
assertFalse(result.containsKey(VmDetailConstants.GUEST_CPU_MODE));
217221
}
@@ -224,7 +228,10 @@ public void testGetDetailsForInstanceCreation_CustomizedOffering_AddsDetails() {
224228
when(offering.getRamSize()).thenReturn(2048);
225229
when(offering.getSpeed()).thenReturn(null);
226230

227-
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(null, offering, null);
231+
Vm request = mock(Vm.class);
232+
when(request.isWorkerVm()).thenReturn(false);
233+
234+
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(request, offering, null);
228235

229236
assertEquals("4", result.get(VmDetailConstants.CPU_NUMBER));
230237
assertEquals("2048", result.get(VmDetailConstants.MEMORY));
@@ -239,7 +246,10 @@ public void testGetDetailsForInstanceCreation_CustomizedOffering_WithSpeed_DoesN
239246
when(offering.getRamSize()).thenReturn(1024);
240247
when(offering.getSpeed()).thenReturn(2000);
241248

242-
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(null, offering, null);
249+
Vm request = mock(Vm.class);
250+
when(request.isWorkerVm()).thenReturn(false);
251+
252+
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(request, offering, null);
243253

244254
assertFalse(result.containsKey(VmDetailConstants.CPU_SPEED));
245255
}
@@ -249,12 +259,15 @@ public void testGetDetailsForInstanceCreation_SkipsBiosAndUefiKeys() {
249259
ServiceOfferingVO offering = mock(ServiceOfferingVO.class);
250260
when(offering.isCustomized()).thenReturn(false);
251261

262+
Vm request = mock(Vm.class);
263+
when(request.isWorkerVm()).thenReturn(false);
264+
252265
Map<String, String> existingDetails = new HashMap<>();
253266
existingDetails.put("BIOS", "bios_value");
254267
existingDetails.put("UEFI", "uefi_value");
255268
existingDetails.put("custom_key", "custom_value");
256269

257-
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(null, offering, existingDetails);
270+
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(request, offering, existingDetails);
258271

259272
assertFalse(result.containsKey("BIOS"));
260273
assertFalse(result.containsKey("UEFI"));
@@ -269,10 +282,13 @@ public void testGetDetailsForInstanceCreation_PreservesExistingCpuSpeed() {
269282
when(offering.getRamSize()).thenReturn(1024);
270283
when(offering.getSpeed()).thenReturn(null);
271284

285+
Vm request = mock(Vm.class);
286+
when(request.isWorkerVm()).thenReturn(false);
287+
272288
Map<String, String> existingDetails = new HashMap<>();
273289
existingDetails.put(VmDetailConstants.CPU_SPEED, "3000");
274290

275-
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(null, offering, existingDetails);
291+
Map<String, String> result = ServerAdapter.getDetailsForInstanceCreation(request, offering, existingDetails);
276292

277293
assertEquals("3000", result.get(VmDetailConstants.CPU_SPEED));
278294
}
@@ -500,24 +516,24 @@ public void testGetNetworkById_ReturnsVoFromDao() {
500516

501517
@Test
502518
public void testWaitForJobCompletion_JobNotFound_Returns() {
503-
when(asyncJobDao.findById(99L)).thenReturn(null);
519+
when(asyncJobDao.findByIdIncludingRemoved(99L)).thenReturn(null);
504520
serverAdapter.waitForJobCompletion(99L);
505-
verify(asyncJobDao).findById(99L);
521+
verify(asyncJobDao).findByIdIncludingRemoved(99L);
506522
}
507523

508524
@Test
509525
public void testWaitForJobCompletion_JobAlreadySucceeded_Returns() {
510526
AsyncJobVO job = mock(AsyncJobVO.class);
511527
when(job.getStatus()).thenReturn(AsyncJobVO.Status.SUCCEEDED);
512-
when(asyncJobDao.findById(1L)).thenReturn(job);
528+
when(asyncJobDao.findByIdIncludingRemoved(1L)).thenReturn(job);
513529
serverAdapter.waitForJobCompletion(1L);
514530
}
515531

516532
@Test
517533
public void testWaitForJobCompletion_JobAlreadyFailed_Returns() {
518534
AsyncJobVO job = mock(AsyncJobVO.class);
519535
when(job.getStatus()).thenReturn(AsyncJobVO.Status.FAILED);
520-
when(asyncJobDao.findById(2L)).thenReturn(job);
536+
when(asyncJobDao.findByIdIncludingRemoved(2L)).thenReturn(job);
521537
serverAdapter.waitForJobCompletion(2L);
522538
}
523539

@@ -536,11 +552,11 @@ public void testWaitForJobCompletion_CompletedJobJoinVO_DelegatesById() {
536552

537553
AsyncJobVO job = mock(AsyncJobVO.class);
538554
when(job.getStatus()).thenReturn(AsyncJobVO.Status.SUCCEEDED);
539-
when(asyncJobDao.findById(5L)).thenReturn(job);
555+
when(asyncJobDao.findByIdIncludingRemoved(5L)).thenReturn(job);
540556

541557
serverAdapter.waitForJobCompletion(jobVO);
542558

543-
verify(asyncJobDao).findById(5L);
559+
verify(asyncJobDao).findByIdIncludingRemoved(5L);
544560
}
545561

546562

plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/OvfXmlUtilTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public void test_restoreConfig_parse() throws Exception {
6565
assertNotNull(is);
6666
ovfXml = new String(is.readAllBytes(), StandardCharsets.UTF_8);
6767
}
68+
when(configMock.getType()).thenReturn("ovf");
6869
when(configMock.getData()).thenReturn(ovfXml);
6970

7071
String instanceConfig = OvfXmlUtil.getConfigMetadataXml(vm, mock(Logger.class));

server/src/main/java/com/cloud/vm/UserVmManagerImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4927,6 +4927,9 @@ private UserVmVO commitUserVm(final boolean isImport, final DataCenter zone, fin
49274927
offering.getLimitCpuUse(), owner.getDomainId(), owner.getId(), userId, offering.getId(), userData, userDataId, userDataDetails, hostName);
49284928
vm.setUuid(uuidName);
49294929
vm.setDynamicallyScalable(dynamicScalingEnabled);
4930+
if (_itMgr.isBlankInstance(template)) {
4931+
vm.setUpdateParameters(false);
4932+
}
49304933

49314934
Map<String, String> details = template.getDetails();
49324935
if (details != null && !details.isEmpty()) {

0 commit comments

Comments
 (0)