This is an automated email from the ASF dual-hosted git repository.
sureshanaparti pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.22 by this push:
new dccfe7d1f40 linstor: fix live migration with storage into Linstor when
the target host has no replica (#14344)
dccfe7d1f40 is described below
commit dccfe7d1f40344dcb1372777e4ef4dde0726d9e3
Author: Rene Peinthor <[email protected]>
AuthorDate: Mon Oct 26 05:24:06 2026 +0100
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
---
plugins/storage/volume/linstor/CHANGELOG.md | 11 +
.../storage/motion/LinstorDataMotionStrategy.java | 120 +++++--
.../motion/LinstorDataMotionStrategyTest.java | 356 +++++++++++++++++++++
3 files changed, 464 insertions(+), 23 deletions(-)
diff --git a/plugins/storage/volume/linstor/CHANGELOG.md
b/plugins/storage/volume/linstor/CHANGELOG.md
index 3f0ef3fc417..b4d0aedda14 100644
--- a/plugins/storage/volume/linstor/CHANGELOG.md
+++ b/plugins/storage/volume/linstor/CHANGELOG.md
@@ -24,6 +24,17 @@ All notable changes to Linstor CloudStack plugin will be
documented in this file
The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).
+## [2026-10-07]
+
+### Fixed
+
+- Live migration with storage into Linstor failed with "cannot precreate
storage for disk type 'block'"
+ if the auto-placement didn't put a copy of the new resource on the target
host; the resource is
+ now made available on the target host before the migration starts.
+- Live migration with storage into Linstor failed on cgroup v2 hosts with
+ "shares '<n>' must be in range [1, 10000]" for VMs with more than 10000 cpus
* MHz, and smaller
+ VMs ended up with an unscaled CPU weight; the CPU shares calculated by the
target host are now used.
+
## [2026-07-30]
### Added
diff --git
a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java
b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java
index f64837e4832..2f0c7b39e68 100644
---
a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java
+++
b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategy.java
@@ -24,6 +24,7 @@ import com.linbit.linstor.api.DevelopersApi;
import com.linbit.linstor.api.model.ApiCallRcList;
import com.linbit.linstor.api.model.ResourceDefinition;
import com.linbit.linstor.api.model.ResourceDefinitionModify;
+import com.linbit.linstor.api.model.ResourceMakeAvailable;
import javax.inject.Inject;
@@ -35,8 +36,11 @@ import java.util.Map;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
+import com.cloud.agent.api.CheckVirtualMachineAnswer;
+import com.cloud.agent.api.CheckVirtualMachineCommand;
import com.cloud.agent.api.MigrateAnswer;
import com.cloud.agent.api.MigrateCommand;
+import com.cloud.agent.api.PrepareForMigrationAnswer;
import com.cloud.agent.api.PrepareForMigrationCommand;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.VirtualMachineTO;
@@ -54,6 +58,7 @@ import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy;
@@ -74,7 +79,6 @@ import
org.apache.cloudstack.storage.datastore.util.LinstorConfigurationManager;
import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
-import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
@@ -169,11 +173,51 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
return _volumeDao.persist(newVol);
}
+ private DevelopersApi getLinstorAPI(StoragePoolVO storagePool) {
+ return LinstorUtil.getLinstorAPI(storagePool.getHostAddress(),
+
LinstorConfigurationManager.ApiToken.valueIn(storagePool.getId()),
+
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(storagePool.getId())));
+ }
+
+ /**
+ * Makes the new resource available on the migration target host and
returns its device path there.
+ *
+ * <p>The resource is spawned by LINSTOR's auto-placement, which does not
know about the target host.
+ * libvirt can only precreate file disks on the target, so if the block
device is missing there the
+ * migration fails with "cannot precreate storage for disk type 'block'".
The source disk is not on
+ * LINSTOR, so a plain (diskless for DRBD) make-available is enough, no
dual-primary needed.</p>
+ */
+ private String makeAvailableOnHost(StoragePoolVO storagePool, String
rscName, Host host) {
+ DevelopersApi api = getLinstorAPI(storagePool);
+ try {
+ logger.info("Linstor: make resource {} available on migration
target {}", rscName, host.getName());
+ ApiCallRcList answers = api.resourceMakeAvailableOnNode(rscName,
host.getName(), new ResourceMakeAvailable());
+ LinstorUtil.checkLinstorAnswersThrow(answers);
+ return LinstorUtil.getDevicePath(api, rscName);
+ } catch (ApiException apiEx) {
+ logger.error("Linstor: ApiEx - {}", apiEx.getMessage());
+ throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx);
+ }
+ }
+
+ /**
+ * A paused domain (incoming migration still in progress) is reported as
PowerUnknown, so this
+ * is only true once the VM really runs on the host.
+ */
+ private boolean isVmRunningOnHost(VirtualMachineTO vmTO, Host host) {
+ try {
+ Answer answer = _agentManager.send(host.getId(), new
CheckVirtualMachineCommand(vmTO.getName()));
+ return answer instanceof CheckVirtualMachineAnswer &&
answer.getResult() &&
+
VirtualMachine.PowerState.PowerOn.equals(((CheckVirtualMachineAnswer)
answer).getState());
+ } catch (AgentUnavailableException | OperationTimedoutException e) {
+ logger.warn("Unable to check if VM [{}] is running on host [{}]",
vmTO, host, e);
+ return false;
+ }
+ }
+
private void removeExactSizeProperty(VolumeInfo volumeInfo) {
StoragePoolVO destStoragePool =
_storagePool.findById(volumeInfo.getDataStore().getId());
- DevelopersApi api =
LinstorUtil.getLinstorAPI(destStoragePool.getHostAddress(),
-
LinstorConfigurationManager.ApiToken.valueIn(destStoragePool.getId()),
-
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(destStoragePool.getId())));
+ DevelopersApi api = getLinstorAPI(destStoragePool);
ResourceDefinitionModify rdm = new ResourceDefinitionModify();
rdm.setDeleteProps(Collections.singletonList(LinstorUtil.LIN_PROP_DRBDOPT_EXACT_SIZE));
@@ -266,10 +310,10 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
_volumeService.expungeVolumeAsync(destVolumeInfo);
if (destroyFuture.get().isFailed()) {
- logger.debug("Failed to clean up dest volume on
storage");
+ logger.warn("Failed to clean up dest volume {} on
storage", destVolumeInfo);
}
} catch (Exception e) {
- logger.debug("Failed to clean up dest volume on storage",
e);
+ logger.warn("Failed to clean up dest volume {} on
storage", destVolumeInfo, e);
}
}
}
@@ -293,9 +337,7 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
private boolean needsExactSizeProp(VolumeInfo srcVolumeInfo) {
StoragePoolVO srcStoragePool =
_storagePool.findById(srcVolumeInfo.getDataStore().getId());
if (srcStoragePool.getPoolType() == Storage.StoragePoolType.Linstor) {
- DevelopersApi api =
LinstorUtil.getLinstorAPI(srcStoragePool.getHostAddress(),
-
LinstorConfigurationManager.ApiToken.valueIn(srcStoragePool.getId()),
-
Boolean.TRUE.equals(LinstorConfigurationManager.InsecureSsl.valueIn(srcStoragePool.getId())));
+ DevelopersApi api = getLinstorAPI(srcStoragePool);
String rscName = LinstorUtil.RSC_PREFIX + srcVolumeInfo.getPath();
try {
@@ -335,6 +377,7 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
Map<String, MigrateCommand.MigrateDiskInfo> migrateStorage = new
HashMap<>();
Map<VolumeInfo, VolumeInfo> srcVolumeInfoToDestVolumeInfo = new
HashMap<>();
+ boolean migrateCommandSent = false;
try {
for (Map.Entry<VolumeInfo, DataStore> entry :
volumeDataStoreMap.entrySet()) {
@@ -351,6 +394,8 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
VolumeVO destVolume = createNewVolumeVO(srcVolume,
destStoragePool);
VolumeInfo destVolumeInfo =
_volumeDataFactory.getVolume(destVolume.getId(), destDataStore);
+ // registered right away, so a failure from here on cleans the
new volume up again
+ srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo,
destVolumeInfo);
destVolumeInfo.processEvent(ObjectInDataStoreStateMachine.Event.MigrationCopyRequested);
destVolumeInfo.processEvent(ObjectInDataStoreStateMachine.Event.MigrationCopySucceeded);
@@ -358,13 +403,16 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
boolean exactSize = needsExactSizeProp(srcVolumeInfo);
- String devPath = LinstorUtil.createResource(
- destVolumeInfo, destStoragePool, _storagePoolDao,
exactSize);
+ LinstorUtil.createResource(destVolumeInfo, destStoragePool,
_storagePoolDao, exactSize);
_volumeDao.update(destVolume.getId(), destVolume);
destVolume = _volumeDao.findById(destVolume.getId());
destVolumeInfo =
_volumeDataFactory.getVolume(destVolume.getId(), destDataStore);
+ srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo,
destVolumeInfo);
+
+ String devPath = makeAvailableOnHost(
+ destStoragePool, LinstorUtil.RSC_PREFIX +
destVolumeInfo.getUuid(), destHost);
MigrateCommand.MigrateDiskInfo migrateDiskInfo = new
MigrateCommand.MigrateDiskInfo(
srcVolumeInfo.getPath(),
@@ -375,13 +423,12 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
migrateDiskInfoList.add(migrateDiskInfo);
migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo);
-
- srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo,
destVolumeInfo);
}
PrepareForMigrationCommand pfmc = new
PrepareForMigrationCommand(vmTO);
+ Answer pfma;
try {
- Answer pfma = _agentManager.send(destHost.getId(), pfmc);
+ pfma = _agentManager.send(destHost.getId(), pfmc);
if (pfma == null || !pfma.getResult()) {
String details = pfma != null ? pfma.getDetails() : "null
answer returned";
@@ -403,23 +450,41 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
migrateCommand.setWait(StorageManager.KvmStorageOnlineMigrationWait.value());
migrateCommand.setMigrateStorage(migrateStorage);
migrateCommand.setMigrateStorageManaged(true);
- migrateCommand.setNewVmCpuShares(
- vmTO.getCpus() *
ObjectUtils.defaultIfNull(vmTO.getMinSpeed(), vmTO.getSpeed()));
+ // the target host scales the shares to its cgroup version (v2
only accepts 1-10000),
+ // the raw cpus * speed value is only valid on cgroup v1
+ Integer newVmCpuShares = ((PrepareForMigrationAnswer)
pfma).getNewVmCpuShares();
+ if (newVmCpuShares != null) {
+ migrateCommand.setNewVmCpuShares(newVmCpuShares);
+ }
migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList);
boolean kvmAutoConvergence =
StorageManager.KvmAutoConvergence.value();
migrateCommand.setAutoConvergence(kvmAutoConvergence);
- MigrateAnswer migrateAnswer = (MigrateAnswer)
_agentManager.send(srcHost.getId(), migrateCommand);
- boolean success = migrateAnswer != null &&
migrateAnswer.getResult();
+ // once the migrate command is sent, the VM may end up running on
the new volumes,
+ // so they are only removed if the source host reports the
migration as failed
+ migrateCommandSent = true;
+ MigrateAnswer migrateAnswer = null;
+ boolean success;
+ try {
+ migrateAnswer = (MigrateAnswer)
_agentManager.send(srcHost.getId(), migrateCommand);
+ success = migrateAnswer != null && migrateAnswer.getResult();
+ } catch (OperationTimedoutException ex) {
+ // no answer from the source host, but the migration may still
have finished
+ if (!isVmRunningOnHost(vmTO, destHost)) {
+ throw ex;
+ }
+ logger.info("VM [{}] is running on the destination host [{}],
migration was successful", vmTO, destHost);
+ success = true;
+ }
handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO,
destHost);
- if (migrateAnswer == null) {
- throw new CloudRuntimeException("Unable to get an answer to
the migrate command");
- }
+ if (!success) {
+ if (migrateAnswer == null) {
+ throw new CloudRuntimeException("Unable to get an answer
to the migrate command");
+ }
- if (!migrateAnswer.getResult()) {
errMsg = migrateAnswer.getDetails();
throw new CloudRuntimeException(errMsg);
@@ -427,9 +492,18 @@ public class LinstorDataMotionStrategy implements
DataMotionStrategy {
} catch (AgentUnavailableException | OperationTimedoutException |
CloudRuntimeException ex) {
errMsg = String.format(
"Copy volume(s) of VM [%s] to storage(s) [%s] and VM to
host [%s] failed in LinstorDataMotionStrategy.copyAsync. Error message: [%s].",
- vmTO, srcHost, destHost, ex.getMessage());
+ vmTO, volumeDataStoreMap.values(), destHost,
ex.getMessage());
logger.error(errMsg, ex);
+ if (!migrateCommandSent &&
!srcVolumeInfoToDestVolumeInfo.isEmpty()) {
+ // failed before the migration was started: remove the already
created destination volumes
+ try {
+ handlePostMigration(false, srcVolumeInfoToDestVolumeInfo,
vmTO, destHost);
+ } catch (Exception e) {
+ logger.warn("Failed to clean up the destination volume(s)
of VM [{}]", vmTO, e);
+ }
+ }
+
throw new CloudRuntimeException(errMsg);
} finally {
CopyCmdAnswer copyCmdAnswer = new CopyCmdAnswer(errMsg);
diff --git
a/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategyTest.java
b/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategyTest.java
new file mode 100644
index 00000000000..9f7aeb8ea46
--- /dev/null
+++
b/plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/motion/LinstorDataMotionStrategyTest.java
@@ -0,0 +1,356 @@
+// 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.motion;
+
+import com.linbit.linstor.api.ApiException;
+import com.linbit.linstor.api.DevelopersApi;
+import com.linbit.linstor.api.model.ResourceMakeAvailable;
+
+import java.util.Collections;
+import java.util.Map;
+
+import com.cloud.agent.AgentManager;
+import com.cloud.agent.api.CheckVirtualMachineAnswer;
+import com.cloud.agent.api.CheckVirtualMachineCommand;
+import com.cloud.agent.api.Command;
+import com.cloud.agent.api.MigrateAnswer;
+import com.cloud.agent.api.MigrateCommand;
+import com.cloud.agent.api.PrepareForMigrationAnswer;
+import com.cloud.agent.api.PrepareForMigrationCommand;
+import com.cloud.agent.api.to.VirtualMachineTO;
+import com.cloud.exception.OperationTimedoutException;
+import com.cloud.host.Host;
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.GuestOSCategoryVO;
+import com.cloud.storage.GuestOSVO;
+import com.cloud.storage.Storage;
+import com.cloud.storage.Volume;
+import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.GuestOSCategoryDao;
+import com.cloud.storage.dao.GuestOSDao;
+import com.cloud.storage.dao.SnapshotDao;
+import com.cloud.storage.dao.VolumeDao;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.dao.VMInstanceDao;
+import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService;
+import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
+import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.datastore.util.LinstorUtil;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+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;
+
+@RunWith(MockitoJUnitRunner.Silent.class)
+public class LinstorDataMotionStrategyTest {
+ private static final long SRC_HOST_ID = 1L;
+ private static final long DEST_HOST_ID = 3L;
+ private static final String DEST_HOST_NAME = "kvm-dest";
+ private static final long SRC_VOLUME_ID = 10L;
+ private static final long DEST_VOLUME_ID = 20L;
+ private static final long SRC_POOL_ID = 7L;
+ private static final long DEST_POOL_ID = 8L;
+ private static final String DEST_VOLUME_UUID =
"8ed5dd1d-16f8-4169-8ed9-948178dbd44b";
+ private static final String DEST_RSC_NAME = "cs-" + DEST_VOLUME_UUID;
+ private static final String DEST_DEVICE_PATH = "/dev/drbd/by-res/" +
DEST_RSC_NAME + "/0";
+
+ @Mock
+ private PrimaryDataStoreDao _storagePool;
+ @Mock
+ private PrimaryDataStoreDao _storagePoolDao;
+ @Mock
+ private VolumeDao _volumeDao;
+ @Mock
+ private VolumeDataFactory _volumeDataFactory;
+ @Mock
+ private VMInstanceDao _vmDao;
+ @Mock
+ private GuestOSDao _guestOsDao;
+ @Mock
+ private VolumeService _volumeService;
+ @Mock
+ private GuestOSCategoryDao _guestOsCategoryDao;
+ @Mock
+ private SnapshotDao _snapshotDao;
+ @Mock
+ private AgentManager _agentManager;
+
+ @InjectMocks
+ private LinstorDataMotionStrategy strategy;
+
+ private MockedStatic<LinstorUtil> linstorUtil;
+ private DevelopersApi api;
+ private Host srcHost;
+ private Host destHost;
+ private VirtualMachineTO vmTO;
+ private VolumeInfo srcVolumeInfo;
+ private VolumeInfo destVolumeInfo;
+ private DataStore destDataStore;
+ private AsyncCompletionCallback<CopyCommandResult> callback;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setUp() throws Exception {
+ api = mock(DevelopersApi.class);
+ linstorUtil = Mockito.mockStatic(LinstorUtil.class);
+ linstorUtil.when(() -> LinstorUtil.getLinstorAPI(any(), any(),
anyBoolean())).thenReturn(api);
+ linstorUtil.when(() -> LinstorUtil.getDevicePath(api,
DEST_RSC_NAME)).thenReturn(DEST_DEVICE_PATH);
+
+ srcHost = mock(Host.class);
+ when(srcHost.getId()).thenReturn(SRC_HOST_ID);
+
when(srcHost.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
+ destHost = mock(Host.class);
+ when(destHost.getId()).thenReturn(DEST_HOST_ID);
+ when(destHost.getName()).thenReturn(DEST_HOST_NAME);
+ when(destHost.getPrivateIpAddress()).thenReturn("192.168.0.3");
+
+ // 2 vCPUs * 5001 MHz: raw shares 10002, more than cgroup v2 accepts
+ vmTO = mock(VirtualMachineTO.class);
+ when(vmTO.getId()).thenReturn(100L);
+ when(vmTO.getName()).thenReturn("i-2-100-VM");
+ when(vmTO.getCpus()).thenReturn(2);
+ when(vmTO.getSpeed()).thenReturn(5001);
+ when(vmTO.getMinSpeed()).thenReturn(5001);
+ VMInstanceVO vm = mock(VMInstanceVO.class);
+ when(vm.getState()).thenReturn(VirtualMachine.State.Migrating);
+ when(vm.getGuestOSId()).thenReturn(5L);
+ when(_vmDao.findById(100L)).thenReturn(vm);
+ GuestOSVO guestOs = mock(GuestOSVO.class);
+ when(guestOs.getCategoryId()).thenReturn(6L);
+ when(_guestOsDao.findById(5L)).thenReturn(guestOs);
+ GuestOSCategoryVO guestOsCategory = mock(GuestOSCategoryVO.class);
+ when(guestOsCategory.getName()).thenReturn("Other");
+ when(_guestOsCategoryDao.findById(6L)).thenReturn(guestOsCategory);
+
+ // source volume on an NFS pool
+ DataStore srcDataStore = mock(DataStore.class);
+ when(srcDataStore.getId()).thenReturn(SRC_POOL_ID);
+ srcVolumeInfo = mock(VolumeInfo.class);
+ when(srcVolumeInfo.getId()).thenReturn(SRC_VOLUME_ID);
+
when(srcVolumeInfo.getPath()).thenReturn("216fb792-c5ed-4ad7-98e6-ebdd25816e55");
+ when(srcVolumeInfo.getDataStore()).thenReturn(srcDataStore);
+ when(srcVolumeInfo.getPassphraseId()).thenReturn(null);
+ StoragePoolVO srcPool = mock(StoragePoolVO.class);
+
when(srcPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(_storagePool.findById(SRC_POOL_ID)).thenReturn(srcPool);
+ when(_volumeDao.findById(SRC_VOLUME_ID)).thenReturn(new
VolumeVO(Volume.Type.ROOT, "ROOT-100", 1L, 1L, 2L, 3L,
+ Storage.ProvisioningType.THIN, 1024L * 1024 * 1024, null,
null, null));
+
+ // destination volume on the Linstor pool
+ destDataStore = mock(DataStore.class);
+ when(destDataStore.getId()).thenReturn(DEST_POOL_ID);
+ StoragePoolVO destPool = mock(StoragePoolVO.class);
+ when(destPool.getId()).thenReturn(DEST_POOL_ID);
+
when(destPool.getHostAddress()).thenReturn("http://linstor-controller");
+ when(_storagePool.findById(DEST_POOL_ID)).thenReturn(destPool);
+ VolumeVO destVolume = mock(VolumeVO.class);
+ when(destVolume.getId()).thenReturn(DEST_VOLUME_ID);
+ when(_volumeDao.persist(any(VolumeVO.class))).thenReturn(destVolume);
+ when(_volumeDao.findById(DEST_VOLUME_ID)).thenReturn(destVolume);
+ destVolumeInfo = mock(VolumeInfo.class);
+ when(destVolumeInfo.getId()).thenReturn(DEST_VOLUME_ID);
+ when(destVolumeInfo.getUuid()).thenReturn(DEST_VOLUME_UUID);
+ when(destVolumeInfo.getDataStore()).thenReturn(destDataStore);
+ when(_volumeDataFactory.getVolume(eq(DEST_VOLUME_ID),
any(DataStore.class))).thenReturn(destVolumeInfo);
+
when(_volumeDataFactory.getVolume(DEST_VOLUME_ID)).thenReturn(destVolumeInfo);
+
when(_volumeDataFactory.getVolume(SRC_VOLUME_ID)).thenReturn(srcVolumeInfo);
+
+ callback = mock(AsyncCompletionCallback.class);
+ }
+
+ @After
+ public void tearDown() {
+ linstorUtil.close();
+ }
+
+ private Map<VolumeInfo, DataStore> volumeMap() {
+ return Collections.singletonMap(srcVolumeInfo, destDataStore);
+ }
+
+ private void prepareForMigrationAnswers(Integer cpuShares) throws
Exception {
+ when(_agentManager.send(eq(DEST_HOST_ID),
any(PrepareForMigrationCommand.class))).thenAnswer(inv -> {
+ PrepareForMigrationAnswer answer = new
PrepareForMigrationAnswer(inv.getArgument(1));
+ answer.setNewVmCpuShares(cpuShares);
+ return answer;
+ });
+ }
+
+ private void migrateSucceeds() throws Exception {
+ when(_agentManager.send(eq(SRC_HOST_ID),
any(MigrateCommand.class))).thenAnswer(
+ inv -> new MigrateAnswer(inv.getArgument(1), true, null,
null));
+ }
+
+ private MigrateCommand sentMigrateCommand() throws Exception {
+ ArgumentCaptor<Command> captor =
ArgumentCaptor.forClass(Command.class);
+ verify(_agentManager, Mockito.atLeastOnce()).send(anyLong(),
captor.capture());
+ return captor.getAllValues().stream()
+ .filter(MigrateCommand.class::isInstance)
+ .map(MigrateCommand.class::cast)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("no MigrateCommand
sent"));
+ }
+
+ private CopyCommandResult completedResult() {
+ ArgumentCaptor<CopyCommandResult> captor =
ArgumentCaptor.forClass(CopyCommandResult.class);
+ verify(callback).complete(captor.capture());
+ return captor.getValue();
+ }
+
+ @Test
+ public void
copyAsyncMakesNewResourceAvailableOnTargetHostBeforeMigrating() throws
Exception {
+ prepareForMigrationAnswers(8335);
+ migrateSucceeds();
+
+ strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost, callback);
+
+ InOrder order = inOrder(api, _agentManager);
+ order.verify(api).resourceMakeAvailableOnNode(eq(DEST_RSC_NAME),
eq(DEST_HOST_NAME), any(ResourceMakeAvailable.class));
+ order.verify(_agentManager).send(eq(DEST_HOST_ID),
any(PrepareForMigrationCommand.class));
+ order.verify(_agentManager).send(eq(SRC_HOST_ID),
any(MigrateCommand.class));
+
+ MigrateCommand migrateCommand = sentMigrateCommand();
+ Assert.assertEquals(1, migrateCommand.getMigrateDiskInfoList().size());
+ Assert.assertEquals(DEST_DEVICE_PATH,
migrateCommand.getMigrateDiskInfoList().get(0).getSourceText());
+ Assert.assertTrue(completedResult().isSuccess());
+ }
+
+ @Test
+ public void copyAsyncUsesCpuSharesCalculatedByTargetHost() throws
Exception {
+ prepareForMigrationAnswers(8335);
+ migrateSucceeds();
+
+ strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost, callback);
+
+ // not the raw cpus * speed (10002), which cgroup v2 hosts reject
+ Assert.assertEquals(8335, sentMigrateCommand().getNewVmCpuShares());
+ }
+
+ @Test
+ public void copyAsyncRemovesDestinationVolumeWhenCreateResourceFails()
throws Exception {
+ linstorUtil.when(() -> LinstorUtil.createResource(any(), any(), any(),
anyBoolean()))
+ .thenThrow(new CloudRuntimeException("Not enough available
nodes"));
+
+ Assert.assertThrows(CloudRuntimeException.class,
+ () -> strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost,
callback));
+
+ verify(api, never()).resourceMakeAvailableOnNode(any(), any(), any());
+ verify(_agentManager, never()).send(anyLong(),
any(MigrateCommand.class));
+ verify(_volumeService).destroyVolume(DEST_VOLUME_ID);
+ verify(_volumeService).expungeVolumeAsync(destVolumeInfo);
+ Assert.assertTrue(completedResult().isFailed());
+ }
+
+ @Test
+ public void copyAsyncRemovesDestinationVolumeWhenMakeAvailableFails()
throws Exception {
+ when(api.resourceMakeAvailableOnNode(eq(DEST_RSC_NAME),
eq(DEST_HOST_NAME), any(ResourceMakeAvailable.class)))
+ .thenThrow(new ApiException("Autoplacer could not find
diskless stor pool"));
+
+ Assert.assertThrows(CloudRuntimeException.class,
+ () -> strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost,
callback));
+
+ verify(_agentManager, never()).send(anyLong(),
any(MigrateCommand.class));
+ verify(_volumeService).destroyVolume(DEST_VOLUME_ID);
+ verify(_volumeService).expungeVolumeAsync(destVolumeInfo);
+ Assert.assertTrue(completedResult().isFailed());
+ }
+
+ @Test
+ public void
copyAsyncRemovesDestinationVolumeWhenPrepareForMigrationFails() throws
Exception {
+ when(_agentManager.send(eq(DEST_HOST_ID),
any(PrepareForMigrationCommand.class))).thenAnswer(
+ inv -> new PrepareForMigrationAnswer(inv.getArgument(1),
"failed to prepare"));
+
+ Assert.assertThrows(CloudRuntimeException.class,
+ () -> strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost,
callback));
+
+ verify(_agentManager, never()).send(anyLong(),
any(MigrateCommand.class));
+ verify(_volumeService).destroyVolume(DEST_VOLUME_ID);
+ verify(_volumeService).expungeVolumeAsync(destVolumeInfo);
+ Assert.assertTrue(completedResult().isFailed());
+ }
+
+ @Test
+ public void copyAsyncRemovesDestinationVolumeWhenMigrationFails() throws
Exception {
+ prepareForMigrationAnswers(8335);
+ when(_agentManager.send(eq(SRC_HOST_ID),
any(MigrateCommand.class))).thenAnswer(
+ inv -> new MigrateAnswer(inv.getArgument(1), false,
"blockdev-add failed", null));
+
+ Assert.assertThrows(CloudRuntimeException.class,
+ () -> strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost,
callback));
+
+ verify(_volumeService).destroyVolume(DEST_VOLUME_ID);
+ verify(_volumeService).expungeVolumeAsync(destVolumeInfo);
+ verify(_volumeService, never()).destroyVolume(SRC_VOLUME_ID);
+ Assert.assertTrue(completedResult().isFailed());
+ }
+
+ private void migrateTimesOutWithVmOnTarget(VirtualMachine.PowerState
powerState) throws Exception {
+ prepareForMigrationAnswers(8335);
+ when(_agentManager.send(eq(SRC_HOST_ID), any(MigrateCommand.class)))
+ .thenThrow(new OperationTimedoutException(null, SRC_HOST_ID,
1L, 86400, false));
+ when(_agentManager.send(eq(DEST_HOST_ID),
any(CheckVirtualMachineCommand.class))).thenAnswer(
+ inv -> new CheckVirtualMachineAnswer(inv.getArgument(1),
powerState, 5900));
+ }
+
+ @Test
+ public void copyAsyncSucceedsWhenMigrateTimesOutButVmRunsOnTargetHost()
throws Exception {
+ migrateTimesOutWithVmOnTarget(VirtualMachine.PowerState.PowerOn);
+
+ strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost, callback);
+
+ verify(_volumeService, never()).destroyVolume(DEST_VOLUME_ID);
+ verify(_volumeService).destroyVolume(SRC_VOLUME_ID);
+ Assert.assertTrue(completedResult().isSuccess());
+ }
+
+ @Test
+ public void copyAsyncKeepsDestinationVolumeWhenMigrateTimesOut() throws
Exception {
+ // a paused domain on the target: the incoming migration may still be
running
+ migrateTimesOutWithVmOnTarget(VirtualMachine.PowerState.PowerUnknown);
+
+ Assert.assertThrows(CloudRuntimeException.class,
+ () -> strategy.copyAsync(volumeMap(), vmTO, srcHost, destHost,
callback));
+
+ verify(_volumeService, never()).destroyVolume(DEST_VOLUME_ID);
+ verify(_volumeService, never()).destroyVolume(SRC_VOLUME_ID);
+ Assert.assertTrue(completedResult().isFailed());
+ }
+}