From 05b75e9d57c216c19c881ce5ceaed23c4a7e9301 Mon Sep 17 00:00:00 2001 From: Sateesh Chodapuneedi Date: Sat, 18 May 2013 00:32:07 +0530 Subject: [PATCH] CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to deal with storage motion tasks. Added target host parameter to MigrateWithStorageCommand. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to application context. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores Unit tests for vmware storage motion. These test the VmwareStorageMotionStrategy. CLOUDSTACK-659 Fixing migrate volume. CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores Added vm type to prepareNetworkFromNicInfo in MigrateWithStorageCommand implementation. CLOUDSTACK-2701 - Enable storage migration for VMware resources Fixing attach volume and delete volume cases for volumes that are moved off original path in datastore when created. If volume is not found in root directory or datastore, do search in sub folders. CLOUDSTACK-2701 - Enable storage migration for VMware resources Sending command MigrateWithStorageCommand to source host instead of target host for the case of migration of VM within cluster. CLOUDSTACK-2701 - Enable storage migration for VMware resources Searching for virtual disk during device tear down. Adding dependency of 'cloud-engine-storage' to vmware hypervisor plugin. Add hypervisor capability storage_motion_supported for VMware 5.0 and 5.1 CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to deal with storage motion tasks. Added target host parameter to MigrateWithStorageCommand. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to application context. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores Unit tests for vmware storage motion. These test the VmwareStorageMotionStrategy. CLOUDSTACK-659 Fixing migrate volume. CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores Added vm type to prepareNetworkFromNicInfo in MigrateWithStorageCommand implementation. Adding dependency of 'cloud-engine-storage' to vmware hypervisor plugin. Add hypervisor capability storage_motion_supported for VMware 5.0 and 5.1 CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to deal with storage motion tasks. Added target host parameter to MigrateWithStorageCommand. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to application context. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores Unit tests for vmware storage motion. These test the VmwareStorageMotionStrategy. CLOUDSTACK-659 Fixing migrate volume. Adding dependency of 'cloud-engine-storage' to vmware hypervisor plugin. Add hypervisor capability storage_motion_supported for VMware 5.0 and 5.1 Adding dependency of 'cloud-engine-storage' to vmware hypervisor plugin. CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to deal with storage motion tasks. Added target host parameter to MigrateWithStorageCommand. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added Resource changes to perform VM live migration along with virtual disks across the clusters in a zone. Signed-off-by: Sateesh Chodapuneedi CLOUDSTACK-659 Support for storage migration in Cloudstack deployment over VMware Added VmwareStorageMotionStrategy to application context. Signed-off-by: Sateesh Chodapuneedi Support for volume live migration across datastores CLOUDSTACK-659 Fixing migrate volume. Unit tests for vmware storage motion. These test the VmwareStorageMotionStrategy. CLOUDSTACK-2701 - Enable storage migration for VMware resources Sending command MigrateWithStorageCommand to source host instead of target host for the case of migration of VM within cluster. CLOUDSTACK-2701 - Enable storage migration for VMware resources Moved 2 methods that are not specific to VMware but Volume are moved to VolumeManager from VmwareManager. Moved datastore volume path constructing code to separate method. Added check for source and target host, if they are from different DCs/vCenter instances. Updated error message & removed stale comment CLOUDSTACK-2701 - Enable storage migration for VMware resources Injecting component VolumeManager into VmwareResource. Signed-off-by: Sateesh Chodapuneedi --- client/tomcatconf/applicationContext.xml.in | 1 + .../agent/api/MigrateWithStorageCommand.java | 12 + .../vmware/manager/VmwareManager.java | 1 + .../vmware/manager/VmwareManagerImpl.java | 12 + .../manager/VmwareStorageManagerImpl.java | 10 +- .../vmware/resource/VmwareResource.java | 315 +++++++++++++++++- .../motion/VmwareStorageMotionStrategy.java | 212 ++++++++++++ .../VmwareStorageMotionStrategyTest.java | 271 +++++++++++++++ .../src/com/cloud/storage/VolumeManager.java | 4 + .../com/cloud/storage/VolumeManagerImpl.java | 13 + .../hypervisor/vmware/mo/DatastoreMO.java | 44 ++- .../vmware/mo/HostDatastoreBrowserMO.java | 20 +- .../vmware/mo/HypervisorHostHelper.java | 1 + .../vmware/mo/VirtualDiskManagerMO.java | 4 + .../vmware/mo/VirtualMachineMO.java | 26 ++ 15 files changed, 931 insertions(+), 15 deletions(-) create mode 100644 plugins/hypervisors/vmware/src/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategy.java create mode 100644 plugins/hypervisors/vmware/test/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategyTest.java diff --git a/client/tomcatconf/applicationContext.xml.in b/client/tomcatconf/applicationContext.xml.in index 26b698af144..2fe51414e3e 100644 --- a/client/tomcatconf/applicationContext.xml.in +++ b/client/tomcatconf/applicationContext.xml.in @@ -733,6 +733,7 @@ + diff --git a/core/src/com/cloud/agent/api/MigrateWithStorageCommand.java b/core/src/com/cloud/agent/api/MigrateWithStorageCommand.java index 058aa15338e..a108a2a7bed 100644 --- a/core/src/com/cloud/agent/api/MigrateWithStorageCommand.java +++ b/core/src/com/cloud/agent/api/MigrateWithStorageCommand.java @@ -24,10 +24,18 @@ import com.cloud.agent.api.to.StorageFilerTO; public class MigrateWithStorageCommand extends Command { VirtualMachineTO vm; Map volumeToFiler; + String tgtHost; public MigrateWithStorageCommand(VirtualMachineTO vm, Map volumeToFiler) { this.vm = vm; this.volumeToFiler = volumeToFiler; + this.tgtHost = null; + } + + public MigrateWithStorageCommand(VirtualMachineTO vm, Map volumeToFiler, String tgtHost) { + this.vm = vm; + this.volumeToFiler = volumeToFiler; + this.tgtHost = tgtHost; } public VirtualMachineTO getVirtualMachine() { @@ -38,6 +46,10 @@ public class MigrateWithStorageCommand extends Command { return volumeToFiler; } + public String getTargetHost() { + return tgtHost; + } + @Override public boolean executeInSequence() { return true; diff --git a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManager.java b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManager.java index fb6d3d6667f..1f4552e4c53 100755 --- a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManager.java +++ b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManager.java @@ -20,6 +20,7 @@ import java.io.File; import java.util.List; import java.util.Map; +import com.cloud.agent.api.to.VolumeTO; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.vmware.manager.VmwareStorageManager; import com.cloud.hypervisor.vmware.mo.HostMO; diff --git a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java index 9f260f1812c..3d7d85fd2f2 100755 --- a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java +++ b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java @@ -35,6 +35,8 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreRole; import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; @@ -45,6 +47,8 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.to.VolumeTO; import com.cloud.cluster.ClusterManager; import com.cloud.configuration.Config; import com.cloud.configuration.dao.ConfigurationDao; @@ -79,6 +83,9 @@ import com.cloud.serializer.GsonHelper; import com.cloud.server.ConfigurationServer; import com.cloud.storage.JavaStorageLayer; import com.cloud.storage.StorageLayer; +import com.cloud.storage.StoragePool; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.secondary.SecondaryStorageVmManager; import com.cloud.utils.FileUtil; import com.cloud.utils.NumbersUtil; @@ -92,6 +99,8 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; import com.cloud.utils.ssh.SshHelper; import com.cloud.vm.DomainRouterVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; import com.google.gson.Gson; import com.vmware.vim25.AboutInfo; import com.vmware.vim25.HostConnectSpec; @@ -124,6 +133,9 @@ public class VmwareManagerImpl extends ManagerBase implements VmwareManager, Vmw @Inject ConfigurationDao _configDao; @Inject ConfigurationServer _configServer; @Inject HypervisorCapabilitiesDao _hvCapabilitiesDao; + @Inject VMInstanceDao _vmDao; + @Inject VolumeDao _volDao; + @Inject DataStoreManager dataStoreMgr; String _mountParent; StorageLayer _storage; diff --git a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java index 00255216cf3..4ae0f305d99 100644 --- a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java +++ b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java @@ -328,7 +328,7 @@ public class VmwareStorageManagerImpl implements VmwareStorageManager { workerVm = vmMo; // attach volume to worker VM - String datastoreVolumePath = String.format("[%s] %s.vmdk", dsMo.getName(), volumePath); + String datastoreVolumePath = getVolumePathInDatastore(dsMo, volumePath + ".vmdk"); vmMo.attachDisk(new String[] { datastoreVolumePath }, morDs); } } @@ -1060,7 +1060,7 @@ public class VmwareStorageManagerImpl implements VmwareStorageManager { } //attach volume to worker VM - String datastoreVolumePath = String.format("[%s] %s.vmdk", dsMo.getName(), volumePath); + String datastoreVolumePath = getVolumePathInDatastore(dsMo, volumePath + ".vmdk"); workerVm.attachDisk(new String[] { datastoreVolumePath }, morDs); vmMo = workerVm; } @@ -1081,6 +1081,12 @@ public class VmwareStorageManagerImpl implements VmwareStorageManager { } } + private String getVolumePathInDatastore(DatastoreMO dsMo, String volumeFileName) throws Exception { + String datastoreVolumePath = dsMo.searchFileInSubFolders(volumeFileName, true); + assert (datastoreVolumePath != null) : "Virtual disk file missing from datastore."; + return datastoreVolumePath; + } + private Pair copyVolumeFromSecStorage(VmwareHypervisorHost hyperHost, long volumeId, DatastoreMO dsMo, String secStorageUrl, String exportName) throws Exception { diff --git a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java index 0a15d4b5b3d..5f99a152fdd 100755 --- a/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -37,6 +37,7 @@ import java.util.Random; import java.util.TimeZone; import java.util.UUID; +import javax.inject.Inject; import javax.naming.ConfigurationException; import org.apache.log4j.Logger; @@ -73,6 +74,9 @@ import com.cloud.agent.api.CreateVolumeFromSnapshotCommand; import com.cloud.agent.api.DeleteStoragePoolCommand; import com.cloud.agent.api.DeleteVMSnapshotAnswer; import com.cloud.agent.api.DeleteVMSnapshotCommand; +import com.cloud.agent.api.MigrateWithStorageAnswer; +import com.cloud.agent.api.MigrateWithStorageCommand; +import com.cloud.agent.api.UnregisterVMCommand; import com.cloud.agent.api.GetDomRVersionAnswer; import com.cloud.agent.api.GetDomRVersionCmd; import com.cloud.agent.api.GetHostStatsAnswer; @@ -162,6 +166,12 @@ import com.cloud.agent.api.routing.VmDataCommand; import com.cloud.agent.api.routing.VpnUsersCfgCommand; import com.cloud.agent.api.storage.CopyVolumeAnswer; import com.cloud.agent.api.storage.CopyVolumeCommand; +import com.cloud.agent.api.storage.CreateVolumeOVACommand; +import com.cloud.agent.api.storage.CreateVolumeOVAAnswer; +import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.storage.MigrateVolumeCommand; +import com.cloud.agent.api.storage.PrepareOVAPackingAnswer; +import com.cloud.agent.api.storage.PrepareOVAPackingCommand; import com.cloud.agent.api.storage.CreateAnswer; import com.cloud.agent.api.storage.CreateCommand; import com.cloud.agent.api.storage.CreatePrivateTemplateAnswer; @@ -185,6 +195,7 @@ import com.cloud.agent.api.to.VolumeTO; import com.cloud.dc.DataCenter.NetworkType; import com.cloud.dc.Vlan; import com.cloud.exception.InternalErrorException; +import com.cloud.host.Host; import com.cloud.host.Host.Type; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.vmware.manager.VmwareHostService; @@ -195,10 +206,12 @@ import com.cloud.hypervisor.vmware.mo.CustomFieldsManagerMO; import com.cloud.hypervisor.vmware.mo.DatacenterMO; import com.cloud.hypervisor.vmware.mo.DatastoreMO; import com.cloud.hypervisor.vmware.mo.DiskControllerType; +import com.cloud.hypervisor.vmware.mo.HostDatastoreSystemMO; import com.cloud.hypervisor.vmware.mo.HostFirewallSystemMO; import com.cloud.hypervisor.vmware.mo.HostMO; import com.cloud.hypervisor.vmware.mo.HypervisorHostHelper; import com.cloud.hypervisor.vmware.mo.NetworkDetails; +import com.cloud.hypervisor.vmware.mo.VirtualDiskManagerMO; import com.cloud.hypervisor.vmware.mo.VirtualEthernetCardType; import com.cloud.hypervisor.vmware.mo.VirtualMachineMO; import com.cloud.hypervisor.vmware.mo.VirtualSwitchType; @@ -219,13 +232,17 @@ import com.cloud.network.rules.FirewallRule; import com.cloud.resource.ServerResource; import com.cloud.serializer.GsonHelper; import com.cloud.storage.Storage; +import com.cloud.storage.StoragePool; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeManager; +import com.cloud.storage.VolumeManagerImpl; import com.cloud.storage.resource.StoragePoolResource; import com.cloud.storage.template.TemplateInfo; import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; import com.cloud.utils.StringUtils; +import com.cloud.utils.component.ComponentContext; import com.cloud.utils.db.DB; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.ExceptionUtil; @@ -246,10 +263,12 @@ import com.vmware.vim25.DatastoreSummary; import com.vmware.vim25.DynamicProperty; import com.vmware.vim25.GuestInfo; import com.vmware.vim25.HostCapability; +import com.vmware.vim25.HostDatastoreBrowserSearchResults; import com.vmware.vim25.HostFirewallInfo; import com.vmware.vim25.HostFirewallRuleset; import com.vmware.vim25.HostNetworkTrafficShapingPolicy; import com.vmware.vim25.HostPortGroupSpec; +import com.vmware.vim25.ManagedObjectNotFound; import com.vmware.vim25.ManagedObjectReference; import com.vmware.vim25.ObjectContent; import com.vmware.vim25.OptionValue; @@ -265,9 +284,13 @@ import com.vmware.vim25.RuntimeFaultFaultMsg; import com.vmware.vim25.ToolsUnavailableFaultMsg; import com.vmware.vim25.VimPortType; import com.vmware.vim25.VirtualDevice; +import com.vmware.vim25.VirtualDeviceBackingInfo; import com.vmware.vim25.VirtualDeviceConfigSpec; import com.vmware.vim25.VirtualDeviceConfigSpecOperation; import com.vmware.vim25.VirtualDisk; +import com.vmware.vim25.VirtualDiskFlatVer2BackingInfo; +import com.vmware.vim25.VirtualDiskMode; +import com.vmware.vim25.VirtualDiskType; import com.vmware.vim25.VirtualEthernetCard; import com.vmware.vim25.VirtualEthernetCardNetworkBackingInfo; import com.vmware.vim25.VirtualLsiLogicController; @@ -275,9 +298,38 @@ import com.vmware.vim25.VirtualMachineConfigSpec; import com.vmware.vim25.VirtualMachineFileInfo; import com.vmware.vim25.VirtualMachineGuestOsIdentifier; import com.vmware.vim25.VirtualMachinePowerState; +import com.vmware.vim25.VirtualMachineRelocateSpec; +import com.vmware.vim25.VirtualMachineRelocateSpecDiskLocator; import com.vmware.vim25.VirtualMachineRuntimeInfo; import com.vmware.vim25.VirtualSCSISharing; +import org.apache.log4j.Logger; +import org.apache.log4j.NDC; + +import javax.naming.ConfigurationException; +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.channels.SocketChannel; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.Random; +import java.util.TimeZone; +import java.util.UUID; public class VmwareResource implements StoragePoolResource, ServerResource, VmwareHostService { private static final Logger s_logger = Logger.getLogger(VmwareResource.class); @@ -288,6 +340,9 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa protected final int _shutdown_waitMs = 300000; // wait up to 5 minutes for shutdown + @Inject + protected VolumeManager volMgr; + // out an operation protected final int _retry = 24; protected final int _sleep = 10000; @@ -398,6 +453,10 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa answer = execute((PrepareForMigrationCommand) cmd); } else if (clz == MigrateCommand.class) { answer = execute((MigrateCommand) cmd); + } else if (clz == MigrateWithStorageCommand.class) { + answer = execute((MigrateWithStorageCommand) cmd); + } else if (clz == MigrateVolumeCommand.class) { + answer = execute((MigrateVolumeCommand) cmd); } else if (clz == DestroyCommand.class) { answer = execute((DestroyCommand) cmd); } else if (clz == CreateStoragePoolCommand.class) { @@ -2506,7 +2565,9 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa Pair volumeDsDetails = dataStoresDetails.get(vol.getPoolUuid()); assert (volumeDsDetails != null); VirtualDevice device; - datastoreDiskPath = String.format("[%s] %s.vmdk", volumeDsDetails.second().getName(), vol.getPath()); + + datastoreDiskPath = volumeDsDetails.second().searchFileInSubFolders(vol.getPath() + ".vmdk", true); + String chainInfo = vol.getChainInfo(); if (chainInfo != null && !chainInfo.isEmpty()) { @@ -3371,6 +3432,254 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa } } + protected Answer execute(MigrateWithStorageCommand cmd) { + + if (s_logger.isInfoEnabled()) { + s_logger.info("Executing resource MigrateWithStorageCommand: " + _gson.toJson(cmd)); + } + + VirtualMachineTO vmTo = cmd.getVirtualMachine(); + final String vmName = vmTo.getName(); + + State state = null; + synchronized (_vms) { + state = _vms.get(vmName); + _vms.put(vmName, State.Stopping); + } + + VmwareHypervisorHost srcHyperHost = null; + VmwareHypervisorHost tgtHyperHost = null; + VirtualMachineMO vmMo = null; + + ManagedObjectReference morDsAtTarget = null; + ManagedObjectReference morDsAtSource = null; + ManagedObjectReference morDc = null; + ManagedObjectReference morDcOfTargetHost = null; + ManagedObjectReference morTgtHost = new ManagedObjectReference(); + VirtualMachineRelocateSpec relocateSpec = new VirtualMachineRelocateSpec(); + List diskLocators = new ArrayList(); + VirtualMachineRelocateSpecDiskLocator diskLocator = null; + + boolean isFirstDs = true; + String srcDiskName = ""; + String srcDsName = ""; + String tgtDsName = ""; + String tgtDsNfsHost; + String tgtDsNfsPath; + int tgtDsNfsPort; + VolumeTO volume; + StorageFilerTO filerTo; + Set mountedDatastoresAtSource = new HashSet(); + + Map volToFiler = cmd.getVolumeToFiler(); + String tgtHost = cmd.getTargetHost(); + String tgtHostMorInfo = tgtHost.split("@")[0]; + morTgtHost.setType(tgtHostMorInfo.split(":")[0]); + morTgtHost.setValue(tgtHostMorInfo.split(":")[1]); + + try { + srcHyperHost = getHyperHost(getServiceContext()); + tgtHyperHost = new HostMO(getServiceContext(), morTgtHost); + morDc = srcHyperHost.getHyperHostDatacenter(); + morDcOfTargetHost = tgtHyperHost.getHyperHostDatacenter(); + if (morDc != morDcOfTargetHost) { + String msg = "Source host & target host are in different datacentesr"; + throw new CloudRuntimeException(msg); + } + VmwareManager mgr = tgtHyperHost.getContext().getStockObject(VmwareManager.CONTEXT_STOCK_NAME); + + // find VM through datacenter (VM is not at the target host yet) + vmMo = srcHyperHost.findVmOnPeerHyperHost(vmName); + if (vmMo == null) { + String msg = "VM " + vmName + " does not exist in VMware datacenter " + morDc.getValue(); + s_logger.error(msg); + throw new Exception(msg); + } + + // Get details of each target datastore & attach to source host. + for (Entry entry : volToFiler.entrySet()) { + volume = entry.getKey(); + filerTo = entry.getValue(); + + srcDsName = volume.getPoolUuid().replace("-", ""); + tgtDsName = filerTo.getUuid().replace("-", ""); + tgtDsNfsHost = filerTo.getHost(); + tgtDsNfsPath = filerTo.getPath(); + tgtDsNfsPort = filerTo.getPort(); + + s_logger.debug("Preparing spec for volume : " + volume.getName()); + morDsAtTarget = HypervisorHostHelper.findDatastoreWithBackwardsCompatibility(tgtHyperHost, filerTo.getUuid()); + if (morDsAtTarget == null) { + String msg = "Unable to find the mounted datastore with uuid " + morDsAtTarget + " to execute MigrateWithStorageCommand"; + s_logger.error(msg); + throw new Exception(msg); + } + morDsAtSource = HypervisorHostHelper.findDatastoreWithBackwardsCompatibility(srcHyperHost, filerTo.getUuid()); + if (morDsAtSource == null) { + morDsAtSource = srcHyperHost.mountDatastore(false, tgtDsNfsHost, tgtDsNfsPort, tgtDsNfsPath, tgtDsName); + if (morDsAtSource == null) { + throw new Exception("Unable to mount datastore " + tgtDsNfsHost + ":/" + tgtDsNfsPath + " on " + _hostName); + } + mountedDatastoresAtSource.add(tgtDsName); + s_logger.debug("Mounted datastore " + tgtDsNfsHost + ":/" + tgtDsNfsPath + " on " + _hostName); + } + + if (isFirstDs) { + relocateSpec.setDatastore(morDsAtSource); + isFirstDs = false; + } + srcDiskName = String.format("[%s] %s.vmdk", srcDsName, volume.getPath()); + diskLocator = new VirtualMachineRelocateSpecDiskLocator(); + diskLocator.setDatastore(morDsAtSource); + diskLocator.setDiskId(getVirtualDiskInfo(vmMo, srcDiskName)); + + diskLocators.add(diskLocator); + + } + relocateSpec.getDisk().addAll(diskLocators); + + // Prepare network at target before migration + NicTO[] nics = vmTo.getNics(); + for (NicTO nic : nics) { + // prepare network on the host + prepareNetworkFromNicInfo(new HostMO(getServiceContext(), morTgtHost), nic, false, vmTo.getType()); + } + + // Ensure secondary storage mounted on target host + String secStoreUrl = mgr.getSecondaryStorageStoreUrl(Long.parseLong(_dcId)); + if(secStoreUrl == null) { + String msg = "secondary storage for dc " + _dcId + " is not ready yet?"; + throw new Exception(msg); + } + mgr.prepareSecondaryStorageStore(secStoreUrl); + ManagedObjectReference morSecDs = prepareSecondaryDatastoreOnHost(secStoreUrl); + if (morSecDs == null) { + String msg = "Failed to prepare secondary storage on host, secondary store url: " + secStoreUrl; + throw new Exception(msg); + } + + // Change datastore + if (!vmMo.changeDatastore(relocateSpec)) { + throw new Exception("Change datastore operation failed during storage migration"); + } else { + s_logger.debug("Successfully migrated storage of VM " + vmName + " to target datastore(s)"); + } + + // Change host + ManagedObjectReference morPool = tgtHyperHost.getHyperHostOwnerResourcePool(); + if (!vmMo.migrate(morPool, tgtHyperHost.getMor())) { + throw new Exception("Change datastore operation failed during storage migration"); + } else { + s_logger.debug("Successfully relocated VM " + vmName + " from " + _hostName + " to " + tgtHyperHost.getHyperHostName()); + } + + state = State.Stopping; + List volumeToList = null; + return new MigrateWithStorageAnswer(cmd, volumeToList); + } catch (Throwable e) { + if (e instanceof RemoteException) { + s_logger.warn("Encountered remote exception at vCenter, invalidating VMware session context"); + invalidateServiceContext(); + } + + String msg = "MigrationCommand failed due to " + VmwareHelper.getExceptionMessage(e); + s_logger.warn(msg, e); + return new MigrateWithStorageAnswer(cmd, (Exception) e); + } finally { + // Cleanup datastores mounted on source host + for(String mountedDatastore : mountedDatastoresAtSource) { + s_logger.debug("Attempting to unmount datastore " + mountedDatastore + " at " + _hostName); + try { + srcHyperHost.unmountDatastore(mountedDatastore); + } catch (Exception unmountEx) { + s_logger.debug("Failed to unmount datastore " + mountedDatastore + " at " + _hostName + + ". Seems the datastore is still being used by " + _hostName + + ". Please unmount manually to cleanup."); + } + s_logger.debug("Successfully unmounted datastore " + mountedDatastore + " at " + _hostName); + } + synchronized (_vms) { + _vms.put(vmName, state); + } + } + } + + private Answer execute(MigrateVolumeCommand cmd) { + String volumePath = cmd.getVolumePath(); + StorageFilerTO poolTo = cmd.getPool(); + + if (s_logger.isInfoEnabled()) { + s_logger.info("Executing resource MigrateVolumeCommand: " + _gson.toJson(cmd)); + } + + VmwareContext context = getServiceContext(); + VmwareManager mgr = context.getStockObject(VmwareManager.CONTEXT_STOCK_NAME); + final String vmName = volMgr.getVmNameFromVolumeId(cmd.getVolumeId()); + + VirtualMachineMO vmMo = null; + VmwareHypervisorHost srcHyperHost = null; + + ManagedObjectReference morDs = null; + ManagedObjectReference morDc = null; + VirtualMachineRelocateSpec relocateSpec = new VirtualMachineRelocateSpec(); + List diskLocators = new ArrayList(); + VirtualMachineRelocateSpecDiskLocator diskLocator = null; + + String srcDiskName = ""; + String srcDsName = ""; + String tgtDsName = ""; + + try { + srcHyperHost = getHyperHost(getServiceContext()); + morDc = srcHyperHost.getHyperHostDatacenter(); + srcDsName = volMgr.getStoragePoolOfVolume(cmd.getVolumeId()); + tgtDsName = poolTo.getUuid().replace("-", ""); + + // find VM through datacenter (VM is not at the target host yet) + vmMo = srcHyperHost.findVmOnPeerHyperHost(vmName); + if (vmMo == null) { + String msg = "VM " + vmName + " does not exist in VMware datacenter " + morDc.getValue(); + s_logger.error(msg); + throw new Exception(msg); + } + morDs = HypervisorHostHelper.findDatastoreWithBackwardsCompatibility(srcHyperHost, tgtDsName); + if (morDs == null) { + String msg = "Unable to find the mounted datastore with name " + tgtDsName + " to execute MigrateVolumeCommand"; + s_logger.error(msg); + throw new Exception(msg); + } + + srcDiskName = String.format("[%s] %s.vmdk", srcDsName, volumePath); + diskLocator = new VirtualMachineRelocateSpecDiskLocator(); + diskLocator.setDatastore(morDs); + diskLocator.setDiskId(getVirtualDiskInfo(vmMo, srcDiskName)); + + diskLocators.add(diskLocator); + relocateSpec.getDisk().add(diskLocator); + + // Change datastore + if (!vmMo.changeDatastore(relocateSpec)) { + throw new Exception("Change datastore operation failed during volume migration"); + } else { + s_logger.debug("Successfully migrated volume " + volumePath + " to target datastore " + tgtDsName); + } + + return new MigrateVolumeAnswer(cmd, true, null, volumePath); + } catch (Exception e) { + String msg = "Catch Exception " + e.getClass().getName() + " due to " + e.toString(); + s_logger.error(msg, e); + return new MigrateVolumeAnswer(cmd, false, msg, null); + } + } + + private int getVirtualDiskInfo(VirtualMachineMO vmMo, String srcDiskName) throws Exception { + Pair deviceInfo = vmMo.getDiskDevice(srcDiskName, false); + if(deviceInfo == null) { + throw new Exception("No such disk device: " + srcDiskName); + } + return deviceInfo.first().getKey(); + } + private VmwareHypervisorHost getTargetHyperHost(DatacenterMO dcMo, String destIp) throws Exception { VmwareManager mgr = dcMo.getContext().getStockObject(VmwareManager.CONTEXT_STOCK_NAME); @@ -3483,7 +3792,8 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa } DatastoreMO dsMo = new DatastoreMO(getServiceContext(), morDs); - String datastoreVolumePath = String.format("[%s] %s.vmdk", dsMo.getName(), cmd.getVolumePath()); + String datastoreVolumePath = dsMo.searchFileInSubFolders(cmd.getVolumePath() + ".vmdk", true); + assert (datastoreVolumePath != null) : "Virtual disk file must exist in specified datastore for attach/detach operations."; AttachVolumeAnswer answer = new AttachVolumeAnswer(cmd, cmd.getDeviceId()); if (cmd.getAttach()) { @@ -5286,6 +5596,7 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa _guestTrafficInfo = (VmwareTrafficLabel) params.get("guestTrafficInfo"); _publicTrafficInfo = (VmwareTrafficLabel) params.get("publicTrafficInfo"); VmwareContext context = getServiceContext(); + volMgr = ComponentContext.inject(VolumeManagerImpl.class); try { VmwareManager mgr = context.getStockObject(VmwareManager.CONTEXT_STOCK_NAME); mgr.setupResourceStartupParams(params); diff --git a/plugins/hypervisors/vmware/src/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategy.java b/plugins/hypervisors/vmware/src/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategy.java new file mode 100644 index 00000000000..c32d3d5abb7 --- /dev/null +++ b/plugins/hypervisors/vmware/src/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategy.java @@ -0,0 +1,212 @@ +/* + * 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 java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; +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.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.MigrateWithStorageAnswer; +import com.cloud.agent.api.MigrateWithStorageCommand; +import com.cloud.agent.api.MigrateWithStorageCompleteAnswer; +import com.cloud.agent.api.MigrateWithStorageCompleteCommand; +import com.cloud.agent.api.MigrateWithStorageReceiveAnswer; +import com.cloud.agent.api.MigrateWithStorageReceiveCommand; +import com.cloud.agent.api.MigrateWithStorageSendAnswer; +import com.cloud.agent.api.MigrateWithStorageSendCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.StoragePool; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; + +@Component +public class VmwareStorageMotionStrategy implements DataMotionStrategy { + private static final Logger s_logger = Logger.getLogger(VmwareStorageMotionStrategy.class); + @Inject AgentManager agentMgr; + @Inject VolumeDao volDao; + @Inject VolumeDataFactory volFactory; + @Inject PrimaryDataStoreDao storagePoolDao; + @Inject VMInstanceDao instanceDao; + + @Override + public boolean canHandle(DataObject srcData, DataObject destData) { + return false; + } + + @Override + public boolean canHandle(Map volumeMap, Host srcHost, Host destHost) { + if (srcHost.getHypervisorType() == HypervisorType.VMware && destHost.getHypervisorType() == HypervisorType.VMware) { + s_logger.debug(this.getClass() + " can handle the request because the hosts have VMware hypervisor"); + return true; + } + return false; + } + + @Override + public Void copyAsync(DataObject srcData, DataObject destData, + AsyncCompletionCallback callback) { + CopyCommandResult result = new CopyCommandResult(null, null); + result.setResult("Unsupported operation requested for copying data."); + callback.complete(result); + + return null; + } + + @Override + public Void copyAsync(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, + AsyncCompletionCallback callback) { + Answer answer = null; + String errMsg = null; + try { + VMInstanceVO instance = instanceDao.findById(vmTo.getId()); + if (instance != null) { + if (srcHost.getClusterId() == destHost.getClusterId()) { + answer = migrateVmWithVolumesWithinCluster(instance, vmTo, srcHost, destHost, volumeMap); + } else { + answer = migrateVmWithVolumesAcrossCluster(instance, vmTo, srcHost, destHost, volumeMap); + } + } else { + throw new CloudRuntimeException("Unsupported operation requested for moving data."); + } + } catch (Exception e) { + s_logger.error("copy failed", e); + errMsg = e.toString(); + } + + CopyCommandResult result = new CopyCommandResult(null, answer); + result.setResult(errMsg); + callback.complete(result); + return null; + } + + private Answer migrateVmWithVolumesAcrossCluster(VMInstanceVO vm, VirtualMachineTO to, Host srcHost, + Host destHost, Map volumeToPool) throws AgentUnavailableException { + + // Initiate migration of a virtual machine with it's volumes. + try { + Map volumeToFilerto = new HashMap(); + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeInfo volume = entry.getKey(); + VolumeTO volumeTo = new VolumeTO(volume, storagePoolDao.findById(volume.getPoolId())); + StorageFilerTO filerTo = new StorageFilerTO((StoragePool)entry.getValue()); + volumeToFilerto.put(volumeTo, filerTo); + } + + // Migration across cluster needs to be done in three phases. + // 1. Send a migrate command to source resource to initiate migration + // Run validations against target!! + // 2. Complete the process. Update the volume details. + MigrateWithStorageCommand migrateWithStorageCmd = new MigrateWithStorageCommand(to, volumeToFilerto, destHost.getGuid()); + MigrateWithStorageAnswer migrateWithStorageAnswer = (MigrateWithStorageAnswer) agentMgr.send( + srcHost.getId(), migrateWithStorageCmd); + if (migrateWithStorageAnswer == null) { + s_logger.error("Migration with storage of vm " + vm+ " to host " + destHost + " failed."); + throw new CloudRuntimeException("Error while migrating the vm " + vm + " to host " + destHost); + } else if (!migrateWithStorageAnswer.getResult()) { + s_logger.error("Migration with storage of vm " + vm+ " failed. Details: " + migrateWithStorageAnswer.getDetails()); + throw new CloudRuntimeException("Error while migrating the vm " + vm + " to host " + destHost + + ". " + migrateWithStorageAnswer.getDetails()); + } else { + // Update the volume details after migration. + updateVolumesAfterMigration(volumeToPool); + } + s_logger.debug("Storage migration of VM " + vm.getInstanceName() + " completed successfully. Migrated to host " + destHost.getName()); + + return migrateWithStorageAnswer; + } catch (OperationTimedoutException e) { + s_logger.error("Error while migrating vm " + vm + " to host " + destHost, e); + throw new AgentUnavailableException("Operation timed out on storage motion for " + vm, destHost.getId()); + } + } + + private Answer migrateVmWithVolumesWithinCluster(VMInstanceVO vm, VirtualMachineTO to, Host srcHost, + Host destHost, Map volumeToPool) throws AgentUnavailableException { + + // Initiate migration of a virtual machine with it's volumes. + try { + Map volumeToFilerto = new HashMap(); + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeInfo volume = entry.getKey(); + VolumeTO volumeTo = new VolumeTO(volume, storagePoolDao.findById(volume.getPoolId())); + StorageFilerTO filerTo = new StorageFilerTO((StoragePool)entry.getValue()); + volumeToFilerto.put(volumeTo, filerTo); + } + + MigrateWithStorageCommand command = new MigrateWithStorageCommand(to, volumeToFilerto, destHost.getGuid()); + MigrateWithStorageAnswer answer = (MigrateWithStorageAnswer) agentMgr.send(srcHost.getId(), command); + if (answer == null) { + s_logger.error("Migration with storage of vm " + vm + " failed."); + throw new CloudRuntimeException("Error while migrating the vm " + vm + " to host " + destHost); + } else if (!answer.getResult()) { + s_logger.error("Migration with storage of vm " + vm+ " failed. Details: " + answer.getDetails()); + throw new CloudRuntimeException("Error while migrating the vm " + vm + " to host " + destHost + + ". " + answer.getDetails()); + } else { + // Update the volume details after migration. + updateVolumesAfterMigration(volumeToPool); + } + + return answer; + } catch (OperationTimedoutException e) { + s_logger.error("Error while migrating vm " + vm + " to host " + destHost, e); + throw new AgentUnavailableException("Operation timed out on storage motion for " + vm, destHost.getId()); + } + } + + private void updateVolumesAfterMigration(Map volumeToPool) { + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeInfo volume = entry.getKey(); + StoragePool pool = (StoragePool)entry.getValue(); + + VolumeVO volumeVO = volDao.findById(volume.getId()); + Long oldPoolId = volumeVO.getPoolId(); + volumeVO.setLastPoolId(oldPoolId); + volumeVO.setFolder(pool.getPath()); + volumeVO.setPodId(pool.getPodId()); + volumeVO.setPoolId(pool.getId()); + + volDao.update(volume.getId(), volumeVO); + s_logger.debug("Volume path was successfully updated for volume " + volume.getName() + " after it was migrated."); + } + } +} diff --git a/plugins/hypervisors/vmware/test/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategyTest.java b/plugins/hypervisors/vmware/test/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategyTest.java new file mode 100644 index 00000000000..ae4f41d55a5 --- /dev/null +++ b/plugins/hypervisors/vmware/test/org/apache/cloudstack/storage/motion/VmwareStorageMotionStrategyTest.java @@ -0,0 +1,271 @@ +// 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.engine.subsystem.api.storage.CommandResult; +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.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.async.AsyncRpcConext; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.test.utils.SpringUtils; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.MigrateWithStorageAnswer; +import com.cloud.agent.api.MigrateWithStorageCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.component.ComponentContext; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class) +public class VmwareStorageMotionStrategyTest { + + @Inject VmwareStorageMotionStrategy strategy = new VmwareStorageMotionStrategy(); + @Inject AgentManager agentMgr; + @Inject VolumeDao volDao; + @Inject VolumeDataFactory volFactory; + @Inject PrimaryDataStoreDao storagePoolDao; + @Inject VMInstanceDao instanceDao; + + CopyCommandResult result; + + @BeforeClass + public static void setUp() throws ConfigurationException { + } + + @Before + public void testSetUp() { + ComponentContext.initComponentsLifeCycle(); + } + + @Test + public void testStrategyHandlesVmwareHosts() throws Exception { + Host srcHost = mock(Host.class); + Host destHost = mock(Host.class); + when(srcHost.getHypervisorType()).thenReturn(HypervisorType.VMware); + when(destHost.getHypervisorType()).thenReturn(HypervisorType.VMware); + Map volumeMap = new HashMap(); + boolean canHandle = strategy.canHandle(volumeMap, srcHost, destHost); + assertTrue("The strategy is only supposed to handle vmware hosts", canHandle); + } + + @Test + public void testStrategyDoesnotHandlesNonVmwareHosts() throws Exception { + Host srcHost = mock(Host.class); + Host destHost = mock(Host.class); + when(srcHost.getHypervisorType()).thenReturn(HypervisorType.XenServer); + when(destHost.getHypervisorType()).thenReturn(HypervisorType.XenServer); + Map volumeMap = new HashMap(); + boolean canHandle = strategy.canHandle(volumeMap, srcHost, destHost); + assertFalse("The strategy is only supposed to handle vmware hosts", canHandle); + } + + @Test + public void testMigrateWithinClusterSuccess() throws Exception { + Host srcHost = mock(Host.class); + Host destHost = mock(Host.class); + when(srcHost.getClusterId()).thenReturn(1L); + when(destHost.getClusterId()).thenReturn(1L); + Map volumeMap = new HashMap(); + VirtualMachineTO to = mock(VirtualMachineTO.class); + when(to.getId()).thenReturn(6L); + VMInstanceVO instance = mock(VMInstanceVO.class); + when(instanceDao.findById(6L)).thenReturn(instance); + + MockContext context = new MockContext(null, null, volumeMap); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().mockCallBack(null, null)).setContext(context); + + MigrateWithStorageAnswer migAnswerMock = mock(MigrateWithStorageAnswer.class); + when(migAnswerMock.getResult()).thenReturn(true); + when(agentMgr.send(anyLong(), isA(MigrateWithStorageCommand.class))).thenReturn(migAnswerMock); + + strategy.copyAsync(volumeMap, to, srcHost, destHost, caller); + assertTrue("Migration within cluster isn't successful.", this.result.isSuccess()); + } + + @Test + public void testMigrateWithinClusterFailure() throws Exception { + Host srcHost = mock(Host.class); + Host destHost = mock(Host.class); + when(srcHost.getClusterId()).thenReturn(1L); + when(destHost.getClusterId()).thenReturn(1L); + Map volumeMap = new HashMap(); + VirtualMachineTO to = mock(VirtualMachineTO.class); + when(to.getId()).thenReturn(6L); + VMInstanceVO instance = mock(VMInstanceVO.class); + when(instanceDao.findById(6L)).thenReturn(instance); + + MockContext context = new MockContext(null, null, volumeMap); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().mockCallBack(null, null)).setContext(context); + + MigrateWithStorageAnswer migAnswerMock = mock(MigrateWithStorageAnswer.class); + when(migAnswerMock.getResult()).thenReturn(false); + when(agentMgr.send(anyLong(), isA(MigrateWithStorageCommand.class))).thenReturn(migAnswerMock); + + strategy.copyAsync(volumeMap, to, srcHost, destHost, caller); + assertFalse("Migration within cluster didn't fail.", this.result.isSuccess()); + } + + @Test + public void testMigrateAcrossClusterSuccess() throws Exception { + Host srcHost = mock(Host.class); + Host destHost = mock(Host.class); + when(srcHost.getClusterId()).thenReturn(1L); + when(destHost.getClusterId()).thenReturn(2L); + Map volumeMap = new HashMap(); + VirtualMachineTO to = mock(VirtualMachineTO.class); + when(to.getId()).thenReturn(6L); + VMInstanceVO instance = mock(VMInstanceVO.class); + when(instanceDao.findById(6L)).thenReturn(instance); + + MockContext context = new MockContext(null, null, volumeMap); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().mockCallBack(null, null)).setContext(context); + + MigrateWithStorageAnswer migAnswerMock = mock(MigrateWithStorageAnswer.class); + when(migAnswerMock.getResult()).thenReturn(true); + when(agentMgr.send(anyLong(), isA(MigrateWithStorageCommand.class))).thenReturn(migAnswerMock); + + strategy.copyAsync(volumeMap, to, srcHost, destHost, caller); + assertTrue("Migration across cluster isn't successful.", this.result.isSuccess()); + } + + @Test + public void testMigrateAcrossClusterFailure() throws Exception { + Host srcHost = mock(Host.class); + Host destHost = mock(Host.class); + when(srcHost.getClusterId()).thenReturn(1L); + when(destHost.getClusterId()).thenReturn(2L); + Map volumeMap = new HashMap(); + VirtualMachineTO to = mock(VirtualMachineTO.class); + when(to.getId()).thenReturn(6L); + VMInstanceVO instance = mock(VMInstanceVO.class); + when(instanceDao.findById(6L)).thenReturn(instance); + + MockContext context = new MockContext(null, null, volumeMap); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().mockCallBack(null, null)).setContext(context); + + MigrateWithStorageAnswer migAnswerMock = mock(MigrateWithStorageAnswer.class); + when(migAnswerMock.getResult()).thenReturn(false); + when(agentMgr.send(anyLong(), isA(MigrateWithStorageCommand.class))).thenReturn(migAnswerMock); + + strategy.copyAsync(volumeMap, to, srcHost, destHost, caller); + assertFalse("Migration across cluster didn't fail.", this.result.isSuccess()); + } + + private class MockContext extends AsyncRpcConext { + final Map volumeToPool; + final AsyncCallFuture future; + /** + * @param callback + */ + public MockContext(AsyncCompletionCallback callback, AsyncCallFuture future, + Map volumeToPool) { + super(callback); + this.volumeToPool = volumeToPool; + this.future = future; + } + } + + protected Void mockCallBack(AsyncCallbackDispatcher callback, MockContext context) { + this.result = callback.getResult(); + return null; + } + + @Configuration + @ComponentScan(basePackageClasses = { VmwareStorageMotionStrategy.class }, + includeFilters = {@Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)}, + useDefaultFilters = false) + public static class TestConfiguration extends SpringUtils.CloudStackTestConfiguration { + + @Bean + public VolumeDao volumeDao() { + return Mockito.mock(VolumeDao.class); + } + + @Bean + public VolumeDataFactory volumeDataFactory() { + return Mockito.mock(VolumeDataFactory.class); + } + + @Bean + public PrimaryDataStoreDao primaryDataStoreDao() { + return Mockito.mock(PrimaryDataStoreDao.class); + } + + @Bean + public VMInstanceDao vmInstanceDao() { + return Mockito.mock(VMInstanceDao.class); + } + + @Bean + public AgentManager agentManager() { + return Mockito.mock(AgentManager.class); + } + + public static class Library implements TypeFilter { + @Override + public boolean match(MetadataReader mdr, MetadataReaderFactory arg1) throws IOException { + ComponentScan cs = TestConfiguration.class.getAnnotation(ComponentScan.class); + return SpringUtils.includedInBasePackageClasses(mdr.getClassMetadata().getClassName(), cs); + } + } + } +} \ No newline at end of file diff --git a/server/src/com/cloud/storage/VolumeManager.java b/server/src/com/cloud/storage/VolumeManager.java index d198e5dd7df..47fbda8df9f 100644 --- a/server/src/com/cloud/storage/VolumeManager.java +++ b/server/src/com/cloud/storage/VolumeManager.java @@ -105,4 +105,8 @@ public interface VolumeManager extends VolumeApiService { DiskProfile allocateTemplatedVolume(Type type, String name, DiskOfferingVO offering, VMTemplateVO template, VMInstanceVO vm, Account owner); + + String getVmNameFromVolumeId(long volumeId); + + String getStoragePoolOfVolume(long volumeId); } diff --git a/server/src/com/cloud/storage/VolumeManagerImpl.java b/server/src/com/cloud/storage/VolumeManagerImpl.java index aedb68e1316..f1d7e642ded 100644 --- a/server/src/com/cloud/storage/VolumeManagerImpl.java +++ b/server/src/com/cloud/storage/VolumeManagerImpl.java @@ -2602,4 +2602,17 @@ public class VolumeManagerImpl extends ManagerBase implements VolumeManager { } } + + @Override + public String getVmNameFromVolumeId(long volumeId) { + Long instanceId; + VolumeVO volume = _volsDao.findById(volumeId); + return getVmNameOnVolume(volume); + } + + @Override + public String getStoragePoolOfVolume(long volumeId) { + VolumeVO vol = _volsDao.findById(volumeId); + return dataStoreMgr.getPrimaryDataStore(vol.getPoolId()).getUuid(); + } } diff --git a/vmware-base/src/com/cloud/hypervisor/vmware/mo/DatastoreMO.java b/vmware-base/src/com/cloud/hypervisor/vmware/mo/DatastoreMO.java index c79605d08ab..75553aeaf43 100755 --- a/vmware-base/src/com/cloud/hypervisor/vmware/mo/DatastoreMO.java +++ b/vmware-base/src/com/cloud/hypervisor/vmware/mo/DatastoreMO.java @@ -133,8 +133,14 @@ public class DatastoreMO extends BaseMO { fullPath = String.format("[%s] %s", datastoreName, path); try { - if(testExistence && !fileExists(fullPath)) - return true; + if(testExistence && !fileExists(fullPath)) { + String searchResult = searchFileInSubFolders(fullPath.split(" ")[1], true); + if (searchResult == null) { + return true; + } else { + fullPath = searchResult; + } + } } catch(Exception e) { s_logger.info("Unable to test file existence due to exception " + e.getClass().getName() + ", skip deleting of it"); return true; @@ -315,4 +321,38 @@ public class DatastoreMO extends BaseMO { s_logger.info("Folder " + folderName + " does not exist on datastore"); return false; } + + public String searchFileInSubFolders(String fileName, boolean caseInsensitive) throws Exception { + String datastorePath = "[" + getName() + "]"; + String rootDirectoryFilePath = String.format("%s %s", datastorePath, fileName); + if(fileExists(rootDirectoryFilePath)) { + return rootDirectoryFilePath; + } + + String parentFolderPath = null; + String absoluteFileName = null; + s_logger.info("Searching file " + fileName + " in " + datastorePath); + + HostDatastoreBrowserMO browserMo = getHostDatastoreBrowserMO(); + ArrayList results = browserMo.searchDatastoreSubFolders("[" + getName() + "]", fileName, caseInsensitive); + if (results.size() > 1) { + s_logger.warn("Multiple files with name " + fileName + " exists in datastore " + datastorePath + ". Trying to choose first file found in search attempt."); + } + for (HostDatastoreBrowserSearchResults result : results) { + if (result != null) { + List info = result.getFile(); + if (info != null && info.size() > 0) { + for (FileInfo fi : info) { + absoluteFileName = parentFolderPath = result.getFolderPath(); + s_logger.info("Found file " + fileName + " in datastore at " + absoluteFileName); + if(parentFolderPath.endsWith("]")) + absoluteFileName += " "; + absoluteFileName += fi.getPath(); + break; + } + } + } + } + return absoluteFileName; + } } diff --git a/vmware-base/src/com/cloud/hypervisor/vmware/mo/HostDatastoreBrowserMO.java b/vmware-base/src/com/cloud/hypervisor/vmware/mo/HostDatastoreBrowserMO.java index 59e754c951d..209aeed4edd 100644 --- a/vmware-base/src/com/cloud/hypervisor/vmware/mo/HostDatastoreBrowserMO.java +++ b/vmware-base/src/com/cloud/hypervisor/vmware/mo/HostDatastoreBrowserMO.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.hypervisor.vmware.mo; +import java.util.ArrayList; + import org.apache.log4j.Logger; import com.cloud.hypervisor.vmware.util.VmwareContext; @@ -76,7 +78,8 @@ public class HostDatastoreBrowserMO extends BaseMO { return searchDatastore(datastorePath, spec); } - public HostDatastoreBrowserSearchResults searchDatastoreSubFolders(String datastorePath, HostDatastoreBrowserSearchSpec searchSpec) throws Exception { + @SuppressWarnings("unchecked") + public ArrayList searchDatastoreSubFolders(String datastorePath, HostDatastoreBrowserSearchSpec searchSpec) throws Exception { if(s_logger.isTraceEnabled()) s_logger.trace("vCenter API trace - searchDatastoreSubFolders(). target mor: " + _mor.getValue() + ", file datastore path: " + datastorePath); @@ -87,7 +90,7 @@ public class HostDatastoreBrowserMO extends BaseMO { if(result) { _context.waitForTaskProgressDone(morTask); - return (HostDatastoreBrowserSearchResults)_context.getVimClient().getDynamicProperty(morTask, "info.result"); + return (ArrayList) _context.getVimClient().getDynamicProperty(morTask, "info.result"); } else { s_logger.error("VMware searchDaastoreSubFolders_Task failed due to " + TaskMO.getTaskFailureInfo(_context, morTask)); } @@ -99,12 +102,11 @@ public class HostDatastoreBrowserMO extends BaseMO { return null; } - public HostDatastoreBrowserSearchResults searchDatastoreSubFolders(String datastorePath, String folderName, boolean caseInsensitive) throws Exception { - HostDatastoreBrowserSearchSpec spec = new HostDatastoreBrowserSearchSpec(); - spec.setSearchCaseInsensitive(caseInsensitive); - spec.getMatchPattern().add(folderName); + public ArrayList searchDatastoreSubFolders(String datastorePath, String fileName, boolean caseInsensitive) throws Exception { + HostDatastoreBrowserSearchSpec spec = new HostDatastoreBrowserSearchSpec(); + spec.setSearchCaseInsensitive(caseInsensitive); + spec.getMatchPattern().add(fileName); - return searchDatastore(datastorePath, spec); - } + return searchDatastoreSubFolders(datastorePath, spec); + } } - diff --git a/vmware-base/src/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java b/vmware-base/src/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java index 20f84784157..157c7a6cd34 100755 --- a/vmware-base/src/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java +++ b/vmware-base/src/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java @@ -70,6 +70,7 @@ import com.vmware.vim25.VirtualDeviceConfigSpecOperation; import com.vmware.vim25.VirtualLsiLogicController; import com.vmware.vim25.VirtualMachineConfigSpec; import com.vmware.vim25.VirtualMachineFileInfo; +import com.vmware.vim25.VirtualMachineRelocateSpec; import com.vmware.vim25.VirtualMachineVideoCard; import com.vmware.vim25.VirtualSCSISharing; import com.vmware.vim25.VmwareDistributedVirtualSwitchPvlanSpec; diff --git a/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualDiskManagerMO.java b/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualDiskManagerMO.java index e21d06adf62..f181d02ce81 100755 --- a/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualDiskManagerMO.java +++ b/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualDiskManagerMO.java @@ -26,6 +26,10 @@ import com.vmware.vim25.VirtualDiskSpec; public class VirtualDiskManagerMO extends BaseMO { private static final Logger s_logger = Logger.getLogger(VirtualDiskManagerMO.class); + public VirtualDiskManagerMO(VmwareContext context) { + super(context, context.getServiceContent().getVirtualDiskManager()); + } + public VirtualDiskManagerMO(VmwareContext context, ManagedObjectReference morDiskMgr) { super(context, morDiskMgr); } diff --git a/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java b/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java index 660d9632baa..cf5ffdeb823 100644 --- a/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java +++ b/vmware-base/src/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java @@ -48,6 +48,7 @@ import com.vmware.vim25.GuestInfo; import com.vmware.vim25.HttpNfcLeaseDeviceUrl; import com.vmware.vim25.HttpNfcLeaseInfo; import com.vmware.vim25.HttpNfcLeaseState; +import com.vmware.vim25.InvalidStateFaultMsg; import com.vmware.vim25.ManagedObjectReference; import com.vmware.vim25.ObjectContent; import com.vmware.vim25.ObjectSpec; @@ -57,6 +58,7 @@ import com.vmware.vim25.OvfCreateDescriptorResult; import com.vmware.vim25.OvfFile; import com.vmware.vim25.PropertyFilterSpec; import com.vmware.vim25.PropertySpec; +import com.vmware.vim25.RuntimeFaultFaultMsg; import com.vmware.vim25.SelectionSpec; import com.vmware.vim25.TraversalSpec; import com.vmware.vim25.VirtualCdrom; @@ -335,6 +337,30 @@ public class VirtualMachineMO extends BaseMO { return false; } + public boolean changeDatastore(VirtualMachineRelocateSpec relocateSpec) throws Exception { + ManagedObjectReference morTask = _context.getVimClient().getService().relocateVMTask(_mor, relocateSpec, VirtualMachineMovePriority.DEFAULT_PRIORITY); + boolean result = _context.getVimClient().waitForTask(morTask); + if(result) { + _context.waitForTaskProgressDone(morTask); + return true; + } else { + s_logger.error("VMware RelocateVM_Task to change datastore failed due to " + TaskMO.getTaskFailureInfo(_context, morTask)); + } + return false; + } + + public boolean changeHost(VirtualMachineRelocateSpec relocateSpec) throws Exception { + ManagedObjectReference morTask = _context.getService().relocateVMTask(_mor, relocateSpec, VirtualMachineMovePriority.DEFAULT_PRIORITY); + boolean result = _context.getVimClient().waitForTask(morTask); + if (result) { + _context.waitForTaskProgressDone(morTask); + return true; + } else { + s_logger.error("VMware RelocateVM_Task to change host failed due to " + TaskMO.getTaskFailureInfo(_context, morTask)); + } + return false; + } + public boolean relocate(ManagedObjectReference morTargetHost) throws Exception { VirtualMachineRelocateSpec relocateSpec = new VirtualMachineRelocateSpec(); relocateSpec.setHost(morTargetHost);