2424import com .linbit .linstor .api .model .ApiCallRcList ;
2525import com .linbit .linstor .api .model .ResourceDefinition ;
2626import com .linbit .linstor .api .model .ResourceDefinitionModify ;
27+ import com .linbit .linstor .api .model .ResourceMakeAvailable ;
2728
2829import javax .inject .Inject ;
2930
3536
3637import com .cloud .agent .AgentManager ;
3738import com .cloud .agent .api .Answer ;
39+ import com .cloud .agent .api .CheckVirtualMachineAnswer ;
40+ import com .cloud .agent .api .CheckVirtualMachineCommand ;
3841import com .cloud .agent .api .MigrateAnswer ;
3942import com .cloud .agent .api .MigrateCommand ;
43+ import com .cloud .agent .api .PrepareForMigrationAnswer ;
4044import com .cloud .agent .api .PrepareForMigrationCommand ;
4145import com .cloud .agent .api .to .DataObjectType ;
4246import com .cloud .agent .api .to .VirtualMachineTO ;
5458import com .cloud .storage .dao .VolumeDao ;
5559import com .cloud .utils .exception .CloudRuntimeException ;
5660import com .cloud .vm .VMInstanceVO ;
61+ import com .cloud .vm .VirtualMachine ;
5762import com .cloud .vm .dao .VMInstanceDao ;
5863import org .apache .cloudstack .engine .subsystem .api .storage .CopyCommandResult ;
5964import org .apache .cloudstack .engine .subsystem .api .storage .DataMotionStrategy ;
7479import org .apache .cloudstack .storage .datastore .util .LinstorUtil ;
7580import org .apache .commons .collections .CollectionUtils ;
7681import org .apache .commons .collections .MapUtils ;
77- import org .apache .commons .lang3 .ObjectUtils ;
7882import org .apache .logging .log4j .LogManager ;
7983import org .apache .logging .log4j .Logger ;
8084import 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