Skip to content

Commit dccfe7d

Browse files
authored
linstor: fix live migration with storage into Linstor when the target host has no replica (#14344)
* linstor: make the new resource available on the target host for live migration * linstor: use the target host's CPU shares for live migration with storage
1 parent b92ab08 commit dccfe7d

3 files changed

Lines changed: 464 additions & 23 deletions

File tree

‎plugins/storage/volume/linstor/CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ All notable changes to Linstor CloudStack plugin will be documented in this file
2424
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2525
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2626

27+
## [2026-10-07]
28+
29+
### Fixed
30+
31+
- Live migration with storage into Linstor failed with "cannot precreate storage for disk type 'block'"
32+
if the auto-placement didn't put a copy of the new resource on the target host; the resource is
33+
now made available on the target host before the migration starts.
34+
- Live migration with storage into Linstor failed on cgroup v2 hosts with
35+
"shares '<n>' must be in range [1, 10000]" for VMs with more than 10000 cpus * MHz, and smaller
36+
VMs ended up with an unscaled CPU weight; the CPU shares calculated by the target host are now used.
37+
2738
## [2026-07-30]
2839

2940
### Added

‎plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java‎

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.linbit.linstor.api.model.ApiCallRcList;
2525
import com.linbit.linstor.api.model.ResourceDefinition;
2626
import com.linbit.linstor.api.model.ResourceDefinitionModify;
27+
import com.linbit.linstor.api.model.ResourceMakeAvailable;
2728

2829
import javax.inject.Inject;
2930

@@ -35,8 +36,11 @@
3536

3637
import com.cloud.agent.AgentManager;
3738
import com.cloud.agent.api.Answer;
39+
import com.cloud.agent.api.CheckVirtualMachineAnswer;
40+
import com.cloud.agent.api.CheckVirtualMachineCommand;
3841
import com.cloud.agent.api.MigrateAnswer;
3942
import com.cloud.agent.api.MigrateCommand;
43+
import com.cloud.agent.api.PrepareForMigrationAnswer;
4044
import com.cloud.agent.api.PrepareForMigrationCommand;
4145
import com.cloud.agent.api.to.DataObjectType;
4246
import com.cloud.agent.api.to.VirtualMachineTO;
@@ -54,6 +58,7 @@
5458
import com.cloud.storage.dao.VolumeDao;
5559
import com.cloud.utils.exception.CloudRuntimeException;
5660
import com.cloud.vm.VMInstanceVO;
61+
import com.cloud.vm.VirtualMachine;
5762
import com.cloud.vm.dao.VMInstanceDao;
5863
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
5964
import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy;
@@ -74,7 +79,6 @@
7479
import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
7580
import org.apache.commons.collections.CollectionUtils;
7681
import org.apache.commons.collections.MapUtils;
77-
import org.apache.commons.lang3.ObjectUtils;
7882
import org.apache.logging.log4j.LogManager;
7983
import org.apache.logging.log4j.Logger;
8084
import org.springframework.stereotype.Component;
@@ -169,11 +173,51 @@ private VolumeVO createNewVolumeVO(Volume volume, StoragePoolVO storagePoolVO) {
169173
return _volumeDao.persist(newVol);
170174
}
171175

176+
private DevelopersApi getLinstorAPI(StoragePoolVO storagePool) {
177+
return LinstorUtil.getLinstorAPI(storagePool.getHostAddress(),
178+
LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()),
179+
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId())));
180+
}
181+
182+
/**
183+
* Makes the new resource available on the migration target host and returns its device path there.
184+
*
185+
* <p>The resource is spawned by LINSTOR's auto-placement, which does not know about the target host.
186+
* libvirt can only precreate file disks on the target, so if the block device is missing there the
187+
* migration fails with "cannot precreate storage for disk type 'block'". The source disk is not on
188+
* LINSTOR, so a plain (diskless for DRBD) make-available is enough, no dual-primary needed.</p>
189+
*/
190+
private String makeAvailableOnHost(StoragePoolVO storagePool, String rscName, Host host) {
191+
DevelopersApi api = getLinstorAPI(storagePool);
192+
try {
193+
logger.info("Linstor: make resource {} available on migration target {}", rscName, host.getName());
194+
ApiCallRcList answers = api.resourceMakeAvailableOnNode(rscName, host.getName(), new ResourceMakeAvailable());
195+
LinstorUtil.checkLinstorAnswersThrow(answers);
196+
return LinstorUtil.getDevicePath(api, rscName);
197+
} catch (ApiException apiEx) {
198+
logger.error("Linstor: ApiEx - {}", apiEx.getMessage());
199+
throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
200+
}
201+
}
202+
203+
/**
204+
* A paused domain (incoming migration still in progress) is reported as PowerUnknown, so this
205+
* is only true once the VM really runs on the host.
206+
*/
207+
private boolean isVmRunningOnHost(VirtualMachineTO vmTO, Host host) {
208+
try {
209+
Answer answer = _agentManager.send(host.getId(), new CheckVirtualMachineCommand(vmTO.getName()));
210+
return answer instanceof CheckVirtualMachineAnswer && answer.getResult() &&
211+
VirtualMachine.PowerState.PowerOn.equals(((CheckVirtualMachineAnswer) answer).getState());
212+
} catch (AgentUnavailableException | OperationTimedoutException e) {
213+
logger.warn("Unable to check if VM [{}] is running on host [{}]", vmTO, host, e);
214+
return false;
215+
}
216+
}
217+
172218
private void removeExactSizeProperty(VolumeInfo volumeInfo) {
173219
StoragePoolVO destStoragePool = _storagePool.findById(volumeInfo.getDataStore().getId());
174-
DevelopersApi api = LinstorUtil.getLinstorAPI(destStoragePool.getHostAddress(),
175-
LinstorConfigurationManager.ApiToken.valueIn(destStoragePool.getId()),
176-
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(destStoragePool.getId())));
220+
DevelopersApi api = getLinstorAPI(destStoragePool);
177221

178222
ResourceDefinitionModify rdm = new ResourceDefinitionModify();
179223
rdm.setDeleteProps(Collections.singletonList(LinstorUtil.LIN_PROP_DRBDOPT_EXACT_SIZE));
@@ -266,10 +310,10 @@ private void handlePostMigration(boolean success, Map<VolumeInfo, VolumeInfo> sr
266310
_volumeService.expungeVolumeAsync(destVolumeInfo);
267311

268312
if (destroyFuture.get().isFailed()) {
269-
logger.debug("Failed to clean up dest volume on storage");
313+
logger.warn("Failed to clean up dest volume {} on storage", destVolumeInfo);
270314
}
271315
} catch (Exception e) {
272-
logger.debug("Failed to clean up dest volume on storage", e);
316+
logger.warn("Failed to clean up dest volume {} on storage", destVolumeInfo, e);
273317
}
274318
}
275319
}
@@ -293,9 +337,7 @@ private void handlePostMigration(boolean success, Map<VolumeInfo, VolumeInfo> sr
293337
private boolean needsExactSizeProp(VolumeInfo srcVolumeInfo) {
294338
StoragePoolVO srcStoragePool = _storagePool.findById(srcVolumeInfo.getDataStore().getId());
295339
if (srcStoragePool.getPoolType() == Storage.StoragePoolType.Linstor) {
296-
DevelopersApi api = LinstorUtil.getLinstorAPI(srcStoragePool.getHostAddress(),
297-
LinstorConfigurationManager.ApiToken.valueIn(srcStoragePool.getId()),
298-
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(srcStoragePool.getId())));
340+
DevelopersApi api = getLinstorAPI(srcStoragePool);
299341

300342
String rscName = LinstorUtil.RSC_PREFIX + srcVolumeInfo.getPath();
301343
try {
@@ -335,6 +377,7 @@ public void copyAsync(Map<VolumeInfo, DataStore> volumeDataStoreMap, VirtualMach
335377

336378
Map<String, MigrateCommand.MigrateDiskInfo> migrateStorage = new HashMap<>();
337379
Map<VolumeInfo, VolumeInfo> srcVolumeInfoToDestVolumeInfo = new HashMap<>();
380+
boolean migrateCommandSent = false;
338381

339382
try {
340383
for (Map.Entry<VolumeInfo, DataStore> entry : volumeDataStoreMap.entrySet()) {
@@ -351,20 +394,25 @@ public void copyAsync(Map<VolumeInfo, DataStore> volumeDataStoreMap, VirtualMach
351394
VolumeVO destVolume = createNewVolumeVO(srcVolume, destStoragePool);
352395

353396
VolumeInfo destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore);
397+
// registered right away, so a failure from here on cleans the new volume up again
398+
srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo);
354399

355400
destVolumeInfo.processEvent(ObjectInDataStoreStateMachine.Event.MigrationCopyRequested);
356401
destVolumeInfo.processEvent(ObjectInDataStoreStateMachine.Event.MigrationCopySucceeded);
357402
destVolumeInfo.processEvent(ObjectInDataStoreStateMachine.Event.MigrationRequested);
358403

359404
boolean exactSize = needsExactSizeProp(srcVolumeInfo);
360405

361-
String devPath = LinstorUtil.createResource(
362-
destVolumeInfo, destStoragePool, _storagePoolDao, exactSize);
406+
LinstorUtil.createResource(destVolumeInfo, destStoragePool, _storagePoolDao, exactSize);
363407

364408
_volumeDao.update(destVolume.getId(), destVolume);
365409
destVolume = _volumeDao.findById(destVolume.getId());
366410

367411
destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore);
412+
srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo);
413+
414+
String devPath = makeAvailableOnHost(
415+
destStoragePool, LinstorUtil.RSC_PREFIX + destVolumeInfo.getUuid(), destHost);
368416

369417
MigrateCommand.MigrateDiskInfo migrateDiskInfo = new MigrateCommand.MigrateDiskInfo(
370418
srcVolumeInfo.getPath(),
@@ -375,13 +423,12 @@ public void copyAsync(Map<VolumeInfo, DataStore> volumeDataStoreMap, VirtualMach
375423
migrateDiskInfoList.add(migrateDiskInfo);
376424

377425
migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo);
378-
379-
srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo);
380426
}
381427

382428
PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO);
429+
Answer pfma;
383430
try {
384-
Answer pfma = _agentManager.send(destHost.getId(), pfmc);
431+
pfma = _agentManager.send(destHost.getId(), pfmc);
385432

386433
if (pfma == null || !pfma.getResult()) {
387434
String details = pfma != null ? pfma.getDetails() : "null answer returned";
@@ -403,33 +450,60 @@ public void copyAsync(Map<VolumeInfo, DataStore> volumeDataStoreMap, VirtualMach
403450
migrateCommand.setWait(StorageManager.KvmStorageOnlineMigrationWait.value());
404451
migrateCommand.setMigrateStorage(migrateStorage);
405452
migrateCommand.setMigrateStorageManaged(true);
406-
migrateCommand.setNewVmCpuShares(
407-
vmTO.getCpus() * ObjectUtils.defaultIfNull(vmTO.getMinSpeed(), vmTO.getSpeed()));
453+
// the target host scales the shares to its cgroup version (v2 only accepts 1-10000),
454+
// the raw cpus * speed value is only valid on cgroup v1
455+
Integer newVmCpuShares = ((PrepareForMigrationAnswer) pfma).getNewVmCpuShares();
456+
if (newVmCpuShares != null) {
457+
migrateCommand.setNewVmCpuShares(newVmCpuShares);
458+
}
408459
migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList);
409460

410461
boolean kvmAutoConvergence = StorageManager.KvmAutoConvergence.value();
411462
migrateCommand.setAutoConvergence(kvmAutoConvergence);
412463

413-
MigrateAnswer migrateAnswer = (MigrateAnswer) _agentManager.send(srcHost.getId(), migrateCommand);
414-
boolean success = migrateAnswer != null && migrateAnswer.getResult();
464+
// once the migrate command is sent, the VM may end up running on the new volumes,
465+
// so they are only removed if the source host reports the migration as failed
466+
migrateCommandSent = true;
467+
MigrateAnswer migrateAnswer = null;
468+
boolean success;
469+
try {
470+
migrateAnswer = (MigrateAnswer) _agentManager.send(srcHost.getId(), migrateCommand);
471+
success = migrateAnswer != null && migrateAnswer.getResult();
472+
} catch (OperationTimedoutException ex) {
473+
// no answer from the source host, but the migration may still have finished
474+
if (!isVmRunningOnHost(vmTO, destHost)) {
475+
throw ex;
476+
}
477+
logger.info("VM [{}] is running on the destination host [{}], migration was successful", vmTO, destHost);
478+
success = true;
479+
}
415480

416481
handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost);
417482

418-
if (migrateAnswer == null) {
419-
throw new CloudRuntimeException("Unable to get an answer to the migrate command");
420-
}
483+
if (!success) {
484+
if (migrateAnswer == null) {
485+
throw new CloudRuntimeException("Unable to get an answer to the migrate command");
486+
}
421487

422-
if (!migrateAnswer.getResult()) {
423488
errMsg = migrateAnswer.getDetails();
424489

425490
throw new CloudRuntimeException(errMsg);
426491
}
427492
} catch (AgentUnavailableException | OperationTimedoutException | CloudRuntimeException ex) {
428493
errMsg = String.format(
429494
"Copy volume(s) of VM [%s] to storage(s) [%s] and VM to host [%s] failed in LinstorDataMotionStrategy.copyAsync. Error message: [%s].",
430-
vmTO, srcHost, destHost, ex.getMessage());
495+
vmTO, volumeDataStoreMap.values(), destHost, ex.getMessage());
431496
logger.error(errMsg, ex);
432497

498+
if (!migrateCommandSent && !srcVolumeInfoToDestVolumeInfo.isEmpty()) {
499+
// failed before the migration was started: remove the already created destination volumes
500+
try {
501+
handlePostMigration(false, srcVolumeInfoToDestVolumeInfo, vmTO, destHost);
502+
} catch (Exception e) {
503+
logger.warn("Failed to clean up the destination volume(s) of VM [{}]", vmTO, e);
504+
}
505+
}
506+
433507
throw new CloudRuntimeException(errMsg);
434508
} finally {
435509
CopyCmdAnswer copyCmdAnswer = new CopyCmdAnswer(errMsg);

0 commit comments

Comments
 (0)