diff --git a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java index f977a1c3f53..e5623acb374 100644 --- a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java @@ -28,6 +28,7 @@ public class VirtualMachineTO { private long id; private String name; private BootloaderType bootloader; + private VirtualMachine.State state; Type type; int cpus; @@ -147,6 +148,14 @@ public class VirtualMachineTO { this.bootloader = bootloader; } + public VirtualMachine.State getState() { + return state; + } + + public void setState(VirtualMachine.State state) { + this.state = state; + } + public int getCpus() { return cpus; } diff --git a/api/src/main/java/com/cloud/vm/DiskProfile.java b/api/src/main/java/com/cloud/vm/DiskProfile.java index a1e5faa974a..2b76c682ba3 100644 --- a/api/src/main/java/com/cloud/vm/DiskProfile.java +++ b/api/src/main/java/com/cloud/vm/DiskProfile.java @@ -72,6 +72,7 @@ public class DiskProfile { offering.isCustomized(), null); this.hyperType = hyperType; + this.provisioningType = offering.getProvisioningType(); } public DiskProfile(DiskProfile dp) { diff --git a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java index 3e7dfc1b1be..e31287cd386 100644 --- a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java +++ b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java @@ -19,7 +19,9 @@ package com.cloud.agent.api; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.cloud.agent.api.to.VirtualMachineTO; @@ -33,6 +35,7 @@ public class MigrateCommand extends Command { private boolean isWindows; private VirtualMachineTO vmTO; private boolean executeInSequence = false; + private List migrateDiskInfoList = new ArrayList<>(); protected MigrateCommand() { } @@ -90,6 +93,14 @@ public class MigrateCommand extends Command { return executeInSequence; } + public List getMigrateDiskInfoList() { + return migrateDiskInfoList; + } + + public void setMigrateDiskInfoList(List migrateDiskInfoList) { + this.migrateDiskInfoList = migrateDiskInfoList; + } + public static class MigrateDiskInfo { public enum DiskType { FILE, BLOCK; @@ -123,6 +134,7 @@ public class MigrateCommand extends Command { private final DriverType driverType; private final Source source; private final String sourceText; + private boolean isSourceDiskOnStorageFileSystem; public MigrateDiskInfo(final String serialNumber, final DiskType diskType, final DriverType driverType, final Source source, final String sourceText) { this.serialNumber = serialNumber; @@ -151,5 +163,13 @@ public class MigrateCommand extends Command { public String getSourceText() { return sourceText; } + + public boolean isSourceDiskOnStorageFileSystem() { + return isSourceDiskOnStorageFileSystem; + } + + public void setSourceDiskOnStorageFileSystem(boolean isDiskOnFileSystemStorage) { + this.isSourceDiskOnStorageFileSystem = isDiskOnFileSystemStorage; + } } } diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java new file mode 100644 index 00000000000..bb75a666b0c --- /dev/null +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java @@ -0,0 +1,143 @@ +/* + * 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 java.io.File; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; +import com.cloud.agent.api.storage.CreateAnswer; +import com.cloud.agent.api.storage.CreateCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.VolumeVO; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.DiskProfile; + +/** + * Extends {@link StorageSystemDataMotionStrategy}, allowing KVM hosts to migrate VMs with the ROOT volume on a non managed local storage pool. + * As {@link StorageSystemDataMotionStrategy} is considering KVM, this implementation also migrates only from/to KVM hosts. + */ +public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMotionStrategy { + + @Inject + private TemplateDataFactory templateDataFactory; + + /** + * Uses the canHandle from the Super class {@link StorageSystemDataMotionStrategy}. If the storage pool is of file and the internalCanHandle from {@link StorageSystemDataMotionStrategy} CANT_HANDLE, returns the StrategyPriority.HYPERVISOR strategy priority. otherwise returns CANT_HANDLE. + * Note that the super implementation (override) is called by {@link #canHandle(Map, Host, Host)} which ensures that {@link #internalCanHandle(Map)} will be executed only if the source host is KVM. + */ + @Override + protected StrategyPriority internalCanHandle(Map volumeMap) { + if (super.internalCanHandle(volumeMap) == StrategyPriority.CANT_HANDLE) { + Set volumeInfoSet = volumeMap.keySet(); + + for (VolumeInfo volumeInfo : volumeInfoSet) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeInfo.getPoolId()); + if (storagePoolVO.getPoolType() != StoragePoolType.Filesystem && storagePoolVO.getPoolType() != StoragePoolType.NetworkFilesystem) { + return StrategyPriority.CANT_HANDLE; + } + } + return StrategyPriority.HYPERVISOR; + } + return StrategyPriority.CANT_HANDLE; + } + + /** + * Configures a {@link MigrateDiskInfo} object configured for migrating a File System volume and calls rootImageProvisioning. + */ + @Override + protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath) { + return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, destPath); + } + + /** + * Generates the volume path by appending the Volume UUID to the Libvirt destiny images path.
+ * Example: /var/lib/libvirt/images/f3d49ecc-870c-475a-89fa-fd0124420a9b + */ + @Override + protected String generateDestPath(VirtualMachineTO vmTO, VolumeVO srcVolume, Host destHost, StoragePoolVO destStoragePool, VolumeInfo destVolumeInfo) { + DiskOfferingVO diskOffering = _diskOfferingDao.findById(srcVolume.getDiskOfferingId()); + DiskProfile diskProfile = new DiskProfile(destVolumeInfo, diskOffering, HypervisorType.KVM); + String templateUuid = getTemplateUuid(destVolumeInfo.getTemplateId()); + CreateCommand rootImageProvisioningCommand = new CreateCommand(diskProfile, templateUuid, destStoragePool, true); + + Answer rootImageProvisioningAnswer = _agentMgr.easySend(destHost.getId(), rootImageProvisioningCommand); + + if (rootImageProvisioningAnswer == null) { + throw new CloudRuntimeException(String.format("Migration with storage of vm [%s] failed while provisioning root image", vmTO.getName())); + } + + if (!rootImageProvisioningAnswer.getResult()) { + throw new CloudRuntimeException(String.format("Unable to modify target volume on the host [host id:%s, name:%s]", destHost.getId(), destHost.getName())); + } + + String libvirtDestImgsPath = null; + if (rootImageProvisioningAnswer instanceof CreateAnswer) { + libvirtDestImgsPath = ((CreateAnswer)rootImageProvisioningAnswer).getVolume().getName(); + } + // File.getAbsolutePath is used to keep the file separator as it should be and eliminate a verification to check if exists a file separator in the last character of libvirtDestImgsPath. + return new File(libvirtDestImgsPath, destVolumeInfo.getUuid()).getAbsolutePath(); + } + + /** + * Returns the template UUID with the given id. If the template ID is null, it returns null. + */ + protected String getTemplateUuid(Long templateId) { + if (templateId == null) { + return null; + } + TemplateInfo templateImage = templateDataFactory.getTemplate(templateId, DataStoreRole.Image); + return templateImage.getUuid(); + } + + /** + * Sets the volume path as the volume UUID. + */ + @Override + protected void setVolumePath(VolumeVO volume) { + volume.setPath(volume.getUuid()); + } + + /** + * Return true if the volume should be migrated. Currently only supports migrating volumes on storage pool of the type StoragePoolType.Filesystem. + * This ensures that volumes on shared storage are not migrated and those on local storage pools are migrated. + */ + @Override + protected boolean shouldMigrateVolume(StoragePoolVO sourceStoragePool, Host destHost, StoragePoolVO destStoragePool) { + return sourceStoragePool.getPoolType() == StoragePoolType.Filesystem; + } +} diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java index 74ad8cb27c1..adeb0d17b64 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java @@ -24,6 +24,7 @@ import com.cloud.agent.api.storage.CopyVolumeAnswer; import com.cloud.agent.api.storage.CopyVolumeCommand; import com.cloud.agent.api.MigrateAnswer; import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; import com.cloud.agent.api.ModifyTargetsAnswer; import com.cloud.agent.api.ModifyTargetsCommand; import com.cloud.agent.api.PrepareForMigrationCommand; @@ -41,7 +42,6 @@ import com.cloud.exception.OperationTimedoutException; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; -import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.resource.ResourceState; import com.cloud.storage.DataStoreRole; @@ -49,6 +49,7 @@ import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateVO; @@ -136,16 +137,18 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { private static final int LOCK_TIME_IN_SECONDS = 300; private static final String OPERATION_NOT_SUPPORTED = "This operation is not supported."; - @Inject private AgentManager _agentMgr; + @Inject + protected AgentManager _agentMgr; @Inject private ConfigurationDao _configDao; @Inject private DataStoreManager dataStoreMgr; - @Inject private DiskOfferingDao _diskOfferingDao; + @Inject + protected DiskOfferingDao _diskOfferingDao; @Inject private GuestOSCategoryDao _guestOsCategoryDao; @Inject private GuestOSDao _guestOsDao; @Inject private ClusterDao clusterDao; @Inject private HostDao _hostDao; - @Inject private HostDetailsDao hostDetailsDao; - @Inject private PrimaryDataStoreDao _storagePoolDao; + @Inject + protected PrimaryDataStoreDao _storagePoolDao; @Inject private SnapshotDao _snapshotDao; @Inject private SnapshotDataStoreDao _snapshotDataStoreDao; @Inject private SnapshotDetailsDao _snapshotDetailsDao; @@ -251,29 +254,36 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } @Override - public StrategyPriority canHandle(Map volumeMap, Host srcHost, Host destHost) { + public final StrategyPriority canHandle(Map volumeMap, Host srcHost, Host destHost) { if (HypervisorType.KVM.equals(srcHost.getHypervisorType())) { - Set volumeInfoSet = volumeMap.keySet(); + return internalCanHandle(volumeMap); + } + return StrategyPriority.CANT_HANDLE; + } - for (VolumeInfo volumeInfo : volumeInfoSet) { - StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeInfo.getPoolId()); + /** + * Handles migrating volumes on managed Storage. + */ + protected StrategyPriority internalCanHandle(Map volumeMap) { + Set volumeInfoSet = volumeMap.keySet(); - if (storagePoolVO.isManaged()) { - return StrategyPriority.HIGHEST; - } - } + for (VolumeInfo volumeInfo : volumeInfoSet) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeInfo.getPoolId()); - Collection dataStores = volumeMap.values(); - - for (DataStore dataStore : dataStores) { - StoragePoolVO storagePoolVO = _storagePoolDao.findById(dataStore.getId()); - - if (storagePoolVO.isManaged()) { - return StrategyPriority.HIGHEST; - } + if (storagePoolVO.isManaged()) { + return StrategyPriority.HIGHEST; } } + Collection dataStores = volumeMap.values(); + + for (DataStore dataStore : dataStores) { + StoragePoolVO storagePoolVO = _storagePoolDao.findById(dataStore.getId()); + + if (storagePoolVO.isManaged()) { + return StrategyPriority.HIGHEST; + } + } return StrategyPriority.CANT_HANDLE; } @@ -1677,10 +1687,12 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { /** * For each disk to migrate: - * Create a volume on the target storage system. - * Make the newly created volume accessible to the target KVM host. - * Send a command to the target KVM host to connect to the newly created volume. - * Send a command to the source KVM host to migrate the VM and its storage. + *
    + *
  • Create a volume on the target storage system.
  • + *
  • Make the newly created volume accessible to the target KVM host.
  • + *
  • Send a command to the target KVM host to connect to the newly created volume.
  • + *
  • Send a command to the source KVM host to migrate the VM and its storage.
  • + *
*/ @Override public void copyAsync(Map volumeDataStoreMap, VirtualMachineTO vmTO, Host srcHost, Host destHost, AsyncCompletionCallback callback) { @@ -1693,6 +1705,10 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { verifyLiveMigrationMapForKVM(volumeDataStoreMap); + VMInstanceVO vmInstance = _vmDao.findById(vmTO.getId()); + vmTO.setState(vmInstance.getState()); + List migrateDiskInfoList = new ArrayList(); + Map migrateStorage = new HashMap<>(); Map srcVolumeInfoToDestVolumeInfo = new HashMap<>(); @@ -1702,6 +1718,11 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { VolumeVO srcVolume = _volumeDao.findById(srcVolumeInfo.getId()); StoragePoolVO destStoragePool = _storagePoolDao.findById(destDataStore.getId()); + StoragePoolVO sourceStoragePool = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + + if (!shouldMigrateVolume(sourceStoragePool, destHost, destStoragePool)) { + continue; + } VolumeVO destVolume = duplicateVolumeOnAnotherStorage(srcVolume, destStoragePool); VolumeInfo destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore); @@ -1718,7 +1739,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { destVolume = _volumeDao.findById(destVolume.getId()); - destVolume.setPath(destVolume.get_iScsiName()); + setVolumePath(destVolume); _volumeDao.update(destVolume.getId(), destVolume); @@ -1728,13 +1749,11 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { _volumeService.grantAccess(destVolumeInfo, destHost, destDataStore); - String connectedPath = connectHostToVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.get_iScsiName()); + String destPath = generateDestPath(vmTO, srcVolume, destHost, destStoragePool, destVolumeInfo); - MigrateCommand.MigrateDiskInfo migrateDiskInfo = new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), - MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, - MigrateCommand.MigrateDiskInfo.DriverType.RAW, - MigrateCommand.MigrateDiskInfo.Source.DEV, - connectedPath); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath); + migrateDiskInfo.setSourceDiskOnStorageFileSystem(isStoragePoolTypeOfFile(sourceStoragePool)); + migrateDiskInfoList.add(migrateDiskInfo); migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo); @@ -1765,6 +1784,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { migrateCommand.setWait(StorageManager.KvmStorageOnlineMigrationWait.value()); migrateCommand.setMigrateStorage(migrateStorage); + migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList); String autoConvergence = _configDao.getValue(Config.KvmAutoConvergence.toString()); boolean kvmAutoConvergence = Boolean.parseBoolean(autoConvergence); @@ -1803,6 +1823,42 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } } + /** + * Returns true. This method was implemented considering the classes that extend this {@link StorageSystemDataMotionStrategy} and cannot migrate volumes from certain types of source storage pools and/or to a different kind of destiny storage pool. + */ + protected boolean shouldMigrateVolume(StoragePoolVO sourceStoragePool, Host destHost, StoragePoolVO destStoragePool) { + return true; + } + + /** + * Returns true if the storage pool type is {@link StoragePoolType.Filesystem}. + */ + protected boolean isStoragePoolTypeOfFile(StoragePoolVO sourceStoragePool) { + return sourceStoragePool.getPoolType() == StoragePoolType.Filesystem; + } + + /** + * Returns the iScsi connection path. + */ + protected String generateDestPath(VirtualMachineTO vmTO, VolumeVO srcVolume, Host destHost, StoragePoolVO destStoragePool, VolumeInfo destVolumeInfo) { + return connectHostToVolume(destHost, destVolumeInfo.getPoolId(), destVolumeInfo.get_iScsiName()); + } + + /** + * Configures a {@link MigrateDiskInfo} object with disk type of BLOCK, Driver type RAW and Source DEV + */ + protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath) { + return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, MigrateCommand.MigrateDiskInfo.DriverType.RAW, + MigrateCommand.MigrateDiskInfo.Source.DEV, destPath); + } + + /** + * Sets the volume path as the iScsi name in case of a configured iScsi. + */ + protected void setVolumePath(VolumeVO volume) { + volume.setPath(volume.get_iScsiName()); + } + private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host destHost) { if (!success) { try { @@ -1913,7 +1969,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { return _volumeDao.persist(newVol); } - private String connectHostToVolume(Host host, long storagePoolId, String iqn) { + protected String connectHostToVolume(Host host, long storagePoolId, String iqn) { ModifyTargetsCommand modifyTargetsCommand = getModifyTargetsCommand(storagePoolId, iqn, true); return sendModifyTargetsCommand(modifyTargetsCommand, host.getId()).get(0); @@ -1990,10 +2046,6 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { if (destStoragePoolVO == null) { throw new CloudRuntimeException("Destination storage pool with ID " + dataStore.getId() + " was not located."); } - - if (!destStoragePoolVO.isManaged()) { - throw new CloudRuntimeException("Migrating a volume online with KVM can currently only be done when moving to managed storage."); - } } } diff --git a/engine/storage/datamotion/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-datamotion-storage-context.xml b/engine/storage/datamotion/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-datamotion-storage-context.xml index 1cefc517eed..52924197600 100644 --- a/engine/storage/datamotion/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-datamotion-storage-context.xml +++ b/engine/storage/datamotion/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-datamotion-storage-context.xml @@ -34,4 +34,6 @@ class="org.apache.cloudstack.storage.motion.HypervStorageMotionStrategy" /> + diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java new file mode 100644 index 00000000000..b344f83569a --- /dev/null +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java @@ -0,0 +1,256 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.PrimaryDataStoreImpl; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.storage.CreateAnswer; +import com.cloud.agent.api.storage.CreateCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.host.HostVO; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.DiskProfile; + +@RunWith(MockitoJUnitRunner.class) +public class KvmNonManagedStorageSystemDataMotionTest { + + @Mock + private PrimaryDataStoreDao primaryDataStoreDao; + + @Mock + private TemplateDataFactory templateDataFactory; + + @Mock + private AgentManager agentManager; + + @Mock + private DiskOfferingDao diskOfferingDao; + + @Spy + @InjectMocks + private KvmNonManagedStorageDataMotionStrategy kvmNonManagedStorageDataMotionStrategy; + + @Test + public void canHandleTestExpectHypervisorStrategyForKvm() { + canHandleExpectCannotHandle(HypervisorType.KVM, 1, StrategyPriority.HYPERVISOR); + } + + @Test + public void canHandleTestExpectCannotHandle() { + HypervisorType[] hypervisorTypeArray = HypervisorType.values(); + for (int i = 0; i < hypervisorTypeArray.length; i++) { + HypervisorType ht = hypervisorTypeArray[i]; + if (ht.equals(HypervisorType.KVM)) { + continue; + } + canHandleExpectCannotHandle(ht, 0, StrategyPriority.CANT_HANDLE); + } + } + + private void canHandleExpectCannotHandle(HypervisorType hypervisorType, int times, StrategyPriority expectedStrategyPriority) { + HostVO srcHost = new HostVO("sourceHostUuid"); + srcHost.setHypervisorType(hypervisorType); + Mockito.doReturn(StrategyPriority.HYPERVISOR).when(kvmNonManagedStorageDataMotionStrategy).internalCanHandle(new HashMap<>()); + + StrategyPriority strategyPriority = kvmNonManagedStorageDataMotionStrategy.canHandle(new HashMap<>(), srcHost, new HostVO("destHostUuid")); + + Mockito.verify(kvmNonManagedStorageDataMotionStrategy, Mockito.times(times)).internalCanHandle(new HashMap<>()); + Assert.assertEquals(expectedStrategyPriority, strategyPriority); + } + + @Test + public void internalCanHandleTestNonManaged() { + StoragePoolType[] storagePoolTypeArray = StoragePoolType.values(); + for (int i = 0; i < storagePoolTypeArray.length; i++) { + Map volumeMap = configureTestInternalCanHandle(false, storagePoolTypeArray[i]); + StrategyPriority strategyPriority = kvmNonManagedStorageDataMotionStrategy.internalCanHandle(volumeMap); + if (storagePoolTypeArray[i] == StoragePoolType.Filesystem || storagePoolTypeArray[i] == StoragePoolType.NetworkFilesystem) { + Assert.assertEquals(StrategyPriority.HYPERVISOR, strategyPriority); + } else { + Assert.assertEquals(StrategyPriority.CANT_HANDLE, strategyPriority); + } + } + } + + @Test + public void internalCanHandleTestIsManaged() { + StoragePoolType[] storagePoolTypeArray = StoragePoolType.values(); + for (int i = 0; i < storagePoolTypeArray.length; i++) { + Map volumeMap = configureTestInternalCanHandle(true, storagePoolTypeArray[i]); + StrategyPriority strategyPriority = kvmNonManagedStorageDataMotionStrategy.internalCanHandle(volumeMap); + Assert.assertEquals(StrategyPriority.CANT_HANDLE, strategyPriority); + } + } + + private Map configureTestInternalCanHandle(boolean isManagedStorage, StoragePoolType storagePoolType) { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0l).when(volumeInfo).getPoolId(); + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.doReturn(0l).when(ds).getId(); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO storagePool = Mockito.spy(new StoragePoolVO()); + Mockito.doReturn(storagePoolType).when(storagePool).getPoolType(); + + Mockito.doReturn(storagePool).when(primaryDataStoreDao).findById(0l); + Mockito.doReturn(isManagedStorage).when(storagePool).isManaged(); + return volumeMap; + } + + @Test + public void getTemplateUuidTestTemplateIdNotNull() { + String expectedTemplateUuid = prepareTestGetTemplateUuid(); + String templateUuid = kvmNonManagedStorageDataMotionStrategy.getTemplateUuid(0l); + Assert.assertEquals(expectedTemplateUuid, templateUuid); + } + + @Test + public void getTemplateUuidTestTemplateIdNull() { + prepareTestGetTemplateUuid(); + String templateUuid = kvmNonManagedStorageDataMotionStrategy.getTemplateUuid(null); + Assert.assertEquals(null, templateUuid); + } + + private String prepareTestGetTemplateUuid() { + TemplateInfo templateImage = Mockito.mock(TemplateInfo.class); + String expectedTemplateUuid = "template uuid"; + Mockito.when(templateImage.getUuid()).thenReturn(expectedTemplateUuid); + Mockito.doReturn(templateImage).when(templateDataFactory).getTemplate(0l, DataStoreRole.Image); + return expectedTemplateUuid; + } + + @Test + public void configureMigrateDiskInfoTest() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("volume path").when(srcVolumeInfo).getPath(); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = kvmNonManagedStorageDataMotionStrategy.configureMigrateDiskInfo(srcVolumeInfo, "destPath"); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.FILE, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.FILE, migrateDiskInfo.getSource()); + Assert.assertEquals("destPath", migrateDiskInfo.getSourceText()); + Assert.assertEquals("volume path", migrateDiskInfo.getSerialNumber()); + } + + @Test + public void generateDestPathTest() { + configureAndVerifygenerateDestPathTest(true, false); + } + + @Test(expected = CloudRuntimeException.class) + public void generateDestPathTestExpectCloudRuntimeException() { + configureAndVerifygenerateDestPathTest(false, false); + } + + @Test(expected = CloudRuntimeException.class) + public void generateDestPathTestExpectCloudRuntimeException2() { + configureAndVerifygenerateDestPathTest(false, true); + } + + private void configureAndVerifygenerateDestPathTest(boolean answerResult, boolean answerIsNull) { + String uuid = "f3d49ecc-870c-475a-89fa-fd0124420a9b"; + String destPath = "/var/lib/libvirt/images/"; + + VirtualMachineTO vmTO = Mockito.mock(VirtualMachineTO.class); + Mockito.when(vmTO.getName()).thenReturn("vmName"); + + VolumeVO srcVolume = Mockito.spy(new VolumeVO("name", 0l, 0l, 0l, 0l, 0l, "folder", "path", Storage.ProvisioningType.THIN, 0l, Volume.Type.ROOT)); + StoragePoolVO destStoragePool = Mockito.spy(new StoragePoolVO()); + + VolumeInfo destVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0l).when(destVolumeInfo).getTemplateId(); + Mockito.doReturn(0l).when(destVolumeInfo).getId(); + Mockito.doReturn(Volume.Type.ROOT).when(destVolumeInfo).getVolumeType(); + Mockito.doReturn("name").when(destVolumeInfo).getName(); + Mockito.doReturn(0l).when(destVolumeInfo).getSize(); + Mockito.doReturn(uuid).when(destVolumeInfo).getUuid(); + + DiskOfferingVO diskOffering = Mockito.spy(new DiskOfferingVO()); + Mockito.doReturn(0l).when(diskOffering).getId(); + Mockito.doReturn(diskOffering).when(diskOfferingDao).findById(0l); + DiskProfile diskProfile = Mockito.spy(new DiskProfile(destVolumeInfo, diskOffering, HypervisorType.KVM)); + + String templateUuid = Mockito.doReturn("templateUuid").when(kvmNonManagedStorageDataMotionStrategy).getTemplateUuid(0l); + CreateCommand rootImageProvisioningCommand = new CreateCommand(diskProfile, templateUuid, destStoragePool, true); + CreateAnswer createAnswer = Mockito.spy(new CreateAnswer(rootImageProvisioningCommand, "details")); + Mockito.doReturn(answerResult).when(createAnswer).getResult(); + + VolumeTO volumeTo = Mockito.mock(VolumeTO.class); + Mockito.doReturn(destPath).when(volumeTo).getName(); + Mockito.doReturn(volumeTo).when(createAnswer).getVolume(); + + if (answerIsNull) { + Mockito.doReturn(null).when(agentManager).easySend(0l, rootImageProvisioningCommand); + } else { + Mockito.doReturn(createAnswer).when(agentManager).easySend(0l, rootImageProvisioningCommand); + } + + String generatedDestPath = kvmNonManagedStorageDataMotionStrategy.generateDestPath(vmTO, srcVolume, new HostVO("sourceHostUuid"), destStoragePool, destVolumeInfo); + + Assert.assertEquals(destPath + uuid, generatedDestPath); + } + + @Test + public void shouldMigrateVolumeTest() { + StoragePoolVO sourceStoragePool = Mockito.spy(new StoragePoolVO()); + HostVO destHost = new HostVO("guid"); + StoragePoolVO destStoragePool = new StoragePoolVO(); + StoragePoolType[] storagePoolTypes = StoragePoolType.values(); + for (int i = 0; i < storagePoolTypes.length; i++) { + Mockito.doReturn(storagePoolTypes[i]).when(sourceStoragePool).getPoolType(); + boolean result = kvmNonManagedStorageDataMotionStrategy.shouldMigrateVolume(sourceStoragePool, destHost, destStoragePool); + if (storagePoolTypes[i] == StoragePoolType.Filesystem) { + Assert.assertTrue(result); + } else { + Assert.assertFalse(result); + } + } + } +} diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java index ec85f7d32fc..d76ff272830 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java @@ -18,67 +18,192 @@ */ package org.apache.cloudstack.storage.motion; -import com.cloud.storage.DataStoreRole; -import com.cloud.storage.ImageStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionStrategy; -import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; -import org.apache.cloudstack.storage.datastore.PrimaryDataStoreImpl; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.image.store.ImageStoreImpl; -import org.apache.cloudstack.storage.volume.VolumeObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.MockitoAnnotations.initMocks; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.PrimaryDataStoreImpl; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.image.store.ImageStoreImpl; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.agent.api.MigrateCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.HostVO; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.ImageStore; +import com.cloud.storage.Storage; +import com.cloud.storage.Volume; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.VolumeVO; + @RunWith(MockitoJUnitRunner.class) public class StorageSystemDataMotionStrategyTest { - @Mock - VolumeObject source; - @Mock - DataObject destination; - @Mock - PrimaryDataStore sourceStore; - @Mock - ImageStore destinationStore; - + @Spy @InjectMocks - DataMotionStrategy strategy = new StorageSystemDataMotionStrategy(); + private StorageSystemDataMotionStrategy storageSystemDataMotionStrategy; + @Mock - PrimaryDataStoreDao _storagePoolDao; + private VolumeObject volumeObjectSource; + @Mock + private DataObject dataObjectDestination; + @Mock + private PrimaryDataStore primaryDataStoreSourceStore; + @Mock + private ImageStore destinationStore; + @Mock + private PrimaryDataStoreDao primaryDataStoreDao; - @Before public void setUp() throws Exception { - sourceStore = mock(PrimaryDataStoreImpl.class); + @Before + public void setUp() throws Exception { + primaryDataStoreSourceStore = mock(PrimaryDataStoreImpl.class); destinationStore = mock(ImageStoreImpl.class); - source = mock(VolumeObject.class); - destination = mock(VolumeObject.class); + volumeObjectSource = mock(VolumeObject.class); + dataObjectDestination = mock(VolumeObject.class); - initMocks(strategy); + initMocks(storageSystemDataMotionStrategy); } @Test public void cantHandleSecondary() { - doReturn(sourceStore).when(source).getDataStore(); - doReturn(DataStoreRole.Primary).when(sourceStore).getRole(); - doReturn(destinationStore).when(destination).getDataStore(); + doReturn(primaryDataStoreSourceStore).when(volumeObjectSource).getDataStore(); + doReturn(DataStoreRole.Primary).when(primaryDataStoreSourceStore).getRole(); + doReturn(destinationStore).when(dataObjectDestination).getDataStore(); doReturn(DataStoreRole.Image).when((DataStore)destinationStore).getRole(); - doReturn(sourceStore).when(source).getDataStore(); - doReturn(destinationStore).when(destination).getDataStore(); + doReturn(primaryDataStoreSourceStore).when(volumeObjectSource).getDataStore(); + doReturn(destinationStore).when(dataObjectDestination).getDataStore(); StoragePoolVO storeVO = new StoragePoolVO(); - doReturn(storeVO).when(_storagePoolDao).findById(0l); + doReturn(storeVO).when(primaryDataStoreDao).findById(0l); - assertTrue(strategy.canHandle(source,destination) == StrategyPriority.CANT_HANDLE); + assertTrue(storageSystemDataMotionStrategy.canHandle(volumeObjectSource, dataObjectDestination) == StrategyPriority.CANT_HANDLE); } -} \ No newline at end of file + + @Test + public void internalCanHandleTestAllStoragePoolsAreManaged() { + configureAndTestInternalCanHandle(true, true, StrategyPriority.HIGHEST); + } + + @Test + public void internalCanHandleTestFirstStoragePoolsIsManaged() { + configureAndTestInternalCanHandle(false, true, StrategyPriority.HIGHEST); + } + + @Test + public void internalCanHandleTestSecondStoragePoolsIsManaged() { + configureAndTestInternalCanHandle(true, false, StrategyPriority.HIGHEST); + } + + @Test + public void internalCanHandleTestNoStoragePoolsIsManaged() { + configureAndTestInternalCanHandle(false, false, StrategyPriority.CANT_HANDLE); + } + + private void configureAndTestInternalCanHandle(boolean sPool0IsManaged, boolean sPool1IsManaged, StrategyPriority expectedStrategyPriority) { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0l).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + Mockito.doReturn(1l).when(ds).getId(); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO storagePool0 = Mockito.spy(new StoragePoolVO()); + Mockito.doReturn(sPool0IsManaged).when(storagePool0).isManaged(); + StoragePoolVO storagePool1 = Mockito.spy(new StoragePoolVO()); + Mockito.doReturn(sPool1IsManaged).when(storagePool1).isManaged(); + + Mockito.doReturn(storagePool0).when(primaryDataStoreDao).findById(0l); + Mockito.doReturn(storagePool1).when(primaryDataStoreDao).findById(1l); + + StrategyPriority strategyPriority = storageSystemDataMotionStrategy.internalCanHandle(volumeMap); + + Assert.assertEquals(expectedStrategyPriority, strategyPriority); + } + + @Test + public void isStoragePoolTypeOfFileTest() { + StoragePoolVO sourceStoragePool = Mockito.spy(new StoragePoolVO()); + StoragePoolType[] storagePoolTypeArray = StoragePoolType.values(); + for (int i = 0; i < storagePoolTypeArray.length; i++) { + Mockito.doReturn(storagePoolTypeArray[i]).when(sourceStoragePool).getPoolType(); + boolean result = storageSystemDataMotionStrategy.isStoragePoolTypeOfFile(sourceStoragePool); + if (sourceStoragePool.getPoolType() == StoragePoolType.Filesystem) { + Assert.assertTrue(result); + } else { + Assert.assertFalse(result); + } + } + } + + @Test + public void generateDestPathTest() { + VolumeObject destVolumeInfo = Mockito.spy(new VolumeObject()); + HostVO destHost = new HostVO("guid"); + Mockito.doReturn("iScsiName").when(destVolumeInfo).get_iScsiName(); + Mockito.doReturn(0l).when(destVolumeInfo).getPoolId(); + Mockito.doReturn("expected").when(storageSystemDataMotionStrategy).connectHostToVolume(destHost, 0l, "iScsiName"); + + String expected = storageSystemDataMotionStrategy.generateDestPath(Mockito.mock(VirtualMachineTO.class), Mockito.mock(VolumeVO.class), destHost, + Mockito.mock(StoragePoolVO.class), destVolumeInfo); + + Assert.assertEquals(expected, "expected"); + Mockito.verify(storageSystemDataMotionStrategy).connectHostToVolume(destHost, 0l, "iScsiName"); + } + + @Test + public void configureMigrateDiskInfoTest() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("volume path").when(srcVolumeInfo).getPath(); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = storageSystemDataMotionStrategy.configureMigrateDiskInfo(srcVolumeInfo, "destPath"); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("destPath", migrateDiskInfo.getSourceText()); + Assert.assertEquals("volume path", migrateDiskInfo.getSerialNumber()); + } + + @Test + public void setVolumePathTest() { + VolumeVO volume = new VolumeVO("name", 0l, 0l, 0l, 0l, 0l, "folder", "path", Storage.ProvisioningType.THIN, 0l, Volume.Type.ROOT); + String volumePath = "iScsiName"; + volume.set_iScsiName(volumePath); + + storageSystemDataMotionStrategy.setVolumePath(volume); + + Assert.assertEquals(volumePath, volume.getPath()); + } + + @Test + public void shouldMigrateVolumeTest() { + StoragePoolVO sourceStoragePool = Mockito.spy(new StoragePoolVO()); + HostVO destHost = new HostVO("guid"); + StoragePoolVO destStoragePool = new StoragePoolVO(); + StoragePoolType[] storagePoolTypes = StoragePoolType.values(); + for (int i = 0; i < storagePoolTypes.length; i++) { + Mockito.doReturn(storagePoolTypes[i]).when(sourceStoragePool).getPoolType(); + boolean result = storageSystemDataMotionStrategy.shouldMigrateVolume(sourceStoragePool, destHost, destStoragePool); + Assert.assertTrue(result); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/MigrateKVMAsync.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/MigrateKVMAsync.java index 4b2afa6a59b..51dbd9234ac 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/MigrateKVMAsync.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/MigrateKVMAsync.java @@ -36,6 +36,36 @@ public class MigrateKVMAsync implements Callable { private boolean migrateStorage; private boolean autoConvergence; + /** + * Do not pause the domain during migration. The domain's memory will be transferred to the destination host while the domain is running. The migration may never converge if the domain is changing its memory faster then it can be transferred. The domain can be manually paused anytime during migration using virDomainSuspend. + * @value 1 + * @see Libvirt virDomainMigrateFlags documentation + */ + private static final long VIR_MIGRATE_LIVE = 1L; + /** + * Migrate full disk images in addition to domain's memory. By default only non-shared non-readonly disk images are transferred. The VIR_MIGRATE_PARAM_MIGRATE_DISKS parameter can be used to specify which disks should be migrated. This flag and VIR_MIGRATE_NON_SHARED_INC are mutually exclusive. + * @value 64 + * @see Libvirt virDomainMigrateFlags documentation + */ + private static final long VIR_MIGRATE_NON_SHARED_DISK = 64L; + /** + * Compress migration data. The compression methods can be specified using VIR_MIGRATE_PARAM_COMPRESSION. A hypervisor default method will be used if this parameter is omitted. Individual compression methods can be tuned via their specific VIR_MIGRATE_PARAM_COMPRESSION_* parameters. + * @value 2048 + * @see Libvirt virDomainMigrateFlags documentation + */ + private static final long VIR_MIGRATE_COMPRESSED = 2048L; + /** + * Enable algorithms that ensure a live migration will eventually converge. This usually means the domain will be slowed down to make sure it does not change its memory faster than a hypervisor can transfer the changed memory to the destination host. VIR_MIGRATE_PARAM_AUTO_CONVERGE_* parameters can be used to tune the algorithm. + * @value 8192 + * @see Libvirt virDomainMigrateFlags documentation + */ + private static final long VIR_MIGRATE_AUTO_CONVERGE = 8192L; + + /** + * Libvirt 1.0.3 supports compression flag for migration. + */ + private static final int LIBVIRT_VERSION_SUPPORTS_MIGRATE_COMPRESSED = 1000003; + public MigrateKVMAsync(final LibvirtComputingResource libvirtComputingResource, final Domain dm, final Connect dconn, final String dxml, final boolean migrateStorage, final boolean autoConvergence, final String vmName, final String destIp) { this.libvirtComputingResource = libvirtComputingResource; @@ -51,19 +81,18 @@ public class MigrateKVMAsync implements Callable { @Override public Domain call() throws LibvirtException { - long flags = 1 << 0; + long flags = VIR_MIGRATE_LIVE; - // set compression flag for migration, if libvirt version supports it - if (dconn.getLibVirVersion() >= 1000003) { - flags |= 1 << 11; + if (dconn.getLibVirVersion() >= LIBVIRT_VERSION_SUPPORTS_MIGRATE_COMPRESSED) { + flags += VIR_MIGRATE_COMPRESSED; } if (migrateStorage) { - flags |= 1 << 6; + flags += VIR_MIGRATE_NON_SHARED_DISK; } if (autoConvergence && dconn.getLibVirVersion() >= 1002003) { - flags |= 1 << 13; + flags += VIR_MIGRATE_AUTO_CONVERGE; } return dm.migrate(dconn, flags, dxml, vmName, "tcp:" + destIp, libvirtComputingResource.getMigrateSpeed()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateCommandWrapper.java index 1796dc50f31..bfa557308e7 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateCommandWrapper.java @@ -59,7 +59,7 @@ public final class LibvirtCreateCommandWrapper extends CommandWrapper migrateDiskInfoList = command.getMigrateDiskInfoList(); String result = null; @@ -203,9 +207,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper + * This method must be executed after a successful migration to a target storage pool, cleaning up the source storage. + */ + protected void deleteOrDisconnectDisksOnSourcePool(final LibvirtComputingResource libvirtComputingResource, final List migrateDiskInfoList, + List disks) { + for (DiskDef disk : disks) { + MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); + if (migrateDiskInfo != null && migrateDiskInfo.isSourceDiskOnStorageFileSystem()) { + deleteLocalVolume(disk.getDiskPath()); + } else { + libvirtComputingResource.cleanupDisk(disk); + } + } + } + + /** + * Deletes the local volume from the storage pool. + */ + protected void deleteLocalVolume(String localPath) { + try { + Connect conn = LibvirtConnection.getConnection(); + StorageVol storageVolLookupByPath = conn.storageVolLookupByPath(localPath); + storageVolLookupByPath.delete(0); + } catch (LibvirtException e) { + s_logger.error(String.format("Cannot delete local volume [%s] due to: %s", localPath, e)); + } + } + + /** + * Searches for a {@link MigrateDiskInfo} with the path matching the {@link DiskDef} path. + */ + protected MigrateDiskInfo searchDiskDefOnMigrateDiskInfoList(List migrateDiskInfoList, DiskDef disk) { + for (MigrateDiskInfo migrateDiskInfo : migrateDiskInfoList) { + if (StringUtils.contains(disk.getDiskPath(), migrateDiskInfo.getSerialNumber())) { + return migrateDiskInfo; + } + } + s_logger.debug(String.format("Cannot find Disk [uuid: %s] on the list of disks to be migrated", disk.getDiskPath())); + return null; + } + /** * This function assumes an qemu machine description containing a single graphics element like * @@ -338,7 +382,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper disks = Arrays.asList(vmSpec.getDisks()); for (DiskTO disk : disks) { - if (disk.getType() != Volume.Type.ISO) { - VolumeObjectTO vol = (VolumeObjectTO)disk.getData(); - PrimaryDataStoreTO store = (PrimaryDataStoreTO)vol.getDataStore(); - KVMStoragePool pool = getStoragePool(store.getPoolType(), store.getUuid()); + if (disk.getType() == Volume.Type.ISO) { + result = true; + continue; + } - StorageAdaptor adaptor = getStorageAdaptor(pool.getType()); + VolumeObjectTO vol = (VolumeObjectTO)disk.getData(); + PrimaryDataStoreTO store = (PrimaryDataStoreTO)vol.getDataStore(); + if (!store.isManaged() && VirtualMachine.State.Migrating.equals(vmSpec.getState())) { + result = true; + continue; + } - result = adaptor.connectPhysicalDisk(vol.getPath(), pool, disk.getDetails()); + KVMStoragePool pool = getStoragePool(store.getPoolType(), store.getUuid()); + StorageAdaptor adaptor = getStorageAdaptor(pool.getType()); - if (!result) { - s_logger.error("Failed to connect disks via vm spec for vm: " + vmName + " volume:" + vol.toString()); + result = adaptor.connectPhysicalDisk(vol.getPath(), pool, disk.getDetails()); - return result; - } + if (!result) { + s_logger.error("Failed to connect disks via vm spec for vm: " + vmName + " volume:" + vol.toString()); + return result; } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index c2396e81c17..69954d0c805 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -1765,8 +1765,7 @@ public class LibvirtComputingResourceTest { when(poolManager.getStoragePool(pool.getType(), pool.getUuid())).thenReturn(primary); when(primary.getPhysicalDisk(command.getTemplateUrl())).thenReturn(baseVol); - when(poolManager.createDiskFromTemplate(baseVol, - diskCharacteristics.getPath(), diskCharacteristics.getProvisioningType(), primary, 0)).thenReturn(vol); + when(poolManager.createDiskFromTemplate(baseVol, diskCharacteristics.getPath(), diskCharacteristics.getProvisioningType(), primary, baseVol.getSize(), 0)).thenReturn(vol); final LibvirtRequestWrapper wrapper = LibvirtRequestWrapper.getInstance(); assertNotNull(wrapper); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java index da71e40c30f..f703a4a324f 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapperTest.java @@ -21,11 +21,31 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import org.junit.Test; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libvirt.Connect; +import org.libvirt.StorageVol; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo.DiskType; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo.DriverType; +import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo.Source; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; import com.cloud.utils.exception.CloudRuntimeException; +@RunWith(PowerMockRunner.class) +@PrepareForTest({LibvirtConnection.class, LibvirtMigrateCommandWrapper.class}) public class LibvirtMigrateCommandWrapperTest { String fullfile = "\n" + @@ -252,11 +272,12 @@ public class LibvirtMigrateCommandWrapperTest { " \n" + ""; + LibvirtMigrateCommandWrapper libvirtMigrateCmdWrapper = new LibvirtMigrateCommandWrapper(); + @Test public void testReplaceIpForVNCInDescFile() { final String targetIp = "192.168.22.21"; - final LibvirtMigrateCommandWrapper lw = new LibvirtMigrateCommandWrapper(); - final String result = lw.replaceIpForVNCInDescFile(fullfile, targetIp); + final String result = libvirtMigrateCmdWrapper.replaceIpForVNCInDescFile(fullfile, targetIp); assertTrue("transformation does not live up to expectation:\n" + result, targetfile.equals(result)); } @@ -279,8 +300,7 @@ public class LibvirtMigrateCommandWrapperTest { " " + ""; final String targetIp = "10.10.10.10"; - final LibvirtMigrateCommandWrapper lw = new LibvirtMigrateCommandWrapper(); - final String result = lw.replaceIpForVNCInDescFile(xmlDesc, targetIp); + final String result = libvirtMigrateCmdWrapper.replaceIpForVNCInDescFile(xmlDesc, targetIp); assertTrue("transformation does not live up to expectation:\n" + result, expectedXmlDesc.equals(result)); } @@ -303,26 +323,116 @@ public class LibvirtMigrateCommandWrapperTest { " " + ""; final String targetIp = "localhost.localdomain"; - final LibvirtMigrateCommandWrapper lw = new LibvirtMigrateCommandWrapper(); - final String result = lw.replaceIpForVNCInDescFile(xmlDesc, targetIp); + final String result = libvirtMigrateCmdWrapper.replaceIpForVNCInDescFile(xmlDesc, targetIp); assertTrue("transformation does not live up to expectation:\n" + result, expectedXmlDesc.equals(result)); } @Test public void testMigrationUri() { final String ip = "10.1.1.1"; - LibvirtMigrateCommandWrapper lw = new LibvirtMigrateCommandWrapper(); LibvirtComputingResource lcr = new LibvirtComputingResource(); if (lcr.isHostSecured()) { - assertEquals(lw.createMigrationURI(ip, lcr), String.format("qemu+tls://%s/system", ip)); + assertEquals(libvirtMigrateCmdWrapper.createMigrationURI(ip, lcr), String.format("qemu+tls://%s/system", ip)); } else { - assertEquals(lw.createMigrationURI(ip, lcr), String.format("qemu+tcp://%s/system", ip)); + assertEquals(libvirtMigrateCmdWrapper.createMigrationURI(ip, lcr), String.format("qemu+tcp://%s/system", ip)); } } @Test(expected = CloudRuntimeException.class) public void testMigrationUriException() { - LibvirtMigrateCommandWrapper lw = new LibvirtMigrateCommandWrapper(); - lw.createMigrationURI(null, new LibvirtComputingResource()); + libvirtMigrateCmdWrapper.createMigrationURI(null, new LibvirtComputingResource()); } + + @Test + public void deleteLocalVolumeTest() throws Exception { + PowerMockito.mockStatic(LibvirtConnection.class); + Connect conn = Mockito.mock(Connect.class); + + PowerMockito.doReturn(conn).when(LibvirtConnection.class, "getConnection"); + + StorageVol storageVolLookupByPath = Mockito.mock(StorageVol.class); + Mockito.when(conn.storageVolLookupByPath("localPath")).thenReturn(storageVolLookupByPath); + + libvirtMigrateCmdWrapper.deleteLocalVolume("localPath"); + + PowerMockito.verifyStatic(Mockito.times(1)); + LibvirtConnection.getConnection(); + InOrder inOrder = Mockito.inOrder(conn, storageVolLookupByPath); + inOrder.verify(conn, Mockito.times(1)).storageVolLookupByPath("localPath"); + inOrder.verify(storageVolLookupByPath, Mockito.times(1)).delete(0); + } + + @Test + public void searchDiskDefOnMigrateDiskInfoListTest() { + configureAndVerifyTestSearchDiskDefOnMigrateDiskInfoList("f3d49ecc-870c-475a-89fa-fd0124420a9b", "/var/lib/libvirt/images/f3d49ecc-870c-475a-89fa-fd0124420a9b", false); + } + + @Test + public void searchDiskDefOnMigrateDiskInfoListTestExpectNull() { + configureAndVerifyTestSearchDiskDefOnMigrateDiskInfoList("f3d49ecc-870c-475a-89fa-fd0124420a9b", "/var/lib/libvirt/images/f3d49ecc-870c-89fa-fd0124420a9b", true); + } + + private void configureAndVerifyTestSearchDiskDefOnMigrateDiskInfoList(String serialNumber, String diskPath, boolean isExpectedDiskInfoNull) { + MigrateDiskInfo migrateDiskInfo = new MigrateDiskInfo(serialNumber, DiskType.FILE, DriverType.QCOW2, Source.FILE, "sourceText"); + List migrateDiskInfoList = new ArrayList<>(); + migrateDiskInfoList.add(migrateDiskInfo); + + DiskDef disk = new DiskDef(); + disk.setDiskPath(diskPath); + + MigrateDiskInfo returnedMigrateDiskInfo = libvirtMigrateCmdWrapper.searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); + + if (isExpectedDiskInfoNull) + Assert.assertEquals(null, returnedMigrateDiskInfo); + else + Assert.assertEquals(migrateDiskInfo, returnedMigrateDiskInfo); + } + + @Test + public void deleteOrDisconnectDisksOnSourcePoolTest() { + LibvirtMigrateCommandWrapper spyLibvirtMigrateCmdWrapper = PowerMockito.spy(libvirtMigrateCmdWrapper); + Mockito.doNothing().when(spyLibvirtMigrateCmdWrapper).deleteLocalVolume("volPath"); + + List migrateDiskInfoList = new ArrayList<>(); + MigrateDiskInfo migrateDiskInfo0 = createMigrateDiskInfo(true); + MigrateDiskInfo migrateDiskInfo2 = createMigrateDiskInfo(false); + + List disks = new ArrayList<>(); + DiskDef diskDef0 = new DiskDef(); + DiskDef diskDef1 = new DiskDef(); + DiskDef diskDef2 = new DiskDef(); + + diskDef0.setDiskPath("volPath"); + disks.add(diskDef0); + disks.add(diskDef1); + disks.add(diskDef2); + + LibvirtComputingResource libvirtComputingResource = Mockito.spy(new LibvirtComputingResource()); + Mockito.doReturn(true).when(libvirtComputingResource).cleanupDisk(diskDef1); + + Mockito.doReturn(migrateDiskInfo0).when(spyLibvirtMigrateCmdWrapper).searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, diskDef0); + Mockito.doReturn(null).when(spyLibvirtMigrateCmdWrapper).searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, diskDef1); + Mockito.doReturn(migrateDiskInfo2).when(spyLibvirtMigrateCmdWrapper).searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, diskDef2); + + spyLibvirtMigrateCmdWrapper.deleteOrDisconnectDisksOnSourcePool(libvirtComputingResource, migrateDiskInfoList, disks); + + InOrder inOrder = Mockito.inOrder(spyLibvirtMigrateCmdWrapper, libvirtComputingResource); + inOrderVerifyDeleteOrDisconnect(inOrder, spyLibvirtMigrateCmdWrapper, libvirtComputingResource, migrateDiskInfoList, diskDef0, 1, 0); + inOrderVerifyDeleteOrDisconnect(inOrder, spyLibvirtMigrateCmdWrapper, libvirtComputingResource, migrateDiskInfoList, diskDef1, 0, 1); + inOrderVerifyDeleteOrDisconnect(inOrder, spyLibvirtMigrateCmdWrapper, libvirtComputingResource, migrateDiskInfoList, diskDef2, 0, 1); + } + + private MigrateDiskInfo createMigrateDiskInfo(boolean isSourceDiskOnStorageFileSystem) { + MigrateDiskInfo migrateDiskInfo = new MigrateDiskInfo("serialNumber", DiskType.FILE, DriverType.QCOW2, Source.FILE, "sourceText"); + migrateDiskInfo.setSourceDiskOnStorageFileSystem(isSourceDiskOnStorageFileSystem); + return migrateDiskInfo; + } + + private void inOrderVerifyDeleteOrDisconnect(InOrder inOrder, LibvirtMigrateCommandWrapper lw, LibvirtComputingResource virtResource, List diskInfoList, + DiskDef disk, int timesDelete, int timesCleanup) { + inOrder.verify(lw).searchDiskDefOnMigrateDiskInfoList(diskInfoList, disk); + inOrder.verify(lw, Mockito.times(timesDelete)).deleteLocalVolume("volPath"); + inOrder.verify(virtResource, Mockito.times(timesCleanup)).cleanupDisk(disk); + } + } diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java index 11363054378..96a03b7b9ca 100644 --- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java @@ -206,7 +206,9 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri result.setResult(errMsg); } - callback.complete(result); + if (callback != null) { + callback.complete(result); + } } @Override diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index 9f67d34b523..52c5a7218ef 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -188,6 +188,7 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis to.setConfigDriveLabel(vmProfile.getConfigDriveLabel()); to.setConfigDriveIsoRootFolder(vmProfile.getConfigDriveIsoRootFolder()); to.setConfigDriveIsoFile(vmProfile.getConfigDriveIsoFile()); + to.setState(vm.getState()); return to; }