diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageAnswer.java b/api/src/com/cloud/agent/api/MigrateWithStorageAnswer.java new file mode 100644 index 00000000000..06aff323bdc --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageAnswer.java @@ -0,0 +1,39 @@ +// 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 com.cloud.agent.api; + +import java.util.List; +import com.cloud.agent.api.to.VolumeTO; + +public class MigrateWithStorageAnswer extends Answer { + + List volumeTos; + + public MigrateWithStorageAnswer(MigrateWithStorageCommand cmd, Exception ex) { + super(cmd, ex); + volumeTos = null; + } + + public MigrateWithStorageAnswer(MigrateWithStorageCommand cmd, List volumeTos) { + super(cmd, true, null); + this.volumeTos = volumeTos; + } + + public List getVolumeTos() { + return volumeTos; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageCommand.java b/api/src/com/cloud/agent/api/MigrateWithStorageCommand.java new file mode 100644 index 00000000000..058aa15338e --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageCommand.java @@ -0,0 +1,45 @@ +// 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 com.cloud.agent.api; + +import java.util.Map; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.agent.api.to.StorageFilerTO; + +public class MigrateWithStorageCommand extends Command { + VirtualMachineTO vm; + Map volumeToFiler; + + public MigrateWithStorageCommand(VirtualMachineTO vm, Map volumeToFiler) { + this.vm = vm; + this.volumeToFiler = volumeToFiler; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public Map getVolumeToFiler() { + return volumeToFiler; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageCompleteAnswer.java b/api/src/com/cloud/agent/api/MigrateWithStorageCompleteAnswer.java new file mode 100644 index 00000000000..920cf48ca49 --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageCompleteAnswer.java @@ -0,0 +1,38 @@ +// 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 com.cloud.agent.api; + +import java.util.List; +import com.cloud.agent.api.to.VolumeTO; + +public class MigrateWithStorageCompleteAnswer extends Answer { + List volumeTos; + + public MigrateWithStorageCompleteAnswer(MigrateWithStorageCompleteCommand cmd, Exception ex) { + super(cmd, ex); + volumeTos = null; + } + + public MigrateWithStorageCompleteAnswer(MigrateWithStorageCompleteCommand cmd, List volumeTos) { + super(cmd, true, null); + this.volumeTos = volumeTos; + } + + public List getVolumeTos() { + return volumeTos; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageCompleteCommand.java b/api/src/com/cloud/agent/api/MigrateWithStorageCompleteCommand.java new file mode 100644 index 00000000000..1303c07c56f --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageCompleteCommand.java @@ -0,0 +1,36 @@ +// 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 com.cloud.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +public class MigrateWithStorageCompleteCommand extends Command { + VirtualMachineTO vm; + + public MigrateWithStorageCompleteCommand(VirtualMachineTO vm) { + this.vm = vm; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageReceiveAnswer.java b/api/src/com/cloud/agent/api/MigrateWithStorageReceiveAnswer.java new file mode 100644 index 00000000000..3bf521ce535 --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageReceiveAnswer.java @@ -0,0 +1,55 @@ +// 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 com.cloud.agent.api; + +import java.util.Map; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.agent.api.to.NicTO; + +public class MigrateWithStorageReceiveAnswer extends Answer { + + Map volumeToSr; + Map nicToNetwork; + Map token; + + public MigrateWithStorageReceiveAnswer(MigrateWithStorageReceiveCommand cmd, Exception ex) { + super(cmd, ex); + volumeToSr = null; + nicToNetwork = null; + token = null; + } + + public MigrateWithStorageReceiveAnswer(MigrateWithStorageReceiveCommand cmd, Map volumeToSr, + Map nicToNetwork, Map token) { + super(cmd, true, null); + this.volumeToSr = volumeToSr; + this.nicToNetwork = nicToNetwork; + this.token = token; + } + + public Map getVolumeToSr() { + return volumeToSr; + } + + public Map getNicToNetwork() { + return nicToNetwork; + } + + public Map getToken() { + return token; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageReceiveCommand.java b/api/src/com/cloud/agent/api/MigrateWithStorageReceiveCommand.java new file mode 100644 index 00000000000..df6740574a7 --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageReceiveCommand.java @@ -0,0 +1,45 @@ +// 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 com.cloud.agent.api; + +import java.util.Map; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.agent.api.to.StorageFilerTO; + +public class MigrateWithStorageReceiveCommand extends Command { + VirtualMachineTO vm; + Map volumeToFiler; + + public MigrateWithStorageReceiveCommand(VirtualMachineTO vm, Map volumeToFiler) { + this.vm = vm; + this.volumeToFiler = volumeToFiler; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public Map getVolumeToFiler() { + return volumeToFiler; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageSendAnswer.java b/api/src/com/cloud/agent/api/MigrateWithStorageSendAnswer.java new file mode 100644 index 00000000000..7cf641f7845 --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageSendAnswer.java @@ -0,0 +1,39 @@ +// 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 com.cloud.agent.api; + +import java.util.Set; +import com.cloud.agent.api.to.VolumeTO; + +public class MigrateWithStorageSendAnswer extends Answer { + + Set volumeToSet; + + public MigrateWithStorageSendAnswer(MigrateWithStorageSendCommand cmd, Exception ex) { + super(cmd, ex); + volumeToSet = null; + } + + public MigrateWithStorageSendAnswer(MigrateWithStorageSendCommand cmd, Set volumeToSet) { + super(cmd, true, null); + this.volumeToSet = volumeToSet; + } + + public Set getVolumeToSet() { + return volumeToSet; + } +} diff --git a/api/src/com/cloud/agent/api/MigrateWithStorageSendCommand.java b/api/src/com/cloud/agent/api/MigrateWithStorageSendCommand.java new file mode 100644 index 00000000000..d10db30effa --- /dev/null +++ b/api/src/com/cloud/agent/api/MigrateWithStorageSendCommand.java @@ -0,0 +1,58 @@ +// 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 com.cloud.agent.api; + +import java.util.Map; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.agent.api.to.NicTO; + +public class MigrateWithStorageSendCommand extends Command { + VirtualMachineTO vm; + Map volumeToSr; + Map nicToNetwork; + Map token; + + public MigrateWithStorageSendCommand(VirtualMachineTO vm, Map volumeToSr, + Map nicToNetwork, Map token) { + this.vm = vm; + this.volumeToSr = volumeToSr; + this.nicToNetwork = nicToNetwork; + this.token = token; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public Map getVolumeToSr() { + return volumeToSr; + } + + public Map getNicToNetwork() { + return nicToNetwork; + } + + public Map getToken() { + return token; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/api/src/com/cloud/agent/api/storage/MigrateVolumeAnswer.java b/api/src/com/cloud/agent/api/storage/MigrateVolumeAnswer.java new file mode 100644 index 00000000000..d5efa9527b5 --- /dev/null +++ b/api/src/com/cloud/agent/api/storage/MigrateVolumeAnswer.java @@ -0,0 +1,38 @@ +// 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class MigrateVolumeAnswer extends Answer { + private String volumePath; + + public MigrateVolumeAnswer(Command command, boolean success, String details, String volumePath) { + super(command, success, details); + this.volumePath = volumePath; + } + + public MigrateVolumeAnswer(Command command) { + super(command); + this.volumePath = null; + } + + public String getVolumePath() { + return volumePath; + } +} \ No newline at end of file diff --git a/api/src/com/cloud/agent/api/storage/MigrateVolumeCommand.java b/api/src/com/cloud/agent/api/storage/MigrateVolumeCommand.java new file mode 100644 index 00000000000..b82d8481f2c --- /dev/null +++ b/api/src/com/cloud/agent/api/storage/MigrateVolumeCommand.java @@ -0,0 +1,51 @@ +// 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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.storage.StoragePool; + +public class MigrateVolumeCommand extends Command { + + long volumeId; + String volumePath; + StorageFilerTO pool; + + public MigrateVolumeCommand(long volumeId, String volumePath, StoragePool pool) { + this.volumeId = volumeId; + this.volumePath = volumePath; + this.pool = new StorageFilerTO(pool); + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getVolumePath() { + return volumePath; + } + + public long getVolumeId() { + return volumeId; + } + + public StorageFilerTO getPool() { + return pool; + } +} \ No newline at end of file diff --git a/api/src/com/cloud/hypervisor/HypervisorCapabilities.java b/api/src/com/cloud/hypervisor/HypervisorCapabilities.java index aff81b0018d..c954750a12a 100644 --- a/api/src/com/cloud/hypervisor/HypervisorCapabilities.java +++ b/api/src/com/cloud/hypervisor/HypervisorCapabilities.java @@ -52,4 +52,6 @@ public interface HypervisorCapabilities extends Identity, InternalIdentity{ */ Integer getMaxHostsPerCluster(); + boolean isStorageMotionSupported(); + } diff --git a/api/src/com/cloud/server/ManagementService.java b/api/src/com/cloud/server/ManagementService.java index 460357b3c97..22494072648 100755 --- a/api/src/com/cloud/server/ManagementService.java +++ b/api/src/com/cloud/server/ManagementService.java @@ -75,9 +75,11 @@ import com.cloud.network.IpAddress; import com.cloud.org.Cluster; import com.cloud.storage.GuestOS; import com.cloud.storage.GuestOsCategory; +import com.cloud.storage.StoragePool; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.SSHKeyPair; import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; import com.cloud.vm.InstanceGroup; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; @@ -388,10 +390,21 @@ public interface ManagementService { * @param Long * vmId * Id of The VM to migrate - * @return Pair, List> List of all Hosts in VM's cluster and list of Hosts with - * enough capacity + * @return Ternary, List, Map> List of all Hosts to which a VM + * can be migrated, list of Hosts with enough capacity and hosts requiring storage motion for migration. */ - Pair, Integer>, List> listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize); + Ternary, Integer>, List, Map> listHostsForMigrationOfVM( + Long vmId, Long startIndex, Long pageSize); + + /** + * List storage pools for live migrating of a volume. The API returns list of all pools in the cluster to which the + * volume can be migrated. Current pool is not included in the list. + * + * @param Long volumeId + * @return Pair, List> List of storage pools in cluster and list + * of pools with enough capacity. + */ + Pair, List> listStoragePoolsForMigrationOfVolume(Long volumeId); String[] listEventTypes(); diff --git a/api/src/com/cloud/vm/UserVmService.java b/api/src/com/cloud/vm/UserVmService.java index d963b74b080..aa2113617bd 100755 --- a/api/src/com/cloud/vm/UserVmService.java +++ b/api/src/com/cloud/vm/UserVmService.java @@ -405,6 +405,33 @@ public interface UserVmService { */ VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException, VirtualMachineMigrationException; + /** + * Migrate the given VM with its volumes to the destination host. The API returns the migrated VM if it succeeds. + * Only root admin can migrate a VM. + * + * @param destinationStorage + * TODO + * @param Long + * vmId of The VM to migrate + * @param Host + * destinationHost to migrate the VM + * @param Map + * A map of volume to which pool it should be migrated + * + * @return VirtualMachine migrated VM + * @throws ManagementServerException + * in case we get error finding the VM or host or access errors or other internal errors. + * @throws ConcurrentOperationException + * if there are multiple users working on the same VM. + * @throws ResourceUnavailableException + * if the destination host to migrate the VM is not currently available. + * @throws VirtualMachineMigrationException + * if the VM to be migrated is not in Running state + */ + VirtualMachine migrateVirtualMachineWithVolume(Long vmId, Host destinationHost, Map volumeToPool) + throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException, + VirtualMachineMigrationException; + UserVm moveVMToUser(AssignVMCmd moveUserVMCmd) throws ResourceAllocationException, ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; VirtualMachine vmStorageMigration(Long vmId, StoragePool destPool); diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 8c32bb383cd..edaaeb3700c 100755 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -365,6 +365,8 @@ public class ApiConstants { public static final String HA_HOST = "hahost"; public static final String CUSTOM_DISK_OFF_MAX_SIZE = "customdiskofferingmaxsize"; public static final String DEFAULT_ZONE_ID = "defaultzoneid"; + public static final String LIVE_MIGRATE = "livemigrate"; + public static final String MIGRATE_TO = "migrateto"; public static final String GUID = "guid"; public static final String VSWITCH_TYPE_GUEST_TRAFFIC = "guestvswitchtype"; public static final String VSWITCH_TYPE_PUBLIC_TRAFFIC = "publicvswitchtype"; diff --git a/api/src/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/org/apache/cloudstack/api/ResponseGenerator.java index c0dd57e15bd..a3aa9de0e3e 100644 --- a/api/src/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/org/apache/cloudstack/api/ResponseGenerator.java @@ -189,6 +189,10 @@ public interface ResponseGenerator { HostResponse createHostResponse(Host host); + HostForMigrationResponse createHostForMigrationResponse(Host host); + + HostForMigrationResponse createHostForMigrationResponse(Host host, EnumSet details); + VlanIpRangeResponse createVlanIpRangeResponse(Vlan vlan); IPAddressResponse createIPAddressResponse(IpAddress ipAddress); @@ -216,6 +220,8 @@ public interface ResponseGenerator { StoragePoolResponse createStoragePoolResponse(StoragePool pool); + StoragePoolForMigrationResponse createStoragePoolForMigrationResponse(StoragePool pool); + ClusterResponse createClusterResponse(Cluster cluster, Boolean showCapacities); FirewallRuleResponse createPortForwardingRuleResponse(PortForwardingRule fwRule); diff --git a/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java b/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java new file mode 100644 index 00000000000..e6e45cc7246 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java @@ -0,0 +1,107 @@ +// 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.api.command.admin.host; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.api.APICommand; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.HostForMigrationResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import com.cloud.host.Host; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; + +@APICommand(name = "findHostsForMigration", description="Find hosts suitable for migrating a virtual machine.", + responseObject=HostForMigrationResponse.class) +public class FindHostsForMigrationCmd extends BaseListCmd { + public static final Logger s_logger = Logger.getLogger(FindHostsForMigrationCmd.class.getName()); + + private static final String s_name = "findhostsformigrationresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name=ApiConstants.VIRTUAL_MACHINE_ID, type=CommandType.UUID, entityType = UserVmResponse.class, + required=false, description="find hosts to which this VM can be migrated and flag the hosts with enough " + + "CPU/RAM to host the VM") + private Long virtualMachineId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVirtualMachineId() { + return virtualMachineId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public void execute() { + ListResponse response = null; + Pair,Integer> result; + List hostsWithCapacity = new ArrayList(); + Map hostsRequiringStorageMotion; + + Ternary,Integer>, List, Map> hostsForMigration = + _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal()); + result = hostsForMigration.first(); + hostsWithCapacity = hostsForMigration.second(); + hostsRequiringStorageMotion = hostsForMigration.third(); + + response = new ListResponse(); + List hostResponses = new ArrayList(); + for (Host host : result.first()) { + HostForMigrationResponse hostResponse = _responseGenerator.createHostForMigrationResponse(host); + Boolean suitableForMigration = false; + if (hostsWithCapacity.contains(host)) { + suitableForMigration = true; + } + hostResponse.setSuitableForMigration(suitableForMigration); + + Boolean requiresStorageMotion = hostsRequiringStorageMotion.get(host); + if (requiresStorageMotion != null && requiresStorageMotion) { + hostResponse.setRequiresStorageMotion(true); + } else { + hostResponse.setRequiresStorageMotion(false); + } + + hostResponse.setObjectName("host"); + hostResponses.add(hostResponse); + } + + response.setResponses(hostResponses, result.second()); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java b/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java index 29844c31113..5ec7cf3e10b 100644 --- a/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java +++ b/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.api.command.admin.host; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.Map; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -37,6 +38,7 @@ import com.cloud.async.AsyncJob; import com.cloud.exception.InvalidParameterValueException; import com.cloud.host.Host; import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; @APICommand(name = "listHosts", description="Lists hosts.", responseObject=HostResponse.class) public class ListHostsCmd extends BaseListCmd { @@ -170,8 +172,8 @@ public class ListHostsCmd extends BaseListCmd { } else { Pair,Integer> result; List hostsWithCapacity = new ArrayList(); - - Pair,Integer>, List> hostsForMigration = _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal()); + Ternary,Integer>, List, Map> hostsForMigration = + _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal()); result = hostsForMigration.first(); hostsWithCapacity = hostsForMigration.second(); @@ -192,6 +194,5 @@ public class ListHostsCmd extends BaseListCmd { } response.setResponseName(getCommandName()); this.setResponseObject(response); - } } diff --git a/api/src/org/apache/cloudstack/api/command/admin/storage/FindStoragePoolsForMigrationCmd.java b/api/src/org/apache/cloudstack/api/command/admin/storage/FindStoragePoolsForMigrationCmd.java new file mode 100644 index 00000000000..37d007c0376 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/storage/FindStoragePoolsForMigrationCmd.java @@ -0,0 +1,98 @@ +// 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.api.command.admin.storage; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.api.APICommand; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.StoragePoolForMigrationResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import com.cloud.async.AsyncJob; +import com.cloud.storage.StoragePool; +import com.cloud.utils.Pair; + +@APICommand(name = "findStoragePoolsForMigration", description="Lists storage pools available for migration of a volume.", + responseObject=StoragePoolForMigrationResponse.class) +public class FindStoragePoolsForMigrationCmd extends BaseListCmd { + public static final Logger s_logger = Logger.getLogger(FindStoragePoolsForMigrationCmd.class.getName()); + + private static final String s_name = "findstoragepoolsformigrationresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name=ApiConstants.ID, type=CommandType.UUID, entityType = VolumeResponse.class, required=true, + description="the ID of the volume") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + public AsyncJob.Type getInstanceType() { + return AsyncJob.Type.StoragePool; + } + + @Override + public void execute() { + Pair, List> pools = + _mgr.listStoragePoolsForMigrationOfVolume(getId()); + ListResponse response = new ListResponse(); + List poolResponses = new ArrayList(); + + List allPools = pools.first(); + List suitablePoolList = pools.second(); + for (StoragePool pool : allPools) { + StoragePoolForMigrationResponse poolResponse = _responseGenerator.createStoragePoolForMigrationResponse(pool); + Boolean suitableForMigration = false; + for (StoragePool suitablePool : suitablePoolList) { + if (suitablePool.getId() == pool.getId()) { + suitableForMigration = true; + break; + } + } + poolResponse.setSuitableForMigration(suitableForMigration); + poolResponse.setObjectName("storagepool"); + poolResponses.add(poolResponse); + } + + response.setResponses(poolResponses); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java b/api/src/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java new file mode 100644 index 00000000000..b1eaf11a251 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java @@ -0,0 +1,160 @@ +// 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.api.command.admin.vm; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.cloudstack.api.*; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.APICommand; + +import org.apache.cloudstack.api.BaseCmd.CommandType; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ManagementServerException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.exception.VirtualMachineMigrationException; +import com.cloud.host.Host; +import com.cloud.storage.StoragePool; +import com.cloud.user.Account; +import com.cloud.user.UserContext; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; + +@APICommand(name = "migrateVirtualMachineWithVolume", description="Attempts Migration of a VM with its volumes to a different host", responseObject=UserVmResponse.class) +public class MigrateVirtualMachineWithVolumeCmd extends BaseAsyncCmd { + public static final Logger s_logger = Logger.getLogger(MigrateVMCmd.class.getName()); + + private static final String s_name = "migratevirtualmachinewithvolumeresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name=ApiConstants.HOST_ID, type=CommandType.UUID, entityType=HostResponse.class, + required=true, description="Destination Host ID to migrate VM to.") + private Long hostId; + + @Parameter(name=ApiConstants.VIRTUAL_MACHINE_ID, type=CommandType.UUID, entityType=UserVmResponse.class, + required=true, description="the ID of the virtual machine") + private Long virtualMachineId; + + @Parameter(name = ApiConstants.MIGRATE_TO, type = CommandType.MAP, required=false, + description = "Map of pool to which each volume should be migrated (volume/pool pair)") + private Map migrateVolumeTo; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getHostId() { + return hostId; + } + + public Long getVirtualMachineId() { + return virtualMachineId; + } + + public Map getVolumeToPool() { + Map volumeToPoolMap = new HashMap(); + if (migrateVolumeTo != null && !migrateVolumeTo.isEmpty()) { + Collection allValues = migrateVolumeTo.values(); + Iterator iter = allValues.iterator(); + while (iter.hasNext()) { + HashMap volumeToPool = (HashMap) iter.next(); + String volume = volumeToPool.get("volume"); + String pool = volumeToPool.get("pool"); + volumeToPoolMap.put(volume, pool); + } + } + return volumeToPoolMap; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + UserVm userVm = _entityMgr.findById(UserVm.class, getVirtualMachineId()); + if (userVm != null) { + return userVm.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_MIGRATE; + } + + @Override + public String getEventDescription() { + return "Attempting to migrate VM Id: " + getVirtualMachineId() + " to host Id: "+ getHostId(); + } + + @Override + public void execute(){ + UserVm userVm = _userVmService.getUserVm(getVirtualMachineId()); + if (userVm == null) { + throw new InvalidParameterValueException("Unable to find the VM by id=" + getVirtualMachineId()); + } + + Host destinationHost = _resourceService.getHost(getHostId()); + if (destinationHost == null) { + throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id =" + getHostId()); + } + + try{ + VirtualMachine migratedVm = _userVmService.migrateVirtualMachineWithVolume(getVirtualMachineId(), + destinationHost, getVolumeToPool()); + if (migratedVm != null) { + UserVmResponse response = _responseGenerator.createUserVmResponse("virtualmachine", (UserVm)migratedVm).get(0); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to migrate vm"); + } + } catch (ResourceUnavailableException ex) { + s_logger.warn("Exception: ", ex); + throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage()); + } catch (ConcurrentOperationException e) { + s_logger.warn("Exception: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } catch (ManagementServerException e) { + s_logger.warn("Exception: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } catch (VirtualMachineMigrationException e) { + s_logger.warn("Exception: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } +} diff --git a/api/src/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java b/api/src/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java index 287241a8d90..ce40f0d2979 100644 --- a/api/src/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java @@ -47,6 +47,10 @@ public class MigrateVolumeCmd extends BaseAsyncCmd { required=true, description="destination storage pool ID to migrate the volume to") private Long storageId; + @Parameter(name=ApiConstants.LIVE_MIGRATE, type=CommandType.BOOLEAN, required=false, + description="if the volume should be live migrated when it is attached to a running vm") + private Boolean liveMigrate; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -58,6 +62,10 @@ public class MigrateVolumeCmd extends BaseAsyncCmd { public Long getStoragePoolId() { return storageId; } + + public boolean isLiveMigrate() { + return (liveMigrate != null) ? liveMigrate : false; + } ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/org/apache/cloudstack/api/response/HostForMigrationResponse.java b/api/src/org/apache/cloudstack/api/response/HostForMigrationResponse.java new file mode 100644 index 00000000000..fde2440a8e2 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/HostForMigrationResponse.java @@ -0,0 +1,365 @@ +// 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.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import com.cloud.host.Host; +import com.cloud.host.Status; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value=Host.class) +public class HostForMigrationResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) @Param(description="the ID of the host") + private String id; + + @SerializedName(ApiConstants.NAME) @Param(description="the name of the host") + private String name; + + @SerializedName(ApiConstants.STATE) @Param(description="the state of the host") + private Status state; + + @SerializedName("disconnected") @Param(description="true if the host is disconnected. False otherwise.") + private Date disconnectedOn; + + @SerializedName(ApiConstants.TYPE) @Param(description="the host type") + private Host.Type hostType; + + @SerializedName("oscategoryid") @Param(description="the OS category ID of the host") + private String osCategoryId; + + @SerializedName("oscategoryname") @Param(description="the OS category name of the host") + private String osCategoryName; + + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description="the IP address of the host") + private String ipAddress; + + @SerializedName(ApiConstants.ZONE_ID) @Param(description="the Zone ID of the host") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) @Param(description="the Zone name of the host") + private String zoneName; + + @SerializedName(ApiConstants.POD_ID) @Param(description="the Pod ID of the host") + private String podId; + + @SerializedName("podname") @Param(description="the Pod name of the host") + private String podName; + + @SerializedName("version") @Param(description="the host version") + private String version; + + @SerializedName(ApiConstants.HYPERVISOR) @Param(description="the host hypervisor") + private HypervisorType hypervisor; + + @SerializedName("cpunumber") @Param(description="the CPU number of the host") + private Integer cpuNumber; + + @SerializedName("cpuspeed") @Param(description="the CPU speed of the host") + private Long cpuSpeed; + + @SerializedName("cpuallocated") @Param(description="the amount of the host's CPU currently allocated") + private String cpuAllocated; + + @SerializedName("cpuused") @Param(description="the amount of the host's CPU currently used") + private String cpuUsed; + + @SerializedName("cpuwithoverprovisioning") @Param(description="the amount of the host's CPU after applying the cpu.overprovisioning.factor ") + private String cpuWithOverprovisioning; + + @SerializedName("averageload") @Param(description="the cpu average load on the host") + private Long averageLoad; + + @SerializedName("networkkbsread") @Param(description="the incoming network traffic on the host") + private Long networkKbsRead; + + @SerializedName("networkkbswrite") @Param(description="the outgoing network traffic on the host") + private Long networkKbsWrite; + + @SerializedName("memorytotal") @Param(description="the memory total of the host") + private Long memoryTotal; + + @SerializedName("memoryallocated") @Param(description="the amount of the host's memory currently allocated") + private Long memoryAllocated; + + @SerializedName("memoryused") @Param(description="the amount of the host's memory currently used") + private Long memoryUsed; + + @SerializedName("disksizetotal") @Param(description="the total disk size of the host") + private Long diskSizeTotal; + + @SerializedName("disksizeallocated") @Param(description="the host's currently allocated disk size") + private Long diskSizeAllocated; + + @SerializedName("capabilities") @Param(description="capabilities of the host") + private String capabilities; + + @SerializedName("lastpinged") @Param(description="the date and time the host was last pinged") + private Date lastPinged; + + @SerializedName("managementserverid") @Param(description="the management server ID of the host") + private Long managementServerId; + + @SerializedName("clusterid") @Param(description="the cluster ID of the host") + private String clusterId; + + @SerializedName("clustername") @Param(description="the cluster name of the host") + private String clusterName; + + @SerializedName("clustertype") @Param(description="the cluster type of the cluster that host belongs to") + private String clusterType; + + @SerializedName("islocalstorageactive") @Param(description="true if local storage is active, false otherwise") + private Boolean localStorageActive; + + @SerializedName(ApiConstants.CREATED) @Param(description="the date and time the host was created") + private Date created; + + @SerializedName("removed") @Param(description="the date and time the host was removed") + private Date removed; + + @SerializedName("events") @Param(description="events available for the host") + private String events; + + @SerializedName("hosttags") @Param(description="comma-separated list of tags for the host") + private String hostTags; + + @SerializedName("hasenoughcapacity") @Param(description="true if this host has enough CPU and RAM capacity to migrate a VM to it, false otherwise") + private Boolean hasEnoughCapacity; + + @SerializedName("suitableformigration") @Param(description="true if this host is suitable(has enough capacity and satisfies all conditions like hosttags, max guests vm limit etc) to migrate a VM to it , false otherwise") + private Boolean suitableForMigration; + + @SerializedName("requiresStorageMotion") @Param(description="true if migrating a vm to this host requires storage motion, false otherwise") + private Boolean requiresStorageMotion; + + @SerializedName("resourcestate") @Param(description="the resource state of the host") + private String resourceState; + + @SerializedName(ApiConstants.HYPERVISOR_VERSION) @Param(description="the hypervisor version") + private String hypervisorVersion; + + @SerializedName(ApiConstants.HA_HOST) @Param(description="true if the host is Ha host (dedicated to vms started by HA process; false otherwise") + private Boolean haHost; + + @Override + public String getObjectId() { + return this.getId(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setState(Status state) { + this.state = state; + } + + public void setDisconnectedOn(Date disconnectedOn) { + this.disconnectedOn = disconnectedOn; + } + + public void setHostType(Host.Type hostType) { + this.hostType = hostType; + } + + public void setOsCategoryId(String osCategoryId) { + this.osCategoryId = osCategoryId; + } + + public void setOsCategoryName(String osCategoryName) { + this.osCategoryName = osCategoryName; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public void setPodId(String podId) { + this.podId = podId; + } + + public void setPodName(String podName) { + this.podName = podName; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setHypervisor(HypervisorType hypervisor) { + this.hypervisor = hypervisor; + } + + public void setCpuNumber(Integer cpuNumber) { + this.cpuNumber = cpuNumber; + } + + public void setCpuSpeed(Long cpuSpeed) { + this.cpuSpeed = cpuSpeed; + } + + public String getCpuAllocated() { + return cpuAllocated; + } + + public void setCpuAllocated(String cpuAllocated) { + this.cpuAllocated = cpuAllocated; + } + + public void setCpuUsed(String cpuUsed) { + this.cpuUsed = cpuUsed; + } + + public void setAverageLoad(Long averageLoad) { + this.averageLoad = averageLoad; + } + + public void setNetworkKbsRead(Long networkKbsRead) { + this.networkKbsRead = networkKbsRead; + } + + public void setNetworkKbsWrite(Long networkKbsWrite) { + this.networkKbsWrite = networkKbsWrite; + } + + public void setMemoryTotal(Long memoryTotal) { + this.memoryTotal = memoryTotal; + } + + public void setMemoryAllocated(Long memoryAllocated) { + this.memoryAllocated = memoryAllocated; + } + + public void setMemoryUsed(Long memoryUsed) { + this.memoryUsed = memoryUsed; + } + + public void setDiskSizeTotal(Long diskSizeTotal) { + this.diskSizeTotal = diskSizeTotal; + } + + public void setDiskSizeAllocated(Long diskSizeAllocated) { + this.diskSizeAllocated = diskSizeAllocated; + } + + public void setCapabilities(String capabilities) { + this.capabilities = capabilities; + } + + public void setLastPinged(Date lastPinged) { + this.lastPinged = lastPinged; + } + + public void setManagementServerId(Long managementServerId) { + this.managementServerId = managementServerId; + } + + public void setClusterId(String clusterId) { + this.clusterId = clusterId; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public void setClusterType(String clusterType) { + this.clusterType = clusterType; + } + + public void setLocalStorageActive(Boolean localStorageActive) { + this.localStorageActive = localStorageActive; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public void setEvents(String events) { + this.events = events; + } + + public String getHostTags() { + return hostTags; + } + + public void setHostTags(String hostTags) { + this.hostTags = hostTags; + } + + public void setHasEnoughCapacity(Boolean hasEnoughCapacity) { + this.hasEnoughCapacity = hasEnoughCapacity; + } + + public void setSuitableForMigration(Boolean suitableForMigration) { + this.suitableForMigration = suitableForMigration; + } + + public void setRequiresStorageMotion(Boolean requiresStorageMotion) { + this.requiresStorageMotion = requiresStorageMotion; + } + + public String getResourceState() { + return resourceState; + } + + public void setResourceState(String resourceState) { + this.resourceState = resourceState; + } + + public String getCpuWithOverprovisioning() { + return cpuWithOverprovisioning; + } + + public void setCpuWithOverprovisioning(String cpuWithOverprovisioning) { + this.cpuWithOverprovisioning = cpuWithOverprovisioning; + } + + public void setHypervisorVersion(String hypervisorVersion) { + this.hypervisorVersion = hypervisorVersion; + } + + public void setHaHost(Boolean haHost) { + this.haHost = haHost; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/HostResponse.java b/api/src/org/apache/cloudstack/api/response/HostResponse.java index f5aa8f90a17..687687d37fc 100644 --- a/api/src/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/org/apache/cloudstack/api/response/HostResponse.java @@ -330,7 +330,6 @@ public class HostResponse extends BaseResponse { this.hasEnoughCapacity = hasEnoughCapacity; } - public void setSuitableForMigration(Boolean suitableForMigration) { this.suitableForMigration = suitableForMigration; } diff --git a/api/src/org/apache/cloudstack/api/response/StoragePoolForMigrationResponse.java b/api/src/org/apache/cloudstack/api/response/StoragePoolForMigrationResponse.java new file mode 100644 index 00000000000..f0bbcb19136 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/StoragePoolForMigrationResponse.java @@ -0,0 +1,248 @@ +// 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.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import com.cloud.serializer.Param; +import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolStatus; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value=StoragePool.class) +public class StoragePoolForMigrationResponse extends BaseResponse { + @SerializedName("id") @Param(description="the ID of the storage pool") + private String id; + + @SerializedName("zoneid") @Param(description="the Zone ID of the storage pool") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) @Param(description="the Zone name of the storage pool") + private String zoneName; + + @SerializedName("podid") @Param(description="the Pod ID of the storage pool") + private String podId; + + @SerializedName("podname") @Param(description="the Pod name of the storage pool") + private String podName; + + @SerializedName("name") @Param(description="the name of the storage pool") + private String name; + + @SerializedName("ipaddress") @Param(description="the IP address of the storage pool") + private String ipAddress; + + @SerializedName("path") @Param(description="the storage pool path") + private String path; + + @SerializedName("created") @Param(description="the date and time the storage pool was created") + private Date created; + + @SerializedName("type") @Param(description="the storage pool type") + private String type; + + @SerializedName("clusterid") @Param(description="the ID of the cluster for the storage pool") + private String clusterId; + + @SerializedName("clustername") @Param(description="the name of the cluster for the storage pool") + private String clusterName; + + @SerializedName("disksizetotal") @Param(description="the total disk size of the storage pool") + private Long diskSizeTotal; + + @SerializedName("disksizeallocated") @Param(description="the host's currently allocated disk size") + private Long diskSizeAllocated; + + @SerializedName("disksizeused") @Param(description="the host's currently used disk size") + private Long diskSizeUsed; + + @SerializedName("tags") @Param(description="the tags for the storage pool") + private String tags; + + @SerializedName(ApiConstants.STATE) @Param(description="the state of the storage pool") + private StoragePoolStatus state; + + @SerializedName(ApiConstants.SCOPE) @Param(description="the scope of the storage pool") + private String scope; + + @SerializedName("suitableformigration") @Param(description="true if this pool is suitable to migrate a volume," + + " false otherwise") + private Boolean suitableForMigration; + + /** + * @return the scope + */ + public String getScope() { + return scope; + } + + /** + * @param scope the scope to set + */ + public void setScope(String scope) { + this.scope = scope; + } + + @Override + public String getObjectId() { + return this.getId(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getZoneId() { + return zoneId; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public String getZoneName() { + return zoneName; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public String getPodId() { + return podId; + } + + public void setPodId(String podId) { + this.podId = podId; + } + + public String getPodName() { + return podName; + } + + public void setPodName(String podName) { + this.podName = podName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getClusterId() { + return clusterId; + } + + public void setClusterId(String clusterId) { + this.clusterId = clusterId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public Long getDiskSizeTotal() { + return diskSizeTotal; + } + + public void setDiskSizeTotal(Long diskSizeTotal) { + this.diskSizeTotal = diskSizeTotal; + } + + public Long getDiskSizeAllocated() { + return diskSizeAllocated; + } + + public void setDiskSizeAllocated(Long diskSizeAllocated) { + this.diskSizeAllocated = diskSizeAllocated; + } + + public Long getDiskSizeUsed() { + return diskSizeUsed; + } + + public void setDiskSizeUsed(Long diskSizeUsed) { + this.diskSizeUsed = diskSizeUsed; + } + + public String getTags() { + return tags; + } + + public void setTags(String tags) { + this.tags = tags; + } + + public StoragePoolStatus getState() { + return state; + } + + public void setState(StoragePoolStatus state) { + this.state = state; + } + + public void setSuitableForMigration(Boolean suitableForMigration) { + this.suitableForMigration = suitableForMigration; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/StoragePoolResponse.java b/api/src/org/apache/cloudstack/api/response/StoragePoolResponse.java index 0b1622640d0..e034b17e8ea 100644 --- a/api/src/org/apache/cloudstack/api/response/StoragePoolResponse.java +++ b/api/src/org/apache/cloudstack/api/response/StoragePoolResponse.java @@ -83,8 +83,6 @@ public class StoragePoolResponse extends BaseResponse { @SerializedName(ApiConstants.SCOPE) @Param(description="the scope of the storage pool") private String scope; - - /** * @return the scope */ @@ -239,5 +237,4 @@ public class StoragePoolResponse extends BaseResponse { public void setState(StoragePoolStatus state) { this.state = state; } - } diff --git a/client/tomcatconf/applicationContext.xml.in b/client/tomcatconf/applicationContext.xml.in index 7487a5ebbfc..d2ea380ddab 100644 --- a/client/tomcatconf/applicationContext.xml.in +++ b/client/tomcatconf/applicationContext.xml.in @@ -714,6 +714,7 @@ + diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in index 10fcfe3f687..b49e1fbf5ff 100644 --- a/client/tomcatconf/commands.properties.in +++ b/client/tomcatconf/commands.properties.in @@ -69,6 +69,7 @@ changeServiceForVirtualMachine=15 scaleVirtualMachine=15 assignVirtualMachine=1 migrateVirtualMachine=1 +migrateVirtualMachineWithVolume=1 recoverVirtualMachine=7 #### snapshot commands @@ -254,6 +255,7 @@ deleteHost=3 prepareHostForMaintenance=1 cancelHostMaintenance=1 listHosts=3 +findHostsForMigration=1 addSecondaryStorage=1 updateHostPassword=1 @@ -288,6 +290,7 @@ deleteStoragePool=1 listClusters=3 enableStorageMaintenance=1 cancelStorageMaintenance=1 +findStoragePoolsForMigration=1 #### security group commands createSecurityGroup=15 diff --git a/core/src/com/cloud/hypervisor/HypervisorCapabilitiesVO.java b/core/src/com/cloud/hypervisor/HypervisorCapabilitiesVO.java index fafc0a33af0..66890668387 100644 --- a/core/src/com/cloud/hypervisor/HypervisorCapabilitiesVO.java +++ b/core/src/com/cloud/hypervisor/HypervisorCapabilitiesVO.java @@ -64,16 +64,21 @@ public class HypervisorCapabilitiesVO implements HypervisorCapabilities { @Column(name="vm_snapshot_enabled") private Boolean vmSnapshotEnabled; - + + @Column(name="storage_motion_supported") + private boolean storageMotionSupported; + protected HypervisorCapabilitiesVO() { this.uuid = UUID.randomUUID().toString(); } - public HypervisorCapabilitiesVO(HypervisorType hypervisorType, String hypervisorVersion, Long maxGuestsLimit, boolean securityGroupEnabled) { + public HypervisorCapabilitiesVO(HypervisorType hypervisorType, String hypervisorVersion, Long maxGuestsLimit, + boolean securityGroupEnabled, boolean storageMotionSupported) { this.hypervisorType = hypervisorType; this.hypervisorVersion = hypervisorVersion; this.maxGuestsLimit = maxGuestsLimit; this.securityGroupEnabled = securityGroupEnabled; + this.storageMotionSupported = storageMotionSupported; this.uuid = UUID.randomUUID().toString(); } @@ -135,6 +140,21 @@ public class HypervisorCapabilitiesVO implements HypervisorCapabilities { return maxGuestsLimit; } + /** + * @param storageMotionSupported + */ + public void setStorageMotionSupported(boolean storageMotionSupported) { + this.storageMotionSupported = storageMotionSupported; + } + + /** + * @return if storage motion is supported + */ + @Override + public boolean isStorageMotionSupported() { + return storageMotionSupported; + } + public long getId() { return id; diff --git a/engine/api/src/org/apache/cloudstack/engine/subsystem/api/storage/ObjectInDataStoreStateMachine.java b/engine/api/src/org/apache/cloudstack/engine/subsystem/api/storage/ObjectInDataStoreStateMachine.java index f619ef4e976..94ae800ab8e 100644 --- a/engine/api/src/org/apache/cloudstack/engine/subsystem/api/storage/ObjectInDataStoreStateMachine.java +++ b/engine/api/src/org/apache/cloudstack/engine/subsystem/api/storage/ObjectInDataStoreStateMachine.java @@ -28,6 +28,7 @@ public interface ObjectInDataStoreStateMachine extends StateObject createVolumeFromTemplateAsync(VolumeInfo volume, long dataStoreId, TemplateInfo template); AsyncCallFuture copyVolume(VolumeInfo srcVolume, DataStore destStore); + AsyncCallFuture migrateVolume(VolumeInfo srcVolume, DataStore destStore); + AsyncCallFuture migrateVolumes(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost); boolean destroyVolume(long volumeId) throws ConcurrentOperationException; diff --git a/engine/storage/imagemotion/src/org/apache/cloudstack/storage/image/motion/DefaultImageMotionStrategy.java b/engine/storage/imagemotion/src/org/apache/cloudstack/storage/image/motion/DefaultImageMotionStrategy.java index a70fd8ab227..1c21496871f 100644 --- a/engine/storage/imagemotion/src/org/apache/cloudstack/storage/image/motion/DefaultImageMotionStrategy.java +++ b/engine/storage/imagemotion/src/org/apache/cloudstack/storage/image/motion/DefaultImageMotionStrategy.java @@ -18,12 +18,15 @@ */ package org.apache.cloudstack.storage.image.motion; +import java.util.Map; + 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.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.framework.async.AsyncRpcConext; @@ -33,6 +36,8 @@ import org.apache.cloudstack.storage.endpoint.EndPointSelector; import org.apache.cloudstack.storage.volume.TemplateOnPrimaryDataStoreInfo; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; //At least one of datastore is coming from image store or image cache store @@ -95,6 +100,11 @@ public class DefaultImageMotionStrategy implements ImageMotionStrategy { return false; } + @Override + public boolean canHandle(Map volumeMap, Host srcHost, Host destHost) { + return false; + } + @Override public Void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) { @@ -137,4 +147,12 @@ public class DefaultImageMotionStrategy implements ImageMotionStrategy { } + @Override + public Void copyAsync(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, + AsyncCompletionCallback callback) { + CopyCommandResult result = new CopyCommandResult("", null); + result.setResult("not implemented"); + callback.complete(result); + return null; + } } diff --git a/engine/storage/integration-test/test/org/apache/cloudstack/storage/test/MockStorageMotionStrategy.java b/engine/storage/integration-test/test/org/apache/cloudstack/storage/test/MockStorageMotionStrategy.java index b619ee9240f..a84f3087d59 100644 --- a/engine/storage/integration-test/test/org/apache/cloudstack/storage/test/MockStorageMotionStrategy.java +++ b/engine/storage/integration-test/test/org/apache/cloudstack/storage/test/MockStorageMotionStrategy.java @@ -18,11 +18,18 @@ */ package org.apache.cloudstack.storage.test; +import java.util.Map; + 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.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.storage.motion.DataMotionStrategy; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; + public class MockStorageMotionStrategy implements DataMotionStrategy { @Override @@ -31,6 +38,11 @@ public class MockStorageMotionStrategy implements DataMotionStrategy { return true; } + @Override + public boolean canHandle(Map volumeMap, Host srcHost, Host destHost) { + return true; + } + @Override public Void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) { @@ -39,4 +51,11 @@ public class MockStorageMotionStrategy implements DataMotionStrategy { return null; } + @Override + public Void copyAsync(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, + AsyncCompletionCallback callback) { + CopyCommandResult result = new CopyCommandResult("something", null); + callback.complete(result); + return null; + } } diff --git a/engine/storage/src/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/src/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index 3602bb16baf..ad9238a937d 100644 --- a/engine/storage/src/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/src/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -20,12 +20,14 @@ package org.apache.cloudstack.storage.motion; import java.util.Date; import java.util.List; +import java.util.Map; 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.DataObjectType; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreRole; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; @@ -36,6 +38,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; 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.BackupSnapshotAnswer; import com.cloud.agent.api.BackupSnapshotCommand; @@ -47,15 +50,21 @@ import com.cloud.agent.api.CreateVolumeFromSnapshotCommand; import com.cloud.agent.api.UpgradeSnapshotCommand; import com.cloud.agent.api.storage.CopyVolumeAnswer; import com.cloud.agent.api.storage.CopyVolumeCommand; +import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.storage.MigrateVolumeCommand; import com.cloud.agent.api.storage.CreateAnswer; import com.cloud.agent.api.storage.CreateCommand; import com.cloud.agent.api.storage.CreatePrivateTemplateAnswer; import com.cloud.agent.api.to.S3TO; import com.cloud.agent.api.to.StorageFilerTO; import com.cloud.agent.api.to.SwiftTO; +import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.configuration.Config; import com.cloud.configuration.dao.ConfigurationDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.storage.DiskOfferingVO; @@ -86,6 +95,8 @@ import com.cloud.utils.db.DB; import com.cloud.utils.db.Transaction; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.DiskProfile; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; @Component public class AncientDataMotionStrategy implements DataMotionStrategy { @@ -102,8 +113,12 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { @Inject StorageManager storageMgr; @Inject + AgentManager agentMgr; + @Inject VolumeDao volDao; @Inject + VMInstanceDao instanceDao; + @Inject VMTemplateDao templateDao; @Inject SnapshotManager snapshotMgr; @@ -130,6 +145,11 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { return true; } + @Override + public boolean canHandle(Map volumeMap, Host srcHost, Host destHost) { + return false; + } + @DB protected Answer copyVolumeFromImage(DataObject srcData, DataObject destData) { String value = configDao.getValue(Config.RecreateSystemVmEnabled.key()); @@ -393,6 +413,53 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { return cvAnswer; } + protected Answer migrateVolumeToPool(DataObject srcData, DataStore destStore) { + VolumeInfo volume = (VolumeInfo)srcData; + Long instanceId = volume.getInstanceId(); + StoragePool destPool = (StoragePool)this.dataStoreMgr.getDataStore(destStore.getId(), DataStoreRole.Primary); + MigrateVolumeAnswer answer = null; + VMInstanceVO vmInstance = null; + if (instanceId != null) { + vmInstance = instanceDao.findById(instanceId); + } + + Long hostId = null; + if (vmInstance != null) { + hostId = vmInstance.getHostId(); + } + + try { + if (hostId != null) { + MigrateVolumeCommand command = new MigrateVolumeCommand(volume.getId(), volume.getPath(), destPool); + answer = (MigrateVolumeAnswer) this.agentMgr.send(hostId, command); + } + } catch (OperationTimedoutException e) { + s_logger.error("Operation timed out on storage motion for volume " + volume, e); + throw new CloudRuntimeException("Failed to live migrate volume " + volume + " to storage pool " + + destPool, e); + } catch (AgentUnavailableException e) { + s_logger.error("Agent unavailable exception while doing storage motion for volume " + volume, e); + throw new CloudRuntimeException("Failed to live migrate volume " + volume + " to storage pool " + + destPool, e); + } + + if (answer == null || !answer.getResult()) { + throw new CloudRuntimeException("Failed to migrate volume " + volume + " to storage pool " + destPool); + } else { + // Update the volume details after migration. + VolumeVO volumeVo = this.volDao.findById(volume.getId()); + Long oldPoolId = volume.getPoolId(); + volumeVo.setPath(answer.getVolumePath()); + volumeVo.setFolder(destPool.getPath()); + volumeVo.setPodId(destPool.getPodId()); + volumeVo.setPoolId(destPool.getId()); + volumeVo.setLastPoolId(oldPoolId); + this.volDao.update(volume.getId(), volumeVo); + } + + return answer; + } + @Override public Void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) { @@ -419,7 +486,12 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { answer = cloneVolume(srcData, destData); } else if (destData.getType() == DataObjectType.VOLUME && srcData.getType() == DataObjectType.VOLUME && srcData.getDataStore().getRole() == DataStoreRole.Primary) { - answer = copyVolumeBetweenPools(srcData, destData); + if (srcData.getId() == destData.getId()) { + // The volume has to be migrated across storage pools. + answer = migrateVolumeToPool(srcData, destData.getDataStore()); + } else { + answer = copyVolumeBetweenPools(srcData, destData); + } } else if (srcData.getType() == DataObjectType.SNAPSHOT && destData.getType() == DataObjectType.SNAPSHOT) { answer = copySnapshot(srcData, destData); @@ -435,6 +507,16 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { return null; } + @Override + public Void copyAsync(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, + AsyncCompletionCallback callback) { + CopyCommandResult result = new CopyCommandResult(null, null); + result.setResult("Unsupported operation requested for copying data."); + callback.complete(result); + + return null; + } + @DB protected Answer createTemplateFromSnashot(DataObject srcData, DataObject destData) { diff --git a/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionService.java b/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionService.java index db36f6492e8..5ecbcb37afb 100644 --- a/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionService.java +++ b/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionService.java @@ -18,11 +18,20 @@ */ package org.apache.cloudstack.storage.motion; +import java.util.Map; + 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.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; + public interface DataMotionService { public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback); + public void copyAsync(Map volumeMap, VirtualMachineTO vmTo, + Host srcHost, Host destHost, AsyncCompletionCallback callback); } diff --git a/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionServiceImpl.java b/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionServiceImpl.java index 343140fb98e..b74e10c460f 100644 --- a/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionServiceImpl.java +++ b/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionServiceImpl.java @@ -19,14 +19,19 @@ package org.apache.cloudstack.storage.motion; import java.util.List; +import java.util.Map; 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.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.springframework.stereotype.Component; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; import com.cloud.utils.exception.CloudRuntimeException; @Component @@ -58,4 +63,15 @@ public class DataMotionServiceImpl implements DataMotionService { throw new CloudRuntimeException("can't find strategy to move data"); } + @Override + public void copyAsync(Map volumeMap, VirtualMachineTO vmTo, + Host srcHost, Host destHost, AsyncCompletionCallback callback) { + for (DataMotionStrategy strategy : strategies) { + if (strategy.canHandle(volumeMap, srcHost, destHost)) { + strategy.copyAsync(volumeMap, vmTo, srcHost, destHost, callback); + return; + } + } + throw new CloudRuntimeException("can't find strategy to move data"); + } } diff --git a/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionStrategy.java b/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionStrategy.java index ba40c6dcbce..e3859b4e131 100644 --- a/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionStrategy.java +++ b/engine/storage/src/org/apache/cloudstack/storage/motion/DataMotionStrategy.java @@ -18,13 +18,23 @@ */ package org.apache.cloudstack.storage.motion; +import java.util.Map; + 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.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; + public interface DataMotionStrategy { public boolean canHandle(DataObject srcData, DataObject destData); + public boolean canHandle(Map volumeMap, Host srcHost, Host destHost); public Void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback); + public Void copyAsync(Map volumeMap, VirtualMachineTO vmTo, Host srcHost, Host destHost, + AsyncCompletionCallback callback); } diff --git a/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeObject.java b/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeObject.java index ceadb253976..ea31be3d6a0 100644 --- a/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeObject.java +++ b/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeObject.java @@ -176,6 +176,8 @@ public class VolumeObject implements VolumeInfo { volEvent = Volume.Event.CreateRequested; } else if (event == ObjectInDataStoreStateMachine.Event.CopyingRequested) { volEvent = Volume.Event.CopyRequested; + } else if (event == ObjectInDataStoreStateMachine.Event.MigrationRequested) { + volEvent = Volume.Event.MigrationRequested; } } diff --git a/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 32e7d274f01..e3526debd6e 100644 --- a/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -18,6 +18,10 @@ */ package org.apache.cloudstack.storage.volume; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; + import javax.inject.Inject; import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity; @@ -27,6 +31,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; 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.ObjectInDataStoreStateMachine.Event; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; @@ -40,11 +45,14 @@ import org.apache.cloudstack.storage.datastore.DataObjectManager; import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager; import org.apache.cloudstack.storage.datastore.PrimaryDataStore; import org.apache.cloudstack.storage.datastore.PrimaryDataStoreProviderManager; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.motion.DataMotionService; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; +import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.exception.ConcurrentOperationException; +import com.cloud.host.Host; import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; import com.cloud.storage.Volume.Type; @@ -561,7 +569,163 @@ public class VolumeServiceImpl implements VolumeService { return null; } - + + private class MigrateVolumeContext extends AsyncRpcConext { + final VolumeInfo srcVolume; + final VolumeInfo destVolume; + final DataStore destStore; + final AsyncCallFuture future; + /** + * @param callback + */ + public MigrateVolumeContext(AsyncCompletionCallback callback, AsyncCallFuture future, + VolumeInfo srcVolume, VolumeInfo destVolume, DataStore destStore) { + super(callback); + this.srcVolume = srcVolume; + this.destVolume = destVolume; + this.destStore = destStore; + this.future = future; + } + } + + @Override + public AsyncCallFuture migrateVolume(VolumeInfo srcVolume, DataStore destStore) { + AsyncCallFuture future = new AsyncCallFuture(); + VolumeApiResult res = new VolumeApiResult(srcVolume); + try { + if (!this.snapshotMgr.canOperateOnVolume(srcVolume)) { + s_logger.debug("Snapshots are being created on this volume. This volume cannot be migrated now."); + res.setResult("Snapshots are being created on this volume. This volume cannot be migrated now."); + future.complete(res); + return future; + } + + VolumeInfo destVolume = this.volFactory.getVolume(srcVolume.getId(), destStore); + srcVolume.processEvent(Event.MigrationRequested); + MigrateVolumeContext context = new MigrateVolumeContext(null, future, + srcVolume, destVolume, destStore); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().migrateVolumeCallBack(null, null)).setContext(context); + this.motionSrv.copyAsync(srcVolume, destVolume, caller); + } catch (Exception e) { + s_logger.debug("Failed to copy volume", e); + res.setResult(e.toString()); + future.complete(res); + } + return future; + } + + protected Void migrateVolumeCallBack(AsyncCallbackDispatcher callback, + MigrateVolumeContext context) { + VolumeInfo srcVolume = context.srcVolume; + VolumeInfo destVolume = context.destVolume; + CopyCommandResult result = callback.getResult(); + AsyncCallFuture future = context.future; + VolumeApiResult res = new VolumeApiResult(srcVolume); + try { + if (result.isFailed()) { + res.setResult(result.getResult()); + srcVolume.processEvent(Event.OperationFailed); + future.complete(res); + } else { + srcVolume.processEvent(Event.OperationSuccessed); + future.complete(res); + } + } catch (Exception e) { + s_logger.error("Failed to process copy volume callback", e); + res.setResult(e.toString()); + future.complete(res); + } + + return null; + } + + private class MigrateVmWithVolumesContext extends AsyncRpcConext { + final Map volumeToPool; + final AsyncCallFuture future; + /** + * @param callback + */ + public MigrateVmWithVolumesContext(AsyncCompletionCallback callback, AsyncCallFuture future, + Map volumeToPool) { + super(callback); + this.volumeToPool = volumeToPool; + this.future = future; + } + } + + @Override + public AsyncCallFuture migrateVolumes(Map volumeMap, VirtualMachineTO vmTo, + Host srcHost, Host destHost) { + AsyncCallFuture future = new AsyncCallFuture(); + CommandResult res = new CommandResult(); + try { + // Check to make sure there are no snapshot operations on a volume and + // put it in the migrating state. + List volumesMigrating = new ArrayList(); + for (Map.Entry entry : volumeMap.entrySet()) { + VolumeInfo volume = entry.getKey(); + if (!this.snapshotMgr.canOperateOnVolume(volume)) { + s_logger.debug("Snapshots are being created on a volume. Volumes cannot be migrated now."); + res.setResult("Snapshots are being created on a volume. Volumes cannot be migrated now."); + future.complete(res); + + // All the volumes that are already in migrating state need to be put back in ready state. + for (VolumeInfo volumeMigrating : volumesMigrating) { + volumeMigrating.processEvent(Event.OperationFailed); + } + return future; + } else { + volume.processEvent(Event.MigrationRequested); + volumesMigrating.add(volume); + } + } + + MigrateVmWithVolumesContext context = new MigrateVmWithVolumesContext(null, + future, volumeMap); + AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().migrateVmWithVolumesCallBack(null, null)).setContext(context); + this.motionSrv.copyAsync(volumeMap, vmTo, srcHost, destHost, caller); + + } catch (Exception e) { + s_logger.debug("Failed to copy volume", e); + res.setResult(e.toString()); + future.complete(res); + } + + return future; + } + + protected Void migrateVmWithVolumesCallBack(AsyncCallbackDispatcher callback, + MigrateVmWithVolumesContext context) { + Map volumeToPool = context.volumeToPool; + CopyCommandResult result = callback.getResult(); + AsyncCallFuture future = context.future; + CommandResult res = new CommandResult(); + try { + if (result.isFailed()) { + res.setResult(result.getResult()); + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeInfo volume = entry.getKey(); + volume.processEvent(Event.OperationFailed); + } + future.complete(res); + } else { + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeInfo volume = entry.getKey(); + volume.processEvent(Event.OperationSuccessed); + } + future.complete(res); + } + } catch (Exception e) { + s_logger.error("Failed to process copy volume callback", e); + res.setResult(e.toString()); + future.complete(res); + } + + return null; + } + @Override public AsyncCallFuture registerVolume(VolumeInfo volume, DataStore store) { diff --git a/plugins/host-allocators/random/src/com/cloud/agent/manager/allocator/impl/RandomAllocator.java b/plugins/host-allocators/random/src/com/cloud/agent/manager/allocator/impl/RandomAllocator.java index a672efdc77e..8243f3a46ad 100755 --- a/plugins/host-allocators/random/src/com/cloud/agent/manager/allocator/impl/RandomAllocator.java +++ b/plugins/host-allocators/random/src/com/cloud/agent/manager/allocator/impl/RandomAllocator.java @@ -53,6 +53,62 @@ public class RandomAllocator extends AdapterBase implements HostAllocator { return allocateTo(vmProfile, plan, type, avoid, returnUpTo, true); } + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, + ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity) { + long dcId = plan.getDataCenterId(); + Long podId = plan.getPodId(); + Long clusterId = plan.getClusterId(); + ServiceOffering offering = vmProfile.getServiceOffering(); + List suitableHosts = new ArrayList(); + + if (type == Host.Type.Storage) { + return suitableHosts; + } + + String hostTag = offering.getHostTag(); + if(hostTag != null){ + s_logger.debug("Looking for hosts in dc: " + dcId + " pod:" + podId + " cluster:" + clusterId + + " having host tag:" + hostTag); + }else{ + s_logger.debug("Looking for hosts in dc: " + dcId + " pod:" + podId + " cluster:" + clusterId); + } + + // list all computing hosts, regardless of whether they support routing...it's random after all + if(hostTag != null){ + hosts.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, hostTag)); + }else{ + hosts.retainAll(_resourceMgr.listAllUpAndEnabledHosts(type, clusterId, podId, dcId)); + } + + s_logger.debug("Random Allocator found " + hosts.size() + " hosts"); + if (hosts.size() == 0) { + return suitableHosts; + } + + Collections.shuffle(hosts); + for (Host host : hosts) { + if(suitableHosts.size() == returnUpTo){ + break; + } + + if (!avoid.shouldAvoid(host)) { + suitableHosts.add(host); + } else { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Host name: " + host.getName() + ", hostId: "+ host.getId() +" is in avoid set, " + + "skipping this and trying other available hosts"); + } + } + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Random Host Allocator returning "+suitableHosts.size() +" suitable hosts"); + } + + return suitableHosts; + } + @Override public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity) { diff --git a/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java b/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java index 4ef583a5463..46ae35a4a54 100644 --- a/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java +++ b/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/CitrixResourceBase.java @@ -3358,7 +3358,7 @@ public abstract class CitrixResourceBase implements ServerResource, HypervisorRe vm.setMemoryLimits(conn, maxMemsize, maxMemsize, minMemsize, maxMemsize); } - private void waitForTask(Connection c, Task task, long pollInterval, long timeout) throws XenAPIException, XmlRpcException { + protected void waitForTask(Connection c, Task task, long pollInterval, long timeout) throws XenAPIException, XmlRpcException { long beginTime = System.currentTimeMillis(); while (task.getStatus(c) == Types.TaskStatusType.PENDING) { try { @@ -3374,7 +3374,7 @@ public abstract class CitrixResourceBase implements ServerResource, HypervisorRe } } - private void checkForSuccess(Connection c, Task task) throws XenAPIException, XmlRpcException { + protected void checkForSuccess(Connection c, Task task) throws XenAPIException, XmlRpcException { if (task.getStatus(c) == Types.TaskStatusType.SUCCESS) { return; } else { diff --git a/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer56FP1Resource.java b/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer56FP1Resource.java index d64e17337a1..96a90a61fff 100644 --- a/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer56FP1Resource.java +++ b/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer56FP1Resource.java @@ -132,6 +132,7 @@ public class XenServer56FP1Resource extends XenServer56Resource { record.affinity = host; record.otherConfig.remove("disks"); record.otherConfig.remove("default_template"); + record.otherConfig.remove("mac_seed"); record.isATemplate = false; record.nameLabel = vmSpec.getName(); record.actionsAfterCrash = Types.OnCrashBehaviour.DESTROY; diff --git a/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer610Resource.java b/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer610Resource.java index 8d267b114fa..bb31136f38e 100644 --- a/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer610Resource.java +++ b/plugins/hypervisors/xen/src/com/cloud/hypervisor/xen/resource/XenServer610Resource.java @@ -20,6 +20,9 @@ package com.cloud.hypervisor.xen.resource; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import javax.ejb.Local; @@ -28,7 +31,34 @@ import org.apache.log4j.Logger; import com.cloud.resource.ServerResource; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; - +import com.cloud.vm.VirtualMachine.State; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.storage.MigrateVolumeCommand; +import com.cloud.agent.api.MigrateWithStorageAnswer; +import com.cloud.agent.api.MigrateWithStorageCommand; +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.MigrateWithStorageCompleteAnswer; +import com.cloud.agent.api.MigrateWithStorageCompleteCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.network.Networks.TrafficType; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; +import com.cloud.agent.api.to.NicTO; +import com.xensource.xenapi.Connection; +import com.xensource.xenapi.Host; +import com.xensource.xenapi.Network; +import com.xensource.xenapi.SR; +import com.xensource.xenapi.Task; +import com.xensource.xenapi.Types; +import com.xensource.xenapi.VBD; +import com.xensource.xenapi.VDI; +import com.xensource.xenapi.VIF; +import com.xensource.xenapi.VM; @Local(value=ServerResource.class) public class XenServer610Resource extends XenServer56FP1Resource { @@ -55,4 +85,331 @@ public class XenServer610Resource extends XenServer56FP1Resource { files.add(file); return files; } + + @Override + public Answer executeRequest(Command cmd) { + if (cmd instanceof MigrateWithStorageCommand) { + return execute((MigrateWithStorageCommand) cmd); + } else if (cmd instanceof MigrateWithStorageReceiveCommand) { + return execute((MigrateWithStorageReceiveCommand) cmd); + } else if (cmd instanceof MigrateWithStorageSendCommand) { + return execute((MigrateWithStorageSendCommand) cmd); + } else if (cmd instanceof MigrateWithStorageCompleteCommand) { + return execute((MigrateWithStorageCompleteCommand) cmd); + } else if (cmd instanceof MigrateVolumeCommand) { + return execute((MigrateVolumeCommand) cmd); + } else { + return super.executeRequest(cmd); + } + } + + private List getUpdatedVolumePathsOfMigratedVm(Connection connection, VM migratedVm, + VolumeTO[] volumes) throws CloudRuntimeException { + List volumeToList = new ArrayList(); + + try { + // Volume paths would have changed. Return that information. + Set vbds = migratedVm.getVBDs(connection); + Map deviceIdToVdiMap = new HashMap(); + // get vdi:vbdr to a map + for (VBD vbd : vbds) { + VBD.Record vbdr = vbd.getRecord(connection); + if (vbdr.type == Types.VbdType.DISK) { + VDI vdi = vbdr.VDI; + deviceIdToVdiMap.put(vbdr.userdevice, vdi); + } + } + + for (VolumeTO volumeTo : volumes) { + Long deviceId = volumeTo.getDeviceId(); + VDI vdi = deviceIdToVdiMap.get(deviceId.toString()); + volumeTo.setPath(vdi.getUuid(connection)); + volumeToList.add(volumeTo); + } + } catch (Exception e) { + s_logger.error("Unable to get the updated VDI paths of the migrated vm " + e.toString(), e); + throw new CloudRuntimeException("Unable to get the updated VDI paths of the migrated vm " + e.toString(), e); + } + + return volumeToList; + } + + protected MigrateWithStorageAnswer execute(MigrateWithStorageCommand cmd) { + Connection connection = getConnection(); + VirtualMachineTO vmSpec = cmd.getVirtualMachine(); + Map volumeToFiler = cmd.getVolumeToFiler(); + final String vmName = vmSpec.getName(); + State state = s_vms.getState(_cluster, vmName); + Task task = null; + + synchronized (_cluster.intern()) { + s_vms.put(_cluster, _name, vmName, State.Stopping); + } + + try { + prepareISO(connection, vmSpec.getName()); + Map other = new HashMap(); + other.put("live", "true"); + Network networkForSm = getNativeNetworkForTraffic(connection, TrafficType.Storage, null).getNetwork(); + Host host = Host.getByUuid(connection, _host.uuid); + Map token = host.migrateReceive(connection, networkForSm, other); + + // Get the vm to migrate. + Set vms = VM.getByNameLabel(connection, vmSpec.getName()); + VM vmToMigrate = vms.iterator().next(); + + // Create the vif map. The vm stays in the same cluster so we have to pass an empty vif map. + Map vifMap = new HashMap(); + Map vdiMap = new HashMap(); + for (Map.Entry entry : volumeToFiler.entrySet()) { + vdiMap.put(getVDIbyUuid(connection, entry.getKey().getPath()), + getStorageRepository(connection, entry.getValue().getUuid())); + } + + // Check migration with storage is possible. + task = vmToMigrate.assertCanMigrateAsync(connection, token, true, vdiMap, vifMap, other); + try { + // poll every 1 seconds + long timeout = (_migratewait) * 1000L; + waitForTask(connection, task, 1000, timeout); + checkForSuccess(connection, task); + } catch (Types.HandleInvalid e) { + s_logger.error("Error while checking if vm " + vmName + " can be migrated to the destination host " + + host, e); + throw new CloudRuntimeException("Error while checking if vm " + vmName + " can be migrated to the " + + "destination host " + host, e); + } + + // Migrate now. + task = vmToMigrate.migrateSendAsync(connection, token, true, vdiMap, vifMap, other); + try { + // poll every 1 seconds. + long timeout = (_migratewait) * 1000L; + waitForTask(connection, task, 1000, timeout); + checkForSuccess(connection, task); + } catch (Types.HandleInvalid e) { + s_logger.error("Error while migrating vm " + vmName + " to the destination host " + host, e); + throw new CloudRuntimeException("Error while migrating vm " + vmName + " to the destination host " + + host, e); + } + + // Volume paths would have changed. Return that information. + List volumeToList = getUpdatedVolumePathsOfMigratedVm(connection, vmToMigrate, vmSpec.getDisks()); + vmToMigrate.setAffinity(connection, host); + state = State.Stopping; + + return new MigrateWithStorageAnswer(cmd, volumeToList); + } catch (Exception e) { + s_logger.warn("Catch Exception " + e.getClass().getName() + ". Storage motion failed due to " + + e.toString(), e); + return new MigrateWithStorageAnswer(cmd, e); + } finally { + if (task != null) { + try { + task.destroy(connection); + } catch (Exception e) { + s_logger.debug("Unable to destroy task " + task.toString() + " on host " + _host.uuid +" due to " + + e.toString()); + } + } + + synchronized (_cluster.intern()) { + s_vms.put(_cluster, _name, vmName, state); + } + } + } + + protected MigrateWithStorageReceiveAnswer execute(MigrateWithStorageReceiveCommand cmd) { + Connection connection = getConnection(); + VirtualMachineTO vmSpec = cmd.getVirtualMachine(); + Map volumeToFiler = cmd.getVolumeToFiler(); + + try { + // Get a map of all the SRs to which the vdis will be migrated. + Map volumeToSr = new HashMap(); + for (Map.Entry entry : volumeToFiler.entrySet()) { + SR sr = getStorageRepository(connection, entry.getValue().getUuid()); + volumeToSr.put(entry.getKey(), sr); + } + + // Get the list of networks to which the vifs will attach. + Map nicToNetwork = new HashMap(); + for (NicTO nicTo : vmSpec.getNics()) { + Network network = getNetwork(connection, nicTo); + nicToNetwork.put(nicTo, network); + } + + Map other = new HashMap(); + other.put("live", "true"); + Network network = getNativeNetworkForTraffic(connection, TrafficType.Storage, null).getNetwork(); + Host host = Host.getByUuid(connection, _host.uuid); + Map token = host.migrateReceive(connection, network, other); + + return new MigrateWithStorageReceiveAnswer(cmd, volumeToSr, nicToNetwork, token); + } catch (CloudRuntimeException e) { + s_logger.error("Migration of vm " + vmSpec.getName() + " with storage failed due to " + e.toString(), e); + return new MigrateWithStorageReceiveAnswer(cmd, e); + } catch (Exception e) { + s_logger.error("Migration of vm " + vmSpec.getName() + " with storage failed due to " + e.toString(), e); + return new MigrateWithStorageReceiveAnswer(cmd, e); + } + } + + protected MigrateWithStorageSendAnswer execute(MigrateWithStorageSendCommand cmd) { + Connection connection = getConnection(); + VirtualMachineTO vmSpec = cmd.getVirtualMachine(); + Map volumeToSr = cmd.getVolumeToSr(); + Map nicToNetwork = cmd.getNicToNetwork(); + Map token = cmd.getToken(); + final String vmName = vmSpec.getName(); + State state = s_vms.getState(_cluster, vmName); + Set volumeToSet = null; + boolean migrated = false; + Task task = null; + + synchronized (_cluster.intern()) { + s_vms.put(_cluster, _name, vmName, State.Stopping); + } + + try { + Set vms = VM.getByNameLabel(connection, vmSpec.getName()); + VM vmToMigrate = vms.iterator().next(); + Map other = new HashMap(); + other.put("live", "true"); + + // Create the vdi map which tells what volumes of the vm need to go on which sr on the destination. + Map vdiMap = new HashMap(); + for (Map.Entry entry : volumeToSr.entrySet()) { + if (entry.getValue() instanceof SR) { + SR sr = (SR)entry.getValue(); + VDI vdi = getVDIbyUuid(connection, entry.getKey().getPath()); + vdiMap.put(vdi, sr); + } else { + throw new CloudRuntimeException("The object " + entry.getValue() + " passed is not of type SR."); + } + } + + // Create the vif map. + Map vifMap = new HashMap(); + for (Map.Entry entry : nicToNetwork.entrySet()) { + if (entry.getValue() instanceof Network) { + Network network = (Network)entry.getValue(); + VIF vif = getVifByMac(connection, vmToMigrate, entry.getKey().getMac()); + vifMap.put(vif, network); + } else { + throw new CloudRuntimeException("The object " + entry.getValue() + " passed is not of type Network."); + } + } + + // Check migration with storage is possible. + task = vmToMigrate.assertCanMigrateAsync(connection, token, true, vdiMap, vifMap, other); + try { + // poll every 1 seconds. + long timeout = (_migratewait) * 1000L; + waitForTask(connection, task, 1000, timeout); + checkForSuccess(connection, task); + } catch (Types.HandleInvalid e) { + s_logger.error("Error while checking if vm " + vmName + " can be migrated.", e); + throw new CloudRuntimeException("Error while checking if vm " + vmName + " can be migrated.", e); + } + + // Migrate now. + task = vmToMigrate.migrateSendAsync(connection, token, true, vdiMap, vifMap, other); + try { + // poll every 1 seconds. + long timeout = (_migratewait) * 1000L; + waitForTask(connection, task, 1000, timeout); + checkForSuccess(connection, task); + } catch (Types.HandleInvalid e) { + s_logger.error("Error while migrating vm " + vmName, e); + throw new CloudRuntimeException("Error while migrating vm " + vmName, e); + } + + migrated = true; + return new MigrateWithStorageSendAnswer(cmd, volumeToSet); + } catch (CloudRuntimeException e) { + s_logger.error("Migration of vm " + vmName + " with storage failed due to " + e.toString(), e); + return new MigrateWithStorageSendAnswer(cmd, e); + } catch (Exception e) { + s_logger.error("Migration of vm " + vmName + " with storage failed due to " + e.toString(), e); + return new MigrateWithStorageSendAnswer(cmd, e); + } finally { + if (task != null) { + try { + task.destroy(connection); + } catch (Exception e) { + s_logger.debug("Unable to destroy task " + task.toString() + " on host " + _host.uuid +" due to " + + e.toString()); + } + } + + // Keep cluster/vm sync happy. + synchronized (_cluster.intern()) { + if (migrated) { + s_vms.remove(_cluster, _name, vmName); + } else { + s_vms.put(_cluster, _name, vmName, state); + } + } + } + } + + protected MigrateWithStorageCompleteAnswer execute(MigrateWithStorageCompleteCommand cmd) { + Connection connection = getConnection(); + VirtualMachineTO vmSpec = cmd.getVirtualMachine(); + + try { + Host host = Host.getByUuid(connection, _host.uuid); + Set vms = VM.getByNameLabel(connection, vmSpec.getName()); + VM migratedVm = vms.iterator().next(); + + // Check the vm is present on the new host. + if (migratedVm == null) { + throw new CloudRuntimeException("Couldn't find the migrated vm " + vmSpec.getName() + + " on the destination host."); + } + + // Volume paths would have changed. Return that information. + List volumeToSet = getUpdatedVolumePathsOfMigratedVm(connection, migratedVm, vmSpec.getDisks()); + migratedVm.setAffinity(connection, host); + + synchronized (_cluster.intern()) { + s_vms.put(_cluster, _name, vmSpec.getName(), State.Running); + } + + return new MigrateWithStorageCompleteAnswer(cmd, volumeToSet); + } catch (CloudRuntimeException e) { + s_logger.error("Migration of vm " + vmSpec.getName() + " with storage failed due to " + e.toString(), e); + return new MigrateWithStorageCompleteAnswer(cmd, e); + } catch (Exception e) { + s_logger.error("Migration of vm " + vmSpec.getName() + " with storage failed due to " + e.toString(), e); + return new MigrateWithStorageCompleteAnswer(cmd, e); + } + } + + protected MigrateVolumeAnswer execute(MigrateVolumeCommand cmd) { + Connection connection = getConnection(); + String volumeUUID = cmd.getVolumePath(); + StorageFilerTO poolTO = cmd.getPool(); + + try { + SR destinationPool = getStorageRepository(connection, poolTO.getUuid()); + VDI srcVolume = getVDIbyUuid(connection, volumeUUID); + Map other = new HashMap(); + other.put("live", "true"); + + // Live migrate the vdi across pool. + Task task = srcVolume.poolMigrateAsync(connection, destinationPool, other); + long timeout = (_migratewait) * 1000L; + waitForTask(connection, task, 1000, timeout); + checkForSuccess(connection, task); + VDI dvdi = Types.toVDI(task, connection); + + return new MigrateVolumeAnswer(cmd, true, null, dvdi.getUuid(connection)); + } 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); + } + } } diff --git a/plugins/hypervisors/xen/src/org/apache/cloudstack/storage/motion/XenServerStorageMotionStrategy.java b/plugins/hypervisors/xen/src/org/apache/cloudstack/storage/motion/XenServerStorageMotionStrategy.java new file mode 100644 index 00000000000..353f2b58d08 --- /dev/null +++ b/plugins/hypervisors/xen/src/org/apache/cloudstack/storage/motion/XenServerStorageMotionStrategy.java @@ -0,0 +1,239 @@ +/* + * 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.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 XenServerStorageMotionStrategy implements DataMotionStrategy { + private static final Logger s_logger = Logger.getLogger(XenServerStorageMotionStrategy.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) { + return true; + } + + @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 receive command to the destination host so that it is ready to receive a vm. + // 2. Send a migrate send command to the source host. This actually migrates the vm to the destination. + // 3. Complete the process. Update the volume details. + MigrateWithStorageReceiveCommand receiveCmd = new MigrateWithStorageReceiveCommand(to, volumeToFilerto); + MigrateWithStorageReceiveAnswer receiveAnswer = (MigrateWithStorageReceiveAnswer) agentMgr.send( + destHost.getId(), receiveCmd); + if (receiveAnswer == 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 (!receiveAnswer.getResult()) { + s_logger.error("Migration with storage of vm " + vm+ " failed. Details: " + receiveAnswer.getDetails()); + throw new CloudRuntimeException("Error while migrating the vm " + vm + " to host " + destHost + + ". " + receiveAnswer.getDetails()); + } + + MigrateWithStorageSendCommand sendCmd = new MigrateWithStorageSendCommand(to, receiveAnswer.getVolumeToSr(), + receiveAnswer.getNicToNetwork(), receiveAnswer.getToken()); + MigrateWithStorageSendAnswer sendAnswer = (MigrateWithStorageSendAnswer) agentMgr.send( + srcHost.getId(), sendCmd); + if (sendAnswer == 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 (!sendAnswer.getResult()) { + s_logger.error("Migration with storage of vm " + vm+ " failed. Details: " + sendAnswer.getDetails()); + throw new CloudRuntimeException("Error while migrating the vm " + vm + " to host " + destHost + + ". " + sendAnswer.getDetails()); + } + + MigrateWithStorageCompleteCommand command = new MigrateWithStorageCompleteCommand(to); + MigrateWithStorageCompleteAnswer answer = (MigrateWithStorageCompleteAnswer) agentMgr.send( + destHost.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. + updateVolumePathsAfterMigration(volumeToPool, answer.getVolumeTos()); + } + + 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 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); + MigrateWithStorageAnswer answer = (MigrateWithStorageAnswer) agentMgr.send(destHost.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. + updateVolumePathsAfterMigration(volumeToPool, answer.getVolumeTos()); + } + + 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 updateVolumePathsAfterMigration(Map volumeToPool, List volumeTos) { + for (Map.Entry entry : volumeToPool.entrySet()) { + boolean updated = false; + VolumeInfo volume = entry.getKey(); + StoragePool pool = (StoragePool)entry.getValue(); + for (VolumeTO volumeTo : volumeTos) { + if (volume.getId() == volumeTo.getId()) { + VolumeVO volumeVO = volDao.findById(volume.getId()); + Long oldPoolId = volumeVO.getPoolId(); + volumeVO.setPath(volumeTo.getPath()); + volumeVO.setFolder(pool.getPath()); + volumeVO.setPodId(pool.getPodId()); + volumeVO.setPoolId(pool.getId()); + volumeVO.setLastPoolId(oldPoolId); + volDao.update(volume.getId(), volumeVO); + updated = true; + break; + } + } + + if (!updated) { + s_logger.error("Volume path wasn't updated for volume " + volume + " after it was migrated."); + } + } + } +} diff --git a/server/src/com/cloud/agent/manager/allocator/HostAllocator.java b/server/src/com/cloud/agent/manager/allocator/HostAllocator.java index 60027e7f9e3..6700f2227d9 100755 --- a/server/src/com/cloud/agent/manager/allocator/HostAllocator.java +++ b/server/src/com/cloud/agent/manager/allocator/HostAllocator.java @@ -21,6 +21,7 @@ import java.util.List; import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.host.Host; +import com.cloud.host.HostVO; import com.cloud.host.Host.Type; import com.cloud.offering.ServiceOffering; import com.cloud.utils.component.Adapter; @@ -63,8 +64,22 @@ public interface HostAllocator extends Adapter { **/ public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity); - - - public static int RETURN_UPTO_ALL = -1; - + + /** + * Determines which physical hosts are suitable to + * allocate the guest virtual machines on + * + * @param VirtualMachineProfile vmProfile + * @param DeploymentPlan plan + * @param GuestType type + * @param ExcludeList avoid + * @param List hosts + * @param int returnUpTo (use -1 to return all possible hosts) + * @param boolean considerReservedCapacity (default should be true, set to false if host capacity calculation should not look at reserved capacity) + * @return List List of hosts that are suitable for VM allocation + **/ + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity); + + public static int RETURN_UPTO_ALL = -1; + } diff --git a/server/src/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java b/server/src/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java index 0091e43cab8..b54b1c1f527 100755 --- a/server/src/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java +++ b/server/src/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java @@ -172,6 +172,53 @@ public class FirstFitAllocator extends AdapterBase implements HostAllocator { return allocateTo(plan, offering, template, avoid, clusterHosts, returnUpTo, considerReservedCapacity, account); } + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, + Type type, ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity) { + long dcId = plan.getDataCenterId(); + Long podId = plan.getPodId(); + Long clusterId = plan.getClusterId(); + ServiceOffering offering = vmProfile.getServiceOffering(); + VMTemplateVO template = (VMTemplateVO)vmProfile.getTemplate(); + Account account = vmProfile.getOwner(); + List suitableHosts = new ArrayList(); + + if (type == Host.Type.Storage) { + // FirstFitAllocator should be used for user VMs only since it won't care whether the host is capable of + // routing or not. + return suitableHosts; + } + + String hostTagOnOffering = offering.getHostTag(); + String hostTagOnTemplate = template.getTemplateTag(); + boolean hasSvcOfferingTag = hostTagOnOffering != null ? true : false; + boolean hasTemplateTag = hostTagOnTemplate != null ? true : false; + + String haVmTag = (String)vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); + if (haVmTag != null) { + hosts.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, haVmTag)); + } else { + if (hostTagOnOffering == null && hostTagOnTemplate == null){ + hosts.retainAll(_resourceMgr.listAllUpAndEnabledNonHAHosts(type, clusterId, podId, dcId)); + } else { + if (hasSvcOfferingTag) { + hosts.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, hostTagOnOffering)); + } + + if (hasTemplateTag) { + hosts.retainAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, hostTagOnTemplate)); + } + } + } + + if (!hosts.isEmpty()) { + suitableHosts = allocateTo(plan, offering, template, avoid, hosts, returnUpTo, considerReservedCapacity, + account); + } + + return suitableHosts; + } + protected List allocateTo(DeploymentPlan plan, ServiceOffering offering, VMTemplateVO template, ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity, Account account) { if (_allocationAlgorithm.equals("random") || _allocationAlgorithm.equals("userconcentratedpod_random")) { // Shuffle this so that we don't check the hosts in the same order. diff --git a/server/src/com/cloud/agent/manager/allocator/impl/TestingAllocator.java b/server/src/com/cloud/agent/manager/allocator/impl/TestingAllocator.java index 90bd95629bf..890c047de50 100755 --- a/server/src/com/cloud/agent/manager/allocator/impl/TestingAllocator.java +++ b/server/src/com/cloud/agent/manager/allocator/impl/TestingAllocator.java @@ -29,6 +29,7 @@ import com.cloud.agent.manager.allocator.HostAllocator; import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.host.Host; +import com.cloud.host.HostVO; import com.cloud.host.Host.Type; import com.cloud.host.dao.HostDao; import com.cloud.offering.ServiceOffering; @@ -50,6 +51,12 @@ public class TestingAllocator extends AdapterBase implements HostAllocator { return allocateTo(vmProfile, plan, type, avoid, returnUpTo, true); } + @Override + public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, + ExcludeList avoid, List hosts, int returnUpTo, boolean considerReservedCapacity) { + return allocateTo(vmProfile, plan, type, avoid, returnUpTo, considerReservedCapacity); + } + @Override public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity) { diff --git a/server/src/com/cloud/api/ApiDBUtils.java b/server/src/com/cloud/api/ApiDBUtils.java index 303f3281d89..c60af279d4f 100755 --- a/server/src/com/cloud/api/ApiDBUtils.java +++ b/server/src/com/cloud/api/ApiDBUtils.java @@ -36,6 +36,7 @@ import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainRouterResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.HostForMigrationResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; import org.apache.cloudstack.api.response.ProjectAccountResponse; import org.apache.cloudstack.api.response.ProjectInvitationResponse; @@ -43,6 +44,7 @@ import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.ResourceTagResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.StoragePoolForMigrationResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; @@ -1518,6 +1520,14 @@ public class ApiDBUtils { return _hostJoinDao.setHostResponse(vrData, vr); } + public static HostForMigrationResponse newHostForMigrationResponse(HostJoinVO vr, EnumSet details) { + return _hostJoinDao.newHostForMigrationResponse(vr, details); + } + + public static HostForMigrationResponse fillHostForMigrationDetails(HostForMigrationResponse vrData, HostJoinVO vr) { + return _hostJoinDao.setHostForMigrationResponse(vrData, vr); + } + public static List newHostView(Host vr){ return _hostJoinDao.newHostView(vr); } @@ -1543,6 +1553,15 @@ public class ApiDBUtils { return _poolJoinDao.setStoragePoolResponse(vrData, vr); } + public static StoragePoolForMigrationResponse newStoragePoolForMigrationResponse(StoragePoolJoinVO vr) { + return _poolJoinDao.newStoragePoolForMigrationResponse(vr); + } + + public static StoragePoolForMigrationResponse fillStoragePoolForMigrationDetails(StoragePoolForMigrationResponse + vrData, StoragePoolJoinVO vr){ + return _poolJoinDao.setStoragePoolForMigrationResponse(vrData, vr); + } + public static List newStoragePoolView(StoragePool vr){ return _poolJoinDao.newStoragePoolView(vr); } diff --git a/server/src/com/cloud/api/ApiResponseHelper.java b/server/src/com/cloud/api/ApiResponseHelper.java index 7629e5e5e59..a7d6165e6ae 100755 --- a/server/src/com/cloud/api/ApiResponseHelper.java +++ b/server/src/com/cloud/api/ApiResponseHelper.java @@ -65,6 +65,7 @@ import org.apache.cloudstack.api.response.FirewallRuleResponse; import org.apache.cloudstack.api.response.GlobalLoadBalancerResponse; import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.HostForMigrationResponse; import org.apache.cloudstack.api.response.HypervisorCapabilitiesResponse; import org.apache.cloudstack.api.response.IPAddressResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; @@ -105,6 +106,7 @@ import org.apache.cloudstack.api.response.SnapshotResponse; import org.apache.cloudstack.api.response.SnapshotScheduleResponse; import org.apache.cloudstack.api.response.StaticRouteResponse; import org.apache.cloudstack.api.response.StorageNetworkIpRangeResponse; +import org.apache.cloudstack.api.response.StoragePoolForMigrationResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.SwiftResponse; import org.apache.cloudstack.api.response.SystemVmInstanceResponse; @@ -510,6 +512,20 @@ public class ApiResponseHelper implements ResponseGenerator { return listHosts.get(0); } + @Override + public HostForMigrationResponse createHostForMigrationResponse(Host host) { + return createHostForMigrationResponse(host, EnumSet.of(HostDetails.all)); + } + + @Override + public HostForMigrationResponse createHostForMigrationResponse(Host host, EnumSet details) { + List viewHosts = ApiDBUtils.newHostView(host); + List listHosts = ViewResponseHelper.createHostForMigrationResponse(details, + viewHosts.toArray(new HostJoinVO[viewHosts.size()])); + assert listHosts != null && listHosts.size() == 1 : "There should be one host returned"; + return listHosts.get(0); + } + @Override public SwiftResponse createSwiftResponse(Swift swift) { SwiftResponse swiftResponse = new SwiftResponse(); @@ -908,16 +924,21 @@ public class ApiResponseHelper implements ResponseGenerator { } - - @Override public StoragePoolResponse createStoragePoolResponse(StoragePool pool) { List viewPools = ApiDBUtils.newStoragePoolView(pool); List listPools = ViewResponseHelper.createStoragePoolResponse(viewPools.toArray(new StoragePoolJoinVO[viewPools.size()])); assert listPools != null && listPools.size() == 1 : "There should be one storage pool returned"; return listPools.get(0); + } - + @Override + public StoragePoolForMigrationResponse createStoragePoolForMigrationResponse(StoragePool pool) { + List viewPools = ApiDBUtils.newStoragePoolView(pool); + List listPools = ViewResponseHelper.createStoragePoolForMigrationResponse( + viewPools.toArray(new StoragePoolJoinVO[viewPools.size()])); + assert listPools != null && listPools.size() == 1 : "There should be one storage pool returned"; + return listPools.get(0); } @Override diff --git a/server/src/com/cloud/api/query/ViewResponseHelper.java b/server/src/com/cloud/api/query/ViewResponseHelper.java index dc2727e8e0d..827ae7b7a66 100644 --- a/server/src/com/cloud/api/query/ViewResponseHelper.java +++ b/server/src/com/cloud/api/query/ViewResponseHelper.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainRouterResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.HostForMigrationResponse; import org.apache.cloudstack.api.response.InstanceGroupResponse; import org.apache.cloudstack.api.response.ProjectAccountResponse; import org.apache.cloudstack.api.response.ProjectInvitationResponse; @@ -38,6 +39,7 @@ import org.apache.cloudstack.api.response.ResourceTagResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; +import org.apache.cloudstack.api.response.StoragePoolForMigrationResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; @@ -230,6 +232,24 @@ public class ViewResponseHelper { return new ArrayList(vrDataList.values()); } + public static List createHostForMigrationResponse(EnumSet details, + HostJoinVO... hosts) { + Hashtable vrDataList = new Hashtable(); + // Initialise the vrdatalist with the input data + for (HostJoinVO vr : hosts) { + HostForMigrationResponse vrData = vrDataList.get(vr.getId()); + if ( vrData == null ) { + // first time encountering this vm + vrData = ApiDBUtils.newHostForMigrationResponse(vr, details); + } else { + // update tags + vrData = ApiDBUtils.fillHostForMigrationDetails(vrData, vr); + } + vrDataList.put(vr.getId(), vrData); + } + return new ArrayList(vrDataList.values()); + } + public static List createVolumeResponse(VolumeJoinVO... volumes) { Hashtable vrDataList = new Hashtable(); for (VolumeJoinVO vr : volumes) { @@ -265,6 +285,23 @@ public class ViewResponseHelper { return new ArrayList(vrDataList.values()); } + public static List createStoragePoolForMigrationResponse(StoragePoolJoinVO... pools) { + Hashtable vrDataList = new Hashtable(); + // Initialise the vrdatalist with the input data + for (StoragePoolJoinVO vr : pools) { + StoragePoolForMigrationResponse vrData = vrDataList.get(vr.getId()); + if ( vrData == null ) { + // first time encountering this vm + vrData = ApiDBUtils.newStoragePoolForMigrationResponse(vr); + } else { + // update tags + vrData = ApiDBUtils.fillStoragePoolForMigrationDetails(vrData, vr); + } + vrDataList.put(vr.getId(), vrData); + } + return new ArrayList(vrDataList.values()); + } + public static List createAccountResponse(AccountJoinVO... accounts) { List respList = new ArrayList(); diff --git a/server/src/com/cloud/api/query/dao/HostJoinDao.java b/server/src/com/cloud/api/query/dao/HostJoinDao.java index 1a2129998ed..f526ca32615 100644 --- a/server/src/com/cloud/api/query/dao/HostJoinDao.java +++ b/server/src/com/cloud/api/query/dao/HostJoinDao.java @@ -21,6 +21,7 @@ import java.util.List; import org.apache.cloudstack.api.ApiConstants.HostDetails; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.HostForMigrationResponse; import com.cloud.api.query.vo.HostJoinVO; import com.cloud.host.Host; import com.cloud.utils.db.GenericDao; @@ -31,6 +32,10 @@ public interface HostJoinDao extends GenericDao { HostResponse setHostResponse(HostResponse response, HostJoinVO host); + HostForMigrationResponse newHostForMigrationResponse(HostJoinVO host, EnumSet details); + + HostForMigrationResponse setHostForMigrationResponse(HostForMigrationResponse response, HostJoinVO host); + List newHostView(Host group); List searchByIds(Long... ids); diff --git a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java index 1adff40d70f..77965290c8f 100644 --- a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java +++ b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java @@ -29,6 +29,7 @@ import javax.inject.Inject; import org.apache.cloudstack.api.ApiConstants.HostDetails; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.HostForMigrationResponse; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -190,10 +191,6 @@ public class HostJoinDaoImpl extends GenericDaoBase implements } - - - - @Override public HostResponse setHostResponse(HostResponse response, HostJoinVO host) { String tag = host.getTag(); @@ -208,7 +205,137 @@ public class HostJoinDaoImpl extends GenericDaoBase implements return response; } + @Override + public HostForMigrationResponse newHostForMigrationResponse(HostJoinVO host, EnumSet details) { + HostForMigrationResponse hostResponse = new HostForMigrationResponse(); + hostResponse.setId(host.getUuid()); + hostResponse.setCapabilities(host.getCapabilities()); + hostResponse.setClusterId(host.getClusterUuid()); + hostResponse.setCpuNumber(host.getCpus()); + hostResponse.setZoneId(host.getZoneUuid()); + hostResponse.setDisconnectedOn(host.getDisconnectedOn()); + hostResponse.setHypervisor(host.getHypervisorType()); + hostResponse.setHostType(host.getType()); + hostResponse.setLastPinged(new Date(host.getLastPinged())); + hostResponse.setManagementServerId(host.getManagementServerId()); + hostResponse.setName(host.getName()); + hostResponse.setPodId(host.getPodUuid()); + hostResponse.setRemoved(host.getRemoved()); + hostResponse.setCpuSpeed(host.getSpeed()); + hostResponse.setState(host.getStatus()); + hostResponse.setIpAddress(host.getPrivateIpAddress()); + hostResponse.setVersion(host.getVersion()); + hostResponse.setCreated(host.getCreated()); + if (details.contains(HostDetails.all) || details.contains(HostDetails.capacity) + || details.contains(HostDetails.stats) || details.contains(HostDetails.events)) { + + hostResponse.setOsCategoryId(host.getOsCategoryUuid()); + hostResponse.setOsCategoryName(host.getOsCategoryName()); + hostResponse.setZoneName(host.getZoneName()); + hostResponse.setPodName(host.getPodName()); + if ( host.getClusterId() > 0) { + hostResponse.setClusterName(host.getClusterName()); + hostResponse.setClusterType(host.getClusterType().toString()); + } + } + + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + if (host.getType() == Host.Type.Routing) { + if (details.contains(HostDetails.all) || details.contains(HostDetails.capacity)) { + // set allocated capacities + Long mem = host.getMemReservedCapacity() + host.getMemUsedCapacity(); + Long cpu = host.getCpuReservedCapacity() + host.getCpuReservedCapacity(); + + hostResponse.setMemoryAllocated(mem); + hostResponse.setMemoryTotal(host.getTotalMemory()); + + String hostTags = host.getTag(); + hostResponse.setHostTags(host.getTag()); + + String haTag = ApiDBUtils.getHaTag(); + if (haTag != null && !haTag.isEmpty() && hostTags != null && !hostTags.isEmpty()) { + if (haTag.equalsIgnoreCase(hostTags)) { + hostResponse.setHaHost(true); + } else { + hostResponse.setHaHost(false); + } + } else { + hostResponse.setHaHost(false); + } + + hostResponse.setHypervisorVersion(host.getHypervisorVersion()); + + String cpuAlloc = decimalFormat.format(((float) cpu / (float) (host.getCpus() * host.getSpeed())) * 100f) + "%"; + hostResponse.setCpuAllocated(cpuAlloc); + String cpuWithOverprovisioning = new Float(host.getCpus() * host.getSpeed() * ApiDBUtils.getCpuOverprovisioningFactor()).toString(); + hostResponse.setCpuWithOverprovisioning(cpuWithOverprovisioning); + } + + if (details.contains(HostDetails.all) || details.contains(HostDetails.stats)) { + // set CPU/RAM/Network stats + String cpuUsed = null; + HostStats hostStats = ApiDBUtils.getHostStatistics(host.getId()); + if (hostStats != null) { + float cpuUtil = (float) hostStats.getCpuUtilization(); + cpuUsed = decimalFormat.format(cpuUtil) + "%"; + hostResponse.setCpuUsed(cpuUsed); + hostResponse.setMemoryUsed((new Double(hostStats.getUsedMemory())).longValue()); + hostResponse.setNetworkKbsRead((new Double(hostStats.getNetworkReadKBs())).longValue()); + hostResponse.setNetworkKbsWrite((new Double(hostStats.getNetworkWriteKBs())).longValue()); + + } + } + + } else if (host.getType() == Host.Type.SecondaryStorage) { + StorageStats secStorageStats = ApiDBUtils.getSecondaryStorageStatistics(host.getId()); + if (secStorageStats != null) { + hostResponse.setDiskSizeTotal(secStorageStats.getCapacityBytes()); + hostResponse.setDiskSizeAllocated(secStorageStats.getByteUsed()); + } + } + + hostResponse.setLocalStorageActive(ApiDBUtils.isLocalStorageActiveOnHost(host.getId())); + + if (details.contains(HostDetails.all) || details.contains(HostDetails.events)) { + Set possibleEvents = host.getStatus().getPossibleEvents(); + if ((possibleEvents != null) && !possibleEvents.isEmpty()) { + String events = ""; + Iterator iter = possibleEvents.iterator(); + while (iter.hasNext()) { + com.cloud.host.Status.Event event = iter.next(); + events += event.toString(); + if (iter.hasNext()) { + events += "; "; + } + } + hostResponse.setEvents(events); + } + } + + hostResponse.setResourceState(host.getResourceState().toString()); + + // set async job + hostResponse.setJobId(host.getJobUuid()); + hostResponse.setJobStatus(host.getJobStatus()); + + hostResponse.setObjectName("host"); + + return hostResponse; + } + + @Override + public HostForMigrationResponse setHostForMigrationResponse(HostForMigrationResponse response, HostJoinVO host) { + String tag = host.getTag(); + if (tag != null) { + if (response.getHostTags() != null && response.getHostTags().length() > 0) { + response.setHostTags(response.getHostTags() + "," + tag); + } else { + response.setHostTags(tag); + } + } + return response; + } @Override public List newHostView(Host host) { diff --git a/server/src/com/cloud/api/query/dao/StoragePoolJoinDao.java b/server/src/com/cloud/api/query/dao/StoragePoolJoinDao.java index bbb02428981..b7e467fd6d3 100644 --- a/server/src/com/cloud/api/query/dao/StoragePoolJoinDao.java +++ b/server/src/com/cloud/api/query/dao/StoragePoolJoinDao.java @@ -18,6 +18,7 @@ package com.cloud.api.query.dao; import java.util.List; +import org.apache.cloudstack.api.response.StoragePoolForMigrationResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import com.cloud.api.query.vo.StoragePoolJoinVO; @@ -30,6 +31,11 @@ public interface StoragePoolJoinDao extends GenericDao StoragePoolResponse setStoragePoolResponse(StoragePoolResponse response, StoragePoolJoinVO host); + StoragePoolForMigrationResponse newStoragePoolForMigrationResponse(StoragePoolJoinVO host); + + StoragePoolForMigrationResponse setStoragePoolForMigrationResponse(StoragePoolForMigrationResponse response, + StoragePoolJoinVO host); + List newStoragePoolView(StoragePool group); List searchByIds(Long... spIds); diff --git a/server/src/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java index 58968df2e56..34b88baa897 100644 --- a/server/src/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java +++ b/server/src/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java @@ -22,6 +22,7 @@ import java.util.List; import javax.ejb.Local; import javax.inject.Inject; +import org.apache.cloudstack.api.response.StoragePoolForMigrationResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -108,10 +109,6 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase 0){ + response.setTags(response.getTags() + "," + tag); + } else { + response.setTags(tag); + } + } + return response; + } + @Override public List newStoragePoolView(StoragePool host) { SearchCriteria sc = spIdSearch.create(); diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index db8db8a7194..16127a22d95 100755 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -214,6 +214,7 @@ import org.apache.cloudstack.api.command.admin.vlan.DeleteVlanIpRangeCmd; import org.apache.cloudstack.api.command.admin.vlan.ListVlanIpRangesCmd; import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; import org.apache.cloudstack.api.command.admin.vm.MigrateVMCmd; +import org.apache.cloudstack.api.command.admin.vm.MigrateVirtualMachineWithVolumeCmd; import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; import org.apache.cloudstack.api.command.admin.zone.CreateZoneCmd; import org.apache.cloudstack.api.command.admin.zone.DeleteZoneCmd; @@ -256,6 +257,7 @@ import org.apache.cloudstack.api.command.user.vmsnapshot.ListVMSnapshotCmd; import org.apache.cloudstack.api.command.user.vmsnapshot.RevertToSnapshotCmd; import org.apache.cloudstack.api.command.user.zone.ListZonesByCmd; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -329,6 +331,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject private StorageManager _storageMgr; @Inject + private VolumeManager _volumeMgr; + @Inject private VirtualMachineManager _itMgr; @Inject private HostPodDao _hostPodDao; @@ -352,10 +356,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe private LoadBalancerDao _loadbalancerDao; @Inject private HypervisorCapabilitiesDao _hypervisorCapabilitiesDao; - private List _hostAllocators; - - @Inject + @Inject + private List _storagePoolAllocators; + @Inject private ConfigurationManager _configMgr; @Inject private ResourceTagDao _resourceTagDao; @@ -679,12 +683,14 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe Object resourceState = cmd.getResourceState(); Object haHosts = cmd.getHaHost(); - Pair, Integer> result = searchForServers(cmd.getStartIndex(), cmd.getPageSizeVal(), name, type, state, zoneId, pod, cluster, id, keyword, resourceState, haHosts); + Pair, Integer> result = searchForServers(cmd.getStartIndex(), cmd.getPageSizeVal(), name, type, + state, zoneId, pod, cluster, id, keyword, resourceState, haHosts, null, null); return new Pair, Integer>(result.first(), result.second()); } @Override - public Pair, Integer>, List> listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize) { + public Ternary, Integer>, List, Map> + listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize) { // access check - only root admin can migrate VM Account caller = UserContext.current().getCaller(); if (caller.getType() != Account.ACCOUNT_TYPE_ADMIN) { @@ -700,12 +706,13 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe ex.addProxyObject(vm, vmId, "vmId"); throw ex; } - // business logic + if (vm.getState() != State.Running) { if (s_logger.isDebugEnabled()) { s_logger.debug("VM is not Running, unable to migrate the vm" + vm); } - InvalidParameterValueException ex = new InvalidParameterValueException("VM is not Running, unable to migrate the vm with specified id"); + InvalidParameterValueException ex = new InvalidParameterValueException("VM is not Running, unable to" + + " migrate the vm with specified id"); ex.addProxyObject(vm, vmId, "vmId"); throw ex; } @@ -715,17 +722,11 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe if (s_logger.isDebugEnabled()) { s_logger.debug(vm + " is not XenServer/VMware/KVM/OVM, cannot migrate this VM."); } - throw new InvalidParameterValueException("Unsupported Hypervisor Type for VM migration, we support XenServer/VMware/KVM only"); - } - ServiceOfferingVO svcOffering = _offeringsDao.findById(vm.getServiceOfferingId()); - if (svcOffering.getUseLocalStorage()) { - if (s_logger.isDebugEnabled()) { - s_logger.debug(vm + " is using Local Storage, cannot migrate this VM."); - } - throw new InvalidParameterValueException("Unsupported operation, VM uses Local storage, cannot migrate"); + throw new InvalidParameterValueException("Unsupported Hypervisor Type for VM migration, we support " + + "XenServer/VMware/KVM/Ovm only"); } + long srcHostId = vm.getHostId(); - // why is this not HostVO? Host srcHost = _hostDao.findById(srcHostId); if (srcHost == null) { if (s_logger.isDebugEnabled()) { @@ -737,32 +738,73 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe ex.addProxyObject(vm, vmId, "vmId"); throw ex; } - Long cluster = srcHost.getClusterId(); - Type hostType = srcHost.getType(); - if (s_logger.isDebugEnabled()) { - s_logger.debug("Searching for all hosts in cluster: " + cluster + " for migrating VM " + vm); + + // Check if the vm can be migrated with storage. + boolean canMigrateWithStorage = false; + HypervisorCapabilitiesVO capabilities = _hypervisorCapabilitiesDao.findByHypervisorTypeAndVersion( + srcHost.getHypervisorType(), srcHost.getHypervisorVersion()); + if (capabilities != null) { + canMigrateWithStorage = capabilities.isStorageMotionSupported(); } - Pair, Integer> allHostsInClusterPair = searchForServers(startIndex, pageSize, null, hostType, null, null, null, cluster, null, null, null, null); - - // filter out the current host - List allHostsInCluster = allHostsInClusterPair.first(); - allHostsInCluster.remove(srcHost); - Pair, Integer> otherHostsInCluster = new Pair, Integer>(allHostsInCluster, new Integer(allHostsInClusterPair.second().intValue()-1)); - - if (s_logger.isDebugEnabled()) { - s_logger.debug("Other Hosts in this cluster: " + allHostsInCluster); - } - - if (s_logger.isDebugEnabled()) { - s_logger.debug("Calling HostAllocators to search for hosts in cluster: " + cluster + " having enough capacity and suitable for migration"); - } - - List suitableHosts = new ArrayList(); - + // Check if the vm is using any disks on local storage. VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vm); + List volumes = _volumeDao.findCreatedByInstance(vmProfile.getId()); + boolean usesLocal = false; + for (VolumeVO volume : volumes) { + DiskOfferingVO diskOffering = _diskOfferingDao.findById(volume.getDiskOfferingId()); + DiskProfile diskProfile = new DiskProfile(volume, diskOffering, vmProfile.getHypervisorType()); + if (diskProfile.useLocalStorage()) { + usesLocal = true; + break; + } + } - DataCenterDeployment plan = new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(), null, null, null); + if (!canMigrateWithStorage && usesLocal) { + throw new InvalidParameterValueException("Unsupported operation, VM uses Local storage, cannot migrate"); + } + + Type hostType = srcHost.getType(); + Pair, Integer> allHostsPair = null; + List allHosts = null; + Map requiresStorageMotion = new HashMap(); + DataCenterDeployment plan = null; + if (canMigrateWithStorage) { + allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, srcHost.getDataCenterId(), null, + null, null, null, null, null, srcHost.getHypervisorType(), srcHost.getHypervisorVersion()); + allHosts = allHostsPair.first(); + allHosts.remove(srcHost); + + // Check if the host has storage pools for all the volumes of the vm to be migrated. + for (Host host : allHosts) { + Map> volumePools = findSuitablePoolsForVolumes(vmProfile, host); + if (volumePools.isEmpty()) { + allHosts.remove(host); + } else { + if (host.getClusterId() != srcHost.getClusterId() || usesLocal) { + requiresStorageMotion.put(host, true); + } + } + } + + plan = new DataCenterDeployment(srcHost.getDataCenterId(), null, null, null, null, null); + } else { + Long cluster = srcHost.getClusterId(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Searching for all hosts in cluster " + cluster + " for migrating VM " + vm); + } + allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, null, null, cluster, null, null, + null, null, null, null); + // Filter out the current host. + allHosts = allHostsPair.first(); + allHosts.remove(srcHost); + plan = new DataCenterDeployment(srcHost.getDataCenterId(), srcHost.getPodId(), srcHost.getClusterId(), + null, null, null); + } + + Pair, Integer> otherHosts = new Pair, Integer>(allHosts, + new Integer(allHosts.size())); + List suitableHosts = new ArrayList(); ExcludeList excludes = new ExcludeList(); excludes.addHost(srcHostId); @@ -776,25 +818,174 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } for (HostAllocator allocator : _hostAllocators) { - suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, HostAllocator.RETURN_UPTO_ALL, false); + if (canMigrateWithStorage) { + suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, allHosts, + HostAllocator.RETURN_UPTO_ALL, false); + } else { + suitableHosts = allocator.allocateTo(vmProfile, plan, Host.Type.Routing, excludes, + HostAllocator.RETURN_UPTO_ALL, false); + } + if (suitableHosts != null && !suitableHosts.isEmpty()) { break; } } - if (suitableHosts.isEmpty()) { - s_logger.debug("No suitable hosts found"); - } else { - if (s_logger.isDebugEnabled()) { + if (s_logger.isDebugEnabled()) { + if (suitableHosts.isEmpty()) { + s_logger.debug("No suitable hosts found"); + } else { s_logger.debug("Hosts having capacity and suitable for migration: " + suitableHosts); } } - return new Pair, Integer>, List>(otherHostsInCluster, suitableHosts); + return new Ternary, Integer>, List, Map> (otherHosts, + suitableHosts, requiresStorageMotion); + } + + private Map> findSuitablePoolsForVolumes(VirtualMachineProfile vmProfile, + Host host) { + List volumes = _volumeDao.findCreatedByInstance(vmProfile.getId()); + Map> suitableVolumeStoragePools = new HashMap>(); + + // For each volume find list of suitable storage pools by calling the allocators + for (VolumeVO volume : volumes) { + DiskOfferingVO diskOffering = _diskOfferingDao.findById(volume.getDiskOfferingId()); + DiskProfile diskProfile = new DiskProfile(volume, diskOffering, vmProfile.getHypervisorType()); + DataCenterDeployment plan = new DataCenterDeployment(host.getDataCenterId(), host.getPodId(), + host.getClusterId(), host.getId(), null, null); + ExcludeList avoid = new ExcludeList(); + + boolean foundPools = false; + for (StoragePoolAllocator allocator : _storagePoolAllocators) { + List poolList = allocator.allocateToPool(diskProfile, vmProfile, plan, avoid, + StoragePoolAllocator.RETURN_UPTO_ALL); + if (poolList != null && !poolList.isEmpty()) { + suitableVolumeStoragePools.put(volume, poolList); + foundPools = true; + break; + } + } + + if (!foundPools) { + suitableVolumeStoragePools.clear(); + break; + } + } + + return suitableVolumeStoragePools; + } + + @Override + public Pair, List> listStoragePoolsForMigrationOfVolume(Long volumeId) { + // Access check - only root administrator can migrate volumes. + Account caller = UserContext.current().getCaller(); + if (caller.getType() != Account.ACCOUNT_TYPE_ADMIN) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Caller is not a root admin, permission denied to migrate the volume"); + } + throw new PermissionDeniedException("No permission to migrate volume, only root admin can migrate a volume"); + } + + VolumeVO volume = _volumeDao.findById(volumeId); + if (volume == null) { + InvalidParameterValueException ex = new InvalidParameterValueException("Unable to find volume with" + + " specified id."); + ex.addProxyObject(volume, volumeId, "volumeId"); + throw ex; + } + + // Volume must be attached to an instance for live migration. + List allPools = new ArrayList(); + List suitablePools = new ArrayList(); + Long instanceId = volume.getInstanceId(); + VMInstanceVO vm = null; + if (instanceId != null) { + vm = _vmInstanceDao.findById(instanceId); + } + + // Check that the VM is in correct state. + if (vm == null || vm.getState() != State.Running) { + s_logger.info("Volume " + volume + " isn't attached to any running vm. Only volumes attached to a running" + + " VM can be migrated."); + return new Pair, List>(allPools, suitablePools); + } + + // Volume must be in Ready state to be migrated. + if (!Volume.State.Ready.equals(volume.getState())) { + s_logger.info("Volume " + volume + " must be in ready state for migration."); + return new Pair, List>(allPools, suitablePools); + } + + if (!_volumeMgr.volumeOnSharedStoragePool(volume)) { + s_logger.info("Volume " + volume + " is on local storage. It cannot be migrated to another pool."); + return new Pair, List>(allPools, suitablePools); + } + + // Check if the underlying hypervisor supports storage motion. + boolean storageMotionSupported = false; + Long hostId = vm.getHostId(); + if (hostId != null) { + HostVO host = _hostDao.findById(hostId); + HypervisorCapabilitiesVO capabilities = null; + if (host != null) { + capabilities = _hypervisorCapabilitiesDao.findByHypervisorTypeAndVersion(host.getHypervisorType(), + host.getHypervisorVersion()); + } else { + s_logger.error("Details of the host on which the vm " + vm + ", to which volume "+ volume + " is " + + "attached, couldn't be retrieved."); + } + + if (capabilities != null) { + storageMotionSupported = capabilities.isStorageMotionSupported(); + } else { + s_logger.error("Capabilities for host " + host + " couldn't be retrieved."); + } + } + + if (storageMotionSupported) { + // Source pool of the volume. + StoragePoolVO srcVolumePool = _poolDao.findById(volume.getPoolId()); + + // Get all the pools available. Only shared pools are considered because only a volume on a shared pools + // can be live migrated while the virtual machine stays on the same host. + List storagePools = _poolDao.findPoolsByTags(volume.getDataCenterId(), + volume.getPodId(), srcVolumePool.getClusterId(), null); + storagePools.remove(srcVolumePool); + for (StoragePoolVO pool : storagePools) { + if (pool.isShared()) { + allPools.add((StoragePool)this.dataStoreMgr.getPrimaryDataStore(pool.getId())); + } + } + + // Get all the suitable pools. + // Exclude the current pool from the list of pools to which the volume can be migrated. + ExcludeList avoid = new ExcludeList(); + avoid.addPool(srcVolumePool.getId()); + + // Volume stays in the same cluster after migration. + DataCenterDeployment plan = new DataCenterDeployment(volume.getDataCenterId(), volume.getPodId(), + srcVolumePool.getClusterId(), null, null, null); + VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); + + DiskOfferingVO diskOffering = _diskOfferingDao.findById(volume.getDiskOfferingId()); + DiskProfile diskProfile = new DiskProfile(volume, diskOffering, profile.getHypervisorType()); + + // Call the storage pool allocator to find the list of storage pools. + for (StoragePoolAllocator allocator : _storagePoolAllocators) { + List pools = allocator.allocateToPool(diskProfile, profile, plan, avoid, + StoragePoolAllocator.RETURN_UPTO_ALL); + if (pools != null && !pools.isEmpty()) { + suitablePools.addAll(pools); + break; + } + } + } + return new Pair, List>(allPools, suitablePools); } private Pair, Integer> searchForServers(Long startIndex, Long pageSize, Object name, Object type, Object state, Object zone, Object pod, Object cluster, Object id, Object keyword, - Object resourceState, Object haHosts) { + Object resourceState, Object haHosts, Object hypervisorType, Object hypervisorVersion) { Filter searchFilter = new Filter(HostVO.class, "id", Boolean.TRUE, startIndex, pageSize); SearchBuilder sb = _hostDao.createSearchBuilder(); @@ -806,6 +997,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe sb.and("podId", sb.entity().getPodId(), SearchCriteria.Op.EQ); sb.and("clusterId", sb.entity().getClusterId(), SearchCriteria.Op.EQ); sb.and("resourceState", sb.entity().getResourceState(), SearchCriteria.Op.EQ); + sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); + sb.and("hypervisorVersion", sb.entity().getHypervisorVersion(), SearchCriteria.Op.EQ); String haTag = _haMgr.getHaTag(); SearchBuilder hostTagSearch = null; @@ -855,6 +1048,12 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe if (cluster != null) { sc.setParameters("clusterId", cluster); } + if (hypervisorType != null) { + sc.setParameters("hypervisorType", hypervisorType); + } + if (hypervisorVersion != null) { + sc.setParameters("hypervisorVersion", hypervisorVersion); + } if (resourceState != null) { sc.setParameters("resourceState", resourceState); @@ -1969,6 +2168,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(CancelMaintenanceCmd.class); cmdList.add(DeleteHostCmd.class); cmdList.add(ListHostsCmd.class); + cmdList.add(FindHostsForMigrationCmd.class); cmdList.add(PrepareForMaintenanceCmd.class); cmdList.add(ReconnectHostCmd.class); cmdList.add(UpdateHostCmd.class); @@ -2025,6 +2225,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(DeletePoolCmd.class); cmdList.add(ListS3sCmd.class); cmdList.add(ListStoragePoolsCmd.class); + cmdList.add(FindStoragePoolsForMigrationCmd.class); cmdList.add(PreparePrimaryStorageForMaintenanceCmd.class); cmdList.add(UpdateStoragePoolCmd.class); cmdList.add(AddSwiftCmd.class); @@ -2064,6 +2265,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(ReleasePublicIpRangeCmd.class); cmdList.add(AssignVMCmd.class); cmdList.add(MigrateVMCmd.class); + cmdList.add(MigrateVirtualMachineWithVolumeCmd.class); cmdList.add(RecoverVMCmd.class); cmdList.add(CreatePrivateGatewayCmd.class); cmdList.add(CreateVPCOfferingCmd.class); diff --git a/server/src/com/cloud/storage/VolumeManager.java b/server/src/com/cloud/storage/VolumeManager.java index 2101038f8f3..d198e5dd7df 100644 --- a/server/src/com/cloud/storage/VolumeManager.java +++ b/server/src/com/cloud/storage/VolumeManager.java @@ -18,6 +18,8 @@ */ package com.cloud.storage; +import java.util.Map; + import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; @@ -25,12 +27,15 @@ import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.deploy.DeployDestination; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientStorageCapacityException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.Host; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.storage.Volume.Type; import com.cloud.user.Account; @@ -80,6 +85,9 @@ public interface VolumeManager extends VolumeApiService { Volume migrateVolume(MigrateVolumeCmd cmd); + void migrateVolumes(T vm, VirtualMachineTO vmTo, Host srcHost, Host destHost, + Map volumeToPool); + boolean storageMigration( VirtualMachineProfile vm, StoragePool destPool); diff --git a/server/src/com/cloud/storage/VolumeManagerImpl.java b/server/src/com/cloud/storage/VolumeManagerImpl.java index 1e8edafe4e2..e57d393eb2d 100644 --- a/server/src/com/cloud/storage/VolumeManagerImpl.java +++ b/server/src/com/cloud/storage/VolumeManagerImpl.java @@ -28,6 +28,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.HashMap; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -42,6 +43,7 @@ import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.api.command.user.volume.UploadVolumeCmd; +import org.apache.cloudstack.engine.subsystem.api.storage.CommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManager; @@ -68,6 +70,7 @@ import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.AttachVolumeAnswer; import com.cloud.agent.api.AttachVolumeCommand; +import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.agent.api.to.VolumeTO; import com.cloud.alert.AlertManager; import com.cloud.api.ApiDBUtils; @@ -102,11 +105,13 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.StorageUnavailableException; +import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; +import com.cloud.hypervisor.HypervisorCapabilitiesVO; import com.cloud.network.NetworkModel; import com.cloud.org.Grouping; import com.cloud.resource.ResourceManager; @@ -2003,7 +2008,7 @@ public class VolumeManagerImpl extends ManagerBase implements VolumeManager { public Volume migrateVolume(MigrateVolumeCmd cmd) { Long volumeId = cmd.getVolumeId(); Long storagePoolId = cmd.getStoragePoolId(); - + VolumeVO vol = _volsDao.findById(volumeId); if (vol == null) { throw new InvalidParameterValueException( @@ -2015,9 +2020,39 @@ public class VolumeManagerImpl extends ManagerBase implements VolumeManager { "Volume must be in ready state"); } - if (vol.getInstanceId() != null) { - throw new InvalidParameterValueException( - "Volume needs to be dettached from VM"); + boolean liveMigrateVolume = false; + Long instanceId = vol.getInstanceId(); + VMInstanceVO vm = null; + if (instanceId != null) { + vm = _vmInstanceDao.findById(instanceId); + } + + if (vm != null && vm.getState() == State.Running) { + // Check if the underlying hypervisor supports storage motion. + Long hostId = vm.getHostId(); + if (hostId != null) { + HostVO host = _hostDao.findById(hostId); + HypervisorCapabilitiesVO capabilities = null; + if (host != null) { + capabilities = _hypervisorCapabilitiesDao.findByHypervisorTypeAndVersion(host.getHypervisorType(), + host.getHypervisorVersion()); + } + + if (capabilities != null) { + liveMigrateVolume = capabilities.isStorageMotionSupported(); + } + } + } + + // If the disk is not attached to any VM then it can be moved. Otherwise, it needs to be attached to a vm + // running on a hypervisor that supports storage motion so that it be be migrated. + if (instanceId != null && !liveMigrateVolume) { + throw new InvalidParameterValueException("Volume needs to be detached from VM"); + } + + if (liveMigrateVolume && !cmd.isLiveMigrate()) { + throw new InvalidParameterValueException("The volume " + vol + "is attached to a vm and for migrating it " + + "the parameter livemigrate should be specified"); } StoragePool destPool = (StoragePool)this.dataStoreMgr.getDataStore(storagePoolId, DataStoreRole.Primary); @@ -2032,12 +2067,15 @@ public class VolumeManagerImpl extends ManagerBase implements VolumeManager { "Migration of volume from local storage pool is not supported"); } - Volume newVol = migrateVolume(vol, destPool); + Volume newVol = null; + if (liveMigrateVolume) { + newVol = liveMigrateVolume(vol, destPool); + } else { + newVol = migrateVolume(vol, destPool); + } return newVol; } - - @DB protected Volume migrateVolume(Volume volume, StoragePool destPool) { VolumeInfo vol = this.volFactory.getVolume(volume.getId()); @@ -2058,6 +2096,66 @@ public class VolumeManagerImpl extends ManagerBase implements VolumeManager { } } + @DB + protected Volume liveMigrateVolume(Volume volume, StoragePool destPool) { + VolumeInfo vol = this.volFactory.getVolume(volume.getId()); + AsyncCallFuture future = this.volService.migrateVolume(vol, (DataStore)destPool); + try { + VolumeApiResult result = future.get(); + if (result.isFailed()) { + s_logger.debug("migrate volume failed:" + result.getResult()); + return null; + } + return result.getVolume(); + } catch (InterruptedException e) { + s_logger.debug("migrate volume failed", e); + return null; + } catch (ExecutionException e) { + s_logger.debug("migrate volume failed", e); + return null; + } + } + + @Override + public void migrateVolumes(T vm, VirtualMachineTO vmTo, Host srcHost, Host destHost, + Map volumeToPool) { + // Check if all the vms being migrated belong to the vm. + // Check if the storage pool is of the right type. + // Create a VolumeInfo to DataStore map too. + Map volumeMap = new HashMap(); + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeVO volume = entry.getKey(); + StoragePoolVO storagePool = entry.getValue(); + StoragePool destPool = (StoragePool)this.dataStoreMgr.getDataStore(storagePool.getId(), + DataStoreRole.Primary); + + if (volume.getInstanceId() != vm.getId()) { + throw new CloudRuntimeException("Volume " + volume + " that has to be migrated doesn't belong to the" + + " instance " + vm); + } + + if (destPool == null) { + throw new CloudRuntimeException("Failed to find the destination storage pool " + storagePool.getId()); + } + + volumeMap.put(this.volFactory.getVolume(volume.getId()), (DataStore)destPool); + } + + AsyncCallFuture future = this.volService.migrateVolumes(volumeMap, vmTo, srcHost, destHost); + try { + CommandResult result = future.get(); + if (result.isFailed()) { + s_logger.debug("Failed to migrated vm " + vm + " along with its volumes. " + result.getResult()); + throw new CloudRuntimeException("Failed to migrated vm " + vm + " along with its volumes. " + + result.getResult()); + } + } catch (InterruptedException e) { + s_logger.debug("Failed to migrated vm " + vm + " along with its volumes.", e); + } catch (ExecutionException e) { + s_logger.debug("Failed to migrated vm " + vm + " along with its volumes.", e); + } + } + @Override public boolean storageMigration( VirtualMachineProfile vm, diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index 3ecdf426721..bc6237f6b62 100755 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -46,6 +46,7 @@ import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; import org.apache.cloudstack.engine.service.api.OrchestrationService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; @@ -108,6 +109,7 @@ import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.hypervisor.HypervisorCapabilitiesVO; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.network.Network; import com.cloud.network.Network.IpAddresses; @@ -3502,6 +3504,127 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Use return migratedVm; } + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_MIGRATE, eventDescription = "migrating VM", async = true) + public VirtualMachine migrateVirtualMachineWithVolume(Long vmId, Host destinationHost, + Map volumeToPool) throws ResourceUnavailableException, ConcurrentOperationException, + ManagementServerException, VirtualMachineMigrationException { + // Access check - only root administrator can migrate VM. + Account caller = UserContext.current().getCaller(); + if (caller.getType() != Account.ACCOUNT_TYPE_ADMIN) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Caller is not a root admin, permission denied to migrate the VM"); + } + throw new PermissionDeniedException("No permission to migrate VM, Only Root Admin can migrate a VM!"); + } + + VMInstanceVO vm = _vmInstanceDao.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find the vm by id " + vmId); + } + + if (vm.getState() != State.Running) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("VM is not Running, unable to migrate the vm " + vm); + } + CloudRuntimeException ex = new CloudRuntimeException("VM is not Running, unable to migrate the vm with" + + " specified id"); + ex.addProxyObject(vm, vmId, "vmId"); + throw ex; + } + + if (!vm.getHypervisorType().equals(HypervisorType.XenServer) && + !vm.getHypervisorType().equals(HypervisorType.VMware) && + !vm.getHypervisorType().equals(HypervisorType.KVM) && + !vm.getHypervisorType().equals(HypervisorType.Ovm)) { + throw new InvalidParameterValueException("Unsupported hypervisor type for vm migration, we support" + + " XenServer/VMware/KVM only"); + } + + long srcHostId = vm.getHostId(); + Host srcHost = _resourceMgr.getHost(srcHostId); + // Check if src and destination hosts are valid and migrating to same host + if (destinationHost.getId() == srcHostId) { + throw new InvalidParameterValueException("Cannot migrate VM, VM is already present on this host, please" + + " specify valid destination host to migrate the VM"); + } + + // Check if the source and destination hosts are of the same type and support storage motion. + if (!(srcHost.getHypervisorType().equals(destinationHost.getHypervisorType()) && + srcHost.getHypervisorVersion().equals(destinationHost.getHypervisorVersion()))) { + throw new CloudRuntimeException("The source and destination hosts are not of the same type and version. " + + "Source hypervisor type and version: " + srcHost.getHypervisorType().toString() + " " + + srcHost.getHypervisorVersion() + ", Destination hypervisor type and version: " + + destinationHost.getHypervisorType().toString() + " " + destinationHost.getHypervisorVersion()); + } + + HypervisorCapabilitiesVO capabilities = _hypervisorCapabilitiesDao.findByHypervisorTypeAndVersion( + srcHost.getHypervisorType(), srcHost.getHypervisorVersion()); + if (!capabilities.isStorageMotionSupported()) { + throw new CloudRuntimeException("Migration with storage isn't supported on hypervisor " + + srcHost.getHypervisorType() + " of version " + srcHost.getHypervisorVersion()); + } + + // Check if destination host is up. + if (destinationHost.getStatus() != com.cloud.host.Status.Up || + destinationHost.getResourceState() != ResourceState.Enabled){ + throw new CloudRuntimeException("Cannot migrate VM, destination host is not in correct state, has " + + "status: " + destinationHost.getStatus() + ", state: " + destinationHost.getResourceState()); + } + + List vmVolumes = _volsDao.findUsableVolumesForInstance(vm.getId()); + Map volToPoolObjectMap = new HashMap(); + if (!isVMUsingLocalStorage(vm) && destinationHost.getClusterId() == srcHost.getClusterId()) { + if (volumeToPool.isEmpty()) { + // If the destination host is in the same cluster and volumes do not have to be migrated across pools + // then fail the call. migrateVirtualMachine api should have been used. + throw new InvalidParameterValueException("Migration of the vm " + vm + "from host " + srcHost + + " to destination host " + destinationHost + " doesn't involve migrating the volumes."); + } + } + + if (!volumeToPool.isEmpty()) { + // Check if all the volumes and pools passed as parameters are valid. + for (Map.Entry entry : volumeToPool.entrySet()) { + VolumeVO volume = _volsDao.findByUuid(entry.getKey()); + StoragePoolVO pool = _storagePoolDao.findByUuid(entry.getValue()); + if (volume == null) { + throw new InvalidParameterValueException("There is no volume present with the given id " + + entry.getKey()); + } else if (pool == null) { + throw new InvalidParameterValueException("There is no storage pool present with the given id " + + entry.getValue()); + } else { + // Verify the volume given belongs to the vm. + if (!vmVolumes.contains(volume)) { + throw new InvalidParameterValueException("There volume " + volume + " doesn't belong to " + + "the virtual machine "+ vm + " that has to be migrated"); + } + volToPoolObjectMap.put(volume, pool); + } + } + } + + // Check if all the volumes are in the correct state. + for (VolumeVO volume : vmVolumes) { + if (volume.getState() != Volume.State.Ready) { + throw new CloudRuntimeException("Volume " + volume + " of the VM is not in Ready state. Cannot " + + "migrate the vm with its volumes."); + } + } + + // Check max guest vm limit for the destinationHost. + HostVO destinationHostVO = _hostDao.findById(destinationHost.getId()); + if(_capacityMgr.checkIfHostReachMaxGuestLimit(destinationHostVO)){ + throw new VirtualMachineMigrationException("Host name: " + destinationHost.getName() + ", hostId: " + + destinationHost.getId() + " already has max running vms (count includes system VMs). Cannot" + + " migrate to this host"); + } + + VMInstanceVO migratedVm = _itMgr.migrateWithStorage(vm, srcHostId, destinationHost.getId(), volToPoolObjectMap); + return migratedVm; + } + @DB @Override @ActionEvent(eventType = EventTypes.EVENT_VM_MOVE, eventDescription = "move VM to another user", async = false) diff --git a/server/src/com/cloud/vm/VirtualMachineManager.java b/server/src/com/cloud/vm/VirtualMachineManager.java index 4a30d97cc54..ea9f7bbb2c9 100644 --- a/server/src/com/cloud/vm/VirtualMachineManager.java +++ b/server/src/com/cloud/vm/VirtualMachineManager.java @@ -20,6 +20,7 @@ import java.net.URI; import java.util.List; import java.util.Map; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; @@ -41,6 +42,7 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; import com.cloud.user.Account; import com.cloud.user.User; import com.cloud.utils.Pair; @@ -109,6 +111,8 @@ public interface VirtualMachineManager extends Manager { T migrate(T vm, long srcHostId, DeployDestination dest) throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException, VirtualMachineMigrationException; + T migrateWithStorage(T vm, long srcId, long destId, Map volumeToPool) throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException, VirtualMachineMigrationException; + T reboot(T vm, Map params, User caller, Account account) throws InsufficientCapacityException, ResourceUnavailableException; T advanceReboot(T vm, Map params, User caller, Account account) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, OperationTimedoutException; diff --git a/server/src/com/cloud/vm/VirtualMachineManagerImpl.java b/server/src/com/cloud/vm/VirtualMachineManagerImpl.java index 19f40055d69..2ecece2891f 100755 --- a/server/src/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/server/src/com/cloud/vm/VirtualMachineManagerImpl.java @@ -38,7 +38,12 @@ import javax.naming.ConfigurationException; import com.cloud.capacity.CapacityManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import com.cloud.dc.*; import com.cloud.agent.api.*; @@ -50,6 +55,8 @@ import com.cloud.agent.Listener; import com.cloud.agent.api.StartupRoutingCommand.VmState; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.agent.api.to.VolumeTO; import com.cloud.agent.manager.Commands; import com.cloud.agent.manager.allocator.HostAllocator; import com.cloud.alert.AlertManager; @@ -57,6 +64,7 @@ import com.cloud.cluster.ClusterManager; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.dao.ConfigurationDao; +import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.HostPodVO; @@ -111,10 +119,13 @@ import com.cloud.storage.Volume; import com.cloud.storage.Volume.Type; import com.cloud.storage.VolumeManager; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSCategoryDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.StoragePoolHostDao; +import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.User; @@ -164,6 +175,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject protected ServiceOfferingDao _offeringDao; @Inject + protected DiskOfferingDao _diskOfferingDao; + @Inject protected VMTemplateDao _templateDao; @Inject protected UserDao _userDao; @@ -202,13 +215,19 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject protected DataCenterDao _dcDao; @Inject + protected ClusterDao _clusterDao; + @Inject protected PrimaryDataStoreDao _storagePoolDao; @Inject protected HypervisorGuruManager _hvGuruMgr; @Inject protected NetworkDao _networkDao; @Inject + protected StoragePoolHostDao _poolHostDao; + @Inject protected VMSnapshotDao _vmSnapshotDao; + @Inject + protected VolumeDataFactory volFactory; protected List _planners; public List getPlanners() { @@ -226,9 +245,15 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac this._hostAllocators = _hostAllocators; } - @Inject + @Inject + protected List _storagePoolAllocators; + + @Inject protected ResourceManager _resourceMgr; + @Inject + protected SnapshotManager _snapshotMgr; + @Inject protected VMSnapshotManager _vmSnapshotMgr = null; @Inject @@ -1427,6 +1452,189 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } } + private Map getPoolListForVolumesForMigration(VirtualMachineProfile profile, + Host host, Map volumeToPool) { + List allVolumes = _volsDao.findUsableVolumesForInstance(profile.getId()); + for (VolumeVO volume : allVolumes) { + StoragePoolVO pool = volumeToPool.get(volume); + DiskOfferingVO diskOffering = _diskOfferingDao.findById(volume.getDiskOfferingId()); + StoragePoolVO currentPool = _storagePoolDao.findById(volume.getPoolId()); + if (pool != null) { + // Check if pool is accessible from the destination host and disk offering with which the volume was + // created is compliant with the pool type. + if (_poolHostDao.findByPoolHost(pool.getId(), host.getId()) == null || + pool.isLocal() != diskOffering.getUseLocalStorage()) { + // Cannot find a pool for the volume. Throw an exception. + throw new CloudRuntimeException("Cannot migrate volume " + volume + " to storage pool " + pool + + " while migrating vm to host " + host + ". Either the pool is not accessible from the " + + "host or because of the offering with which the volume is created it cannot be placed on " + + "the given pool."); + } else if (pool.getId() == currentPool.getId()){ + // If the pool to migrate too is the same as current pool, remove the volume from the list of + // volumes to be migrated. + volumeToPool.remove(volume); + } + } else { + // Find a suitable pool for the volume. Call the storage pool allocator to find the list of pools. + DiskProfile diskProfile = new DiskProfile(volume, diskOffering, profile.getHypervisorType()); + DataCenterDeployment plan = new DataCenterDeployment(host.getDataCenterId(), host.getPodId(), + host.getClusterId(), host.getId(), null, null); + ExcludeList avoid = new ExcludeList(); + boolean currentPoolAvailable = false; + + for (StoragePoolAllocator allocator : _storagePoolAllocators) { + List poolList = allocator.allocateToPool(diskProfile, profile, plan, avoid, + StoragePoolAllocator.RETURN_UPTO_ALL); + if (poolList != null && !poolList.isEmpty()) { + // Volume needs to be migrated. Pick the first pool from the list. Add a mapping to migrate the + // volume to a pool only if it is required; that is the current pool on which the volume resides + // is not available on the destination host. + if (poolList.contains(currentPool)) { + currentPoolAvailable = true; + } else { + volumeToPool.put(volume, _storagePoolDao.findByUuid(poolList.get(0).getUuid())); + } + + break; + } + } + + if (!currentPoolAvailable && !volumeToPool.containsKey(volume)) { + // Cannot find a pool for the volume. Throw an exception. + throw new CloudRuntimeException("Cannot find a storage pool which is available for volume " + + volume + " while migrating virtual machine " + profile.getVirtualMachine() + " to host " + + host); + } + } + } + + return volumeToPool; + } + + private void moveVmToMigratingState(T vm, Long hostId, ItWorkVO work) + throws ConcurrentOperationException { + // Put the vm in migrating state. + try { + if (!changeState(vm, Event.MigrationRequested, hostId, work, Step.Migrating)) { + s_logger.info("Migration cancelled because state has changed: " + vm); + throw new ConcurrentOperationException("Migration cancelled because state has changed: " + vm); + } + } catch (NoTransitionException e) { + s_logger.info("Migration cancelled because " + e.getMessage()); + throw new ConcurrentOperationException("Migration cancelled because " + e.getMessage()); + } + } + + private void moveVmOutofMigratingStateOnSuccess(T vm, Long hostId, ItWorkVO work) + throws ConcurrentOperationException { + // Put the vm in running state. + try { + if (!changeState(vm, Event.OperationSucceeded, hostId, work, Step.Started)) { + s_logger.error("Unable to change the state for " + vm); + throw new ConcurrentOperationException("Unable to change the state for " + vm); + } + } catch (NoTransitionException e) { + s_logger.error("Unable to change state due to " + e.getMessage()); + throw new ConcurrentOperationException("Unable to change state due to " + e.getMessage()); + } + } + + @Override + public T migrateWithStorage(T vm, long srcHostId, long destHostId, + Map volumeToPool) throws ResourceUnavailableException, ConcurrentOperationException, + ManagementServerException, VirtualMachineMigrationException { + + HostVO srcHost = _hostDao.findById(srcHostId); + HostVO destHost = _hostDao.findById(destHostId); + VirtualMachineGuru vmGuru = getVmGuru(vm); + + DataCenterVO dc = _dcDao.findById(destHost.getDataCenterId()); + HostPodVO pod = _podDao.findById(destHost.getPodId()); + Cluster cluster = _clusterDao.findById(destHost.getClusterId()); + DeployDestination destination = new DeployDestination(dc, pod, cluster, destHost); + + // Create a map of which volume should go in which storage pool. + long vmId = vm.getId(); + vm = vmGuru.findById(vmId); + VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); + volumeToPool = getPoolListForVolumesForMigration(profile, destHost, volumeToPool); + + // If none of the volumes have to be migrated, fail the call. Administrator needs to make a call for migrating + // a vm and not migrating a vm with storage. + if (volumeToPool.isEmpty()) { + throw new InvalidParameterValueException("Migration of the vm " + vm + "from host " + srcHost + + " to destination host " + destHost + " doesn't involve migrating the volumes."); + } + + short alertType = AlertManager.ALERT_TYPE_USERVM_MIGRATE; + if (VirtualMachine.Type.DomainRouter.equals(vm.getType())) { + alertType = AlertManager.ALERT_TYPE_DOMAIN_ROUTER_MIGRATE; + } else if (VirtualMachine.Type.ConsoleProxy.equals(vm.getType())) { + alertType = AlertManager.ALERT_TYPE_CONSOLE_PROXY_MIGRATE; + } + + _networkMgr.prepareNicForMigration(profile, destination); + this.volumeMgr.prepareForMigration(profile, destination); + HypervisorGuru hvGuru = _hvGuruMgr.getGuru(vm.getHypervisorType()); + VirtualMachineTO to = hvGuru.implement(profile); + + ItWorkVO work = new ItWorkVO(UUID.randomUUID().toString(), _nodeId, State.Migrating, vm.getType(), vm.getId()); + work.setStep(Step.Prepare); + work.setResourceType(ItWorkVO.ResourceType.Host); + work.setResourceId(destHostId); + work = _workDao.persist(work); + + // Put the vm in migrating state. + vm.setLastHostId(srcHostId); + moveVmToMigratingState(vm, destHostId, work); + + boolean migrated = false; + try { + // Migrate the vm and its volume. + this.volumeMgr.migrateVolumes(vm, to, srcHost, destHost, volumeToPool); + + // Put the vm back to running state. + moveVmOutofMigratingStateOnSuccess(vm, destHost.getId(), work); + + try { + if (!checkVmOnHost(vm, destHostId)) { + s_logger.error("Vm not found on destination host. Unable to complete migration for " + vm); + try { + _agentMgr.send(srcHostId, new Commands(cleanup(vm.getInstanceName())), null); + } catch (AgentUnavailableException e) { + s_logger.error("AgentUnavailableException while cleanup on source host: " + srcHostId); + } + cleanup(vmGuru, new VirtualMachineProfileImpl(vm), work, Event.AgentReportStopped, true, + _accountMgr.getSystemUser(), _accountMgr.getSystemAccount()); + return null; + } + } catch (OperationTimedoutException e) { + s_logger.warn("Error while checking the vm " + vm + " is on host " + destHost, e); + } + + migrated = true; + return vm; + } finally { + if (!migrated) { + s_logger.info("Migration was unsuccessful. Cleaning up: " + vm); + _alertMgr.sendAlert(alertType, srcHost.getDataCenterId(), srcHost.getPodId(), "Unable to migrate vm " + + vm.getInstanceName() + " from host " + srcHost.getName() + " in zone " + dc.getName() + + " and pod " + dc.getName(), "Migrate Command failed. Please check logs."); + try { + _agentMgr.send(destHostId, new Commands(cleanup(vm.getInstanceName())), null); + stateTransitTo(vm, Event.OperationFailed, srcHostId); + } catch (AgentUnavailableException e) { + s_logger.warn("Looks like the destination Host is unavailable for cleanup.", e); + } catch (NoTransitionException e) { + s_logger.error("Error while transitioning vm from migrating to running state.", e); + } + } + + work.setStep(Step.Done); + _workDao.update(work.getId(), work); + } + } + @Override public VirtualMachineTO toVmTO(VirtualMachineProfile profile) { HypervisorGuru hvGuru = _hvGuruMgr.getGuru(profile.getVirtualMachine().getHypervisorType()); diff --git a/server/test/com/cloud/vm/MockUserVmManagerImpl.java b/server/test/com/cloud/vm/MockUserVmManagerImpl.java index fd826d9e86e..0d0a8f4a323 100644 --- a/server/test/com/cloud/vm/MockUserVmManagerImpl.java +++ b/server/test/com/cloud/vm/MockUserVmManagerImpl.java @@ -366,6 +366,14 @@ public class MockUserVmManagerImpl extends ManagerBase implements UserVmManager, return null; } + @Override + public VirtualMachine migrateVirtualMachineWithVolume(Long vmId, Host destinationHost, Map volumeToPool) + throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException, + VirtualMachineMigrationException { + // TODO Auto-generated method stub + return null; + } + @Override public UserVm moveVMToUser(AssignVMCmd moveUserVMCmd) throws ResourceAllocationException, ConcurrentOperationException, diff --git a/server/test/com/cloud/vm/MockVirtualMachineManagerImpl.java b/server/test/com/cloud/vm/MockVirtualMachineManagerImpl.java index 4917e77cdef..94ddea68879 100755 --- a/server/test/com/cloud/vm/MockVirtualMachineManagerImpl.java +++ b/server/test/com/cloud/vm/MockVirtualMachineManagerImpl.java @@ -23,6 +23,7 @@ import java.util.Map; import javax.ejb.Local; import javax.naming.ConfigurationException; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.springframework.stereotype.Component; import com.cloud.agent.api.to.NicTO; @@ -45,6 +46,7 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; import com.cloud.user.Account; import com.cloud.user.User; import com.cloud.utils.Pair; @@ -142,6 +144,14 @@ public class MockVirtualMachineManagerImpl extends ManagerBase implements Virtua return null; } + @Override + public T migrateWithStorage(T vm, long srcHostId, long destHostId, + Map volumeToPool) throws ResourceUnavailableException, + ConcurrentOperationException, ManagementServerException, VirtualMachineMigrationException { + // TODO Auto-generated method stub + return null; + } + @Override public VMInstanceVO findByIdAndType(Type type, long vmId) { // TODO Auto-generated method stub diff --git a/server/test/com/cloud/vm/VirtualMachineManagerImplTest.java b/server/test/com/cloud/vm/VirtualMachineManagerImplTest.java index 322f051c675..dd51e74a618 100644 --- a/server/test/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/server/test/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -31,6 +31,41 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.VolumeManager; import com.cloud.storage.VolumeVO; +import com.cloud.agent.api.PrepareForMigrationAnswer; +import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.MigrateWithStorageAnswer; +import com.cloud.agent.api.MigrateWithStorageCommand; +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.MigrateWithStorageCompleteAnswer; +import com.cloud.agent.api.MigrateWithStorageCompleteCommand; +import com.cloud.agent.api.CheckVirtualMachineAnswer; +import com.cloud.agent.api.CheckVirtualMachineCommand; +import com.cloud.capacity.CapacityManager; +import com.cloud.configuration.ConfigurationManager; +import com.cloud.configuration.dao.ConfigurationDao; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.HostPodDao; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.ManagementServerException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.exception.VirtualMachineMigrationException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.network.NetworkManager; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.StoragePoolHostVO; +import com.cloud.storage.VolumeManager; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.*; @@ -41,20 +76,33 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.snapshot.VMSnapshotManager; +import com.cloud.vm.VirtualMachine.Event; +import com.cloud.vm.VirtualMachine.State; + +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; + import org.junit.Test; import org.junit.Before; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.*; - import java.lang.reflect.Field; import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; public class VirtualMachineManagerImplTest { - @Spy VirtualMachineManagerImpl _vmMgr = new VirtualMachineManagerImpl(); @Mock VolumeManager _storageMgr; @@ -106,6 +154,25 @@ public class VirtualMachineManagerImplTest { List _rootVols; @Mock ItWorkVO _work; + + @Mock ClusterDao _clusterDao; + @Mock HostPodDao _podDao; + @Mock DataCenterDao _dcDao; + @Mock DiskOfferingDao _diskOfferingDao; + @Mock PrimaryDataStoreDao _storagePoolDao; + @Mock StoragePoolHostDao _poolHostDao; + @Mock NetworkManager _networkMgr; + @Mock HypervisorGuruManager _hvGuruMgr; + @Mock VMSnapshotManager _vmSnapshotMgr; + + // Mock objects for vm migration with storage test. + @Mock DiskOfferingVO _diskOfferingMock; + @Mock StoragePoolVO _srcStoragePoolMock; + @Mock StoragePoolVO _destStoragePoolMock; + @Mock HostVO _srcHostMock; + @Mock HostVO _destHostMock; + @Mock Map _volumeToPoolMock; + @Before public void setup(){ MockitoAnnotations.initMocks(this); @@ -122,6 +189,16 @@ public class VirtualMachineManagerImplTest { _vmMgr._nodeId = 1L; _vmMgr._workDao = _workDao; _vmMgr._agentMgr = _agentMgr; + _vmMgr._podDao = _podDao; + _vmMgr._clusterDao = _clusterDao; + _vmMgr._dcDao = _dcDao; + _vmMgr._diskOfferingDao = _diskOfferingDao; + _vmMgr._storagePoolDao = _storagePoolDao; + _vmMgr._poolHostDao= _poolHostDao; + _vmMgr._networkMgr = _networkMgr; + _vmMgr._hvGuruMgr = _hvGuruMgr; + _vmMgr._vmSnapshotMgr = _vmSnapshotMgr; + _vmMgr._vmDao = _vmInstanceDao; when(_vmMock.getId()).thenReturn(314l); when(_vmInstance.getId()).thenReturn(1L); @@ -204,5 +281,155 @@ public class VirtualMachineManagerImplTest { return serviceOffering; } + private void initializeMockConfigForMigratingVmWithVolumes() throws OperationTimedoutException, + ResourceUnavailableException { + // Mock the source and destination hosts. + when(_srcHostMock.getId()).thenReturn(5L); + when(_destHostMock.getId()).thenReturn(6L); + when(_hostDao.findById(5L)).thenReturn(_srcHostMock); + when(_hostDao.findById(6L)).thenReturn(_destHostMock); + + // Mock the vm being migrated. + when(_vmMock.getId()).thenReturn(1L); + when(_vmMock.getHypervisorType()).thenReturn(HypervisorType.XenServer); + when(_vmMock.getState()).thenReturn(State.Running).thenReturn(State.Running).thenReturn(State.Migrating) + .thenReturn(State.Migrating); + when(_vmMock.getHostId()).thenReturn(5L); + when(_vmInstance.getId()).thenReturn(1L); + when(_vmInstance.getServiceOfferingId()).thenReturn(2L); + when(_vmInstance.getInstanceName()).thenReturn("myVm"); + when(_vmInstance.getHostId()).thenReturn(5L); + when(_vmInstance.getType()).thenReturn(VirtualMachine.Type.User); + when(_vmInstance.getState()).thenReturn(State.Running).thenReturn(State.Running).thenReturn(State.Migrating) + .thenReturn(State.Migrating); + + // Mock the work item. + when(_workDao.persist(any(ItWorkVO.class))).thenReturn(_work); + when(_workDao.update("1", _work)).thenReturn(true); + when(_work.getId()).thenReturn("1"); + doNothing().when(_work).setStep(ItWorkVO.Step.Done); + + // Mock the vm guru and the user vm object that gets returned. + _vmMgr._vmGurus = new HashMap>(); + UserVmManagerImpl userVmManager = mock(UserVmManagerImpl.class); + _vmMgr.registerGuru(VirtualMachine.Type.User, userVmManager); + when(userVmManager.findById(anyLong())).thenReturn(_vmMock); + + // Mock the iteration over all the volumes of an instance. + Iterator volumeIterator = mock(Iterator.class); + when(_volsDao.findUsableVolumesForInstance(anyLong())).thenReturn(_rootVols); + when(_rootVols.iterator()).thenReturn(volumeIterator); + when(volumeIterator.hasNext()).thenReturn(true, false); + when(volumeIterator.next()).thenReturn(_volumeMock); + + // Mock the disk offering and pool objects for a volume. + when(_volumeMock.getDiskOfferingId()).thenReturn(5L); + when(_volumeMock.getPoolId()).thenReturn(200L); + when(_diskOfferingDao.findById(anyLong())).thenReturn(_diskOfferingMock); + when(_storagePoolDao.findById(anyLong())).thenReturn(_srcStoragePoolMock); + + // Mock the volume to pool mapping. + when(_volumeToPoolMock.get(_volumeMock)).thenReturn(_destStoragePoolMock); + when(_destStoragePoolMock.getId()).thenReturn(201L); + when(_srcStoragePoolMock.getId()).thenReturn(200L); + when(_destStoragePoolMock.isLocal()).thenReturn(false); + when(_diskOfferingMock.getUseLocalStorage()).thenReturn(false); + when(_poolHostDao.findByPoolHost(anyLong(), anyLong())).thenReturn(mock(StoragePoolHostVO.class)); + + // Mock hypervisor guru. + HypervisorGuru guruMock = mock(HypervisorGuru.class); + when(_hvGuruMgr.getGuru(HypervisorType.XenServer)).thenReturn(guruMock); + + when(_srcHostMock.getClusterId()).thenReturn(3L); + when(_destHostMock.getClusterId()).thenReturn(3L); + + // Mock the commands and answers to the agent. + PrepareForMigrationAnswer prepAnswerMock = mock(PrepareForMigrationAnswer.class); + when(prepAnswerMock.getResult()).thenReturn(true); + when(_agentMgr.send(anyLong(), isA(PrepareForMigrationCommand.class))).thenReturn(prepAnswerMock); + + MigrateWithStorageAnswer migAnswerMock = mock(MigrateWithStorageAnswer.class); + when(migAnswerMock.getResult()).thenReturn(true); + when(_agentMgr.send(anyLong(), isA(MigrateWithStorageCommand.class))).thenReturn(migAnswerMock); + + MigrateWithStorageReceiveAnswer migRecAnswerMock = mock(MigrateWithStorageReceiveAnswer.class); + when(migRecAnswerMock.getResult()).thenReturn(true); + when(_agentMgr.send(anyLong(), isA(MigrateWithStorageReceiveCommand.class))).thenReturn(migRecAnswerMock); + + MigrateWithStorageSendAnswer migSendAnswerMock = mock(MigrateWithStorageSendAnswer.class); + when(migSendAnswerMock.getResult()).thenReturn(true); + when(_agentMgr.send(anyLong(), isA(MigrateWithStorageSendCommand.class))).thenReturn(migSendAnswerMock); + + MigrateWithStorageCompleteAnswer migCompleteAnswerMock = mock(MigrateWithStorageCompleteAnswer.class); + when(migCompleteAnswerMock.getResult()).thenReturn(true); + when(_agentMgr.send(anyLong(), isA(MigrateWithStorageCompleteCommand.class))).thenReturn(migCompleteAnswerMock); + + CheckVirtualMachineAnswer checkVmAnswerMock = mock(CheckVirtualMachineAnswer.class); + when(checkVmAnswerMock.getResult()).thenReturn(true); + when(checkVmAnswerMock.getState()).thenReturn(State.Running); + when(_agentMgr.send(anyLong(), isA(CheckVirtualMachineCommand.class))).thenReturn(checkVmAnswerMock); + + // Mock the state transitions of vm. + Pair opaqueMock = new Pair (_vmMock.getHostId(), _destHostMock.getId()); + when(_vmSnapshotMgr.hasActiveVMSnapshotTasks(anyLong())).thenReturn(false); + when(_vmInstanceDao.updateState(State.Running, Event.MigrationRequested, State.Migrating, _vmMock, opaqueMock)) + .thenReturn(true); + when(_vmInstanceDao.updateState(State.Migrating, Event.OperationSucceeded, State.Running, _vmMock, opaqueMock)) + .thenReturn(true); + } + + // Check migration of a vm with its volumes within a cluster. + @Test + public void testMigrateWithVolumeWithinCluster() throws ResourceUnavailableException, ConcurrentOperationException, + ManagementServerException, VirtualMachineMigrationException, OperationTimedoutException { + + initializeMockConfigForMigratingVmWithVolumes(); + when(_srcHostMock.getClusterId()).thenReturn(3L); + when(_destHostMock.getClusterId()).thenReturn(3L); + + _vmMgr.migrateWithStorage(_vmInstance, _srcHostMock.getId(), _destHostMock.getId(), _volumeToPoolMock); + } + + // Check migration of a vm with its volumes across a cluster. + @Test + public void testMigrateWithVolumeAcrossCluster() throws ResourceUnavailableException, ConcurrentOperationException, + ManagementServerException, VirtualMachineMigrationException, OperationTimedoutException { + + initializeMockConfigForMigratingVmWithVolumes(); + when(_srcHostMock.getClusterId()).thenReturn(3L); + when(_destHostMock.getClusterId()).thenReturn(4L); + + _vmMgr.migrateWithStorage(_vmInstance, _srcHostMock.getId(), _destHostMock.getId(), _volumeToPoolMock); + } + + // Check migration of a vm fails when src and destination pool are not of same type; that is, one is shared and + // other is local. + @Test(expected=CloudRuntimeException.class) + public void testMigrateWithVolumeFail1() throws ResourceUnavailableException, ConcurrentOperationException, + ManagementServerException, VirtualMachineMigrationException, OperationTimedoutException { + + initializeMockConfigForMigratingVmWithVolumes(); + when(_srcHostMock.getClusterId()).thenReturn(3L); + when(_destHostMock.getClusterId()).thenReturn(3L); + + when(_destStoragePoolMock.isLocal()).thenReturn(true); + when(_diskOfferingMock.getUseLocalStorage()).thenReturn(false); + + _vmMgr.migrateWithStorage(_vmInstance, _srcHostMock.getId(), _destHostMock.getId(), _volumeToPoolMock); + } + + // Check migration of a vm fails when vm is not in Running state. + @Test(expected=ConcurrentOperationException.class) + public void testMigrateWithVolumeFail2() throws ResourceUnavailableException, ConcurrentOperationException, + ManagementServerException, VirtualMachineMigrationException, OperationTimedoutException { + + initializeMockConfigForMigratingVmWithVolumes(); + when(_srcHostMock.getClusterId()).thenReturn(3L); + when(_destHostMock.getClusterId()).thenReturn(3L); + + when(_vmMock.getState()).thenReturn(State.Stopped); + + _vmMgr.migrateWithStorage(_vmInstance, _srcHostMock.getId(), _destHostMock.getId(), _volumeToPoolMock); + } } diff --git a/setup/db/db/schema-410to420.sql b/setup/db/db/schema-410to420.sql index ce651a01a77..14aa2ebdefa 100644 --- a/setup/db/db/schema-410to420.sql +++ b/setup/db/db/schema-410to420.sql @@ -23,7 +23,9 @@ SET foreign_key_checks = 0; ALTER TABLE `cloud`.`hypervisor_capabilities` ADD COLUMN `max_hosts_per_cluster` int unsigned DEFAULT NULL COMMENT 'Max. hosts in cluster supported by hypervisor'; +ALTER TABLE `cloud`.`hypervisor_capabilities` ADD COLUMN `storage_motion_supported` int(1) unsigned DEFAULT 0 COMMENT 'Is storage motion supported'; UPDATE `cloud`.`hypervisor_capabilities` SET `max_hosts_per_cluster`=32 WHERE `hypervisor_type`='VMware'; +INSERT IGNORE INTO `cloud`.`hypervisor_capabilities`(hypervisor_type, hypervisor_version, max_guests_limit, security_group_enabled, max_data_volumes_limit, storage_motion_supported) VALUES ('XenServer', '6.1.0', 50, 1, 13, 1); INSERT IGNORE INTO `cloud`.`hypervisor_capabilities`(hypervisor_type, hypervisor_version, max_guests_limit, security_group_enabled, max_hosts_per_cluster) VALUES ('VMware', '5.1', 128, 0, 32); DELETE FROM `cloud`.`configuration` where name='vmware.percluster.host.max'; INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'AgentManager', 'xen.nics.max', '7', 'Maximum allowed nics for Vms created on Xen'); diff --git a/test/integration/component/test_storage_motion.py b/test/integration/component/test_storage_motion.py new file mode 100644 index 00000000000..cc55a084602 --- /dev/null +++ b/test/integration/component/test_storage_motion.py @@ -0,0 +1,298 @@ +# 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. +""" P1 tests for Storage motion +""" +#Import Local Modules +import marvin +from marvin.cloudstackTestCase import * +from marvin.cloudstackAPI import * +from marvin.remoteSSHClient import remoteSSHClient +from marvin.integration.lib.utils import * +from marvin.integration.lib.base import * +from marvin.integration.lib.common import * +from nose.plugins.attrib import attr +#Import System modules +import time + +_multiprocess_shared_ = True +class Services: + """Test VM Life Cycle Services + """ + + def __init__(self): + self.services = { + "disk_offering":{ + "displaytext": "Small", + "name": "Small", + "disksize": 1 + }, + "account": { + "email": "test@test.com", + "firstname": "Test", + "lastname": "User", + "username": "test", + # Random characters are appended in create account to + # ensure unique username generated each time + "password": "password", + }, + "small": + # Create a small virtual machine instance with disk offering + { + "displayname": "testserver", + "username": "root", # VM creds for SSH + "password": "password", + "ssh_port": 22, + "hypervisor": 'XenServer', + "privateport": 22, + "publicport": 22, + "protocol": 'TCP', + }, + "service_offerings": + { + "small": + { + # Small service offering ID to for change VM + # service offering from medium to small + "name": "Small Instance", + "displaytext": "Small Instance", + "cpunumber": 1, + "cpuspeed": 100, + "memory": 256, + } + }, + "template": { + "displaytext": "Cent OS Template", + "name": "Cent OS Template", + "passwordenabled": True, + }, + "diskdevice": '/dev/xvdd', + # Disk device where ISO is attached to instance + "mount_dir": "/mnt/tmp", + "sleep": 60, + "timeout": 10, + #Migrate VM to hostid + "ostype": 'CentOS 5.3 (64-bit)', + # CentOS 5.3 (64-bit) + } + +class TestStorageMotion(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.api_client = super(TestStorageMotion, cls).getClsTestClient().getApiClient() + cls.services = Services().services + + # Get Zone, Domain and templates + domain = get_domain(cls.api_client, cls.services) + zone = get_zone(cls.api_client, cls.services) + cls.services['mode'] = zone.networktype + + template = get_template( + cls.api_client, + zone.id, + cls.services["ostype"] + ) + # Set Zones and disk offerings + cls.services["small"]["zoneid"] = zone.id + cls.services["small"]["template"] = template.id + + # Create VMs, NAT Rules etc + cls.account = Account.create( + cls.api_client, + cls.services["account"], + domainid=domain.id + ) + + cls.small_offering = ServiceOffering.create( + cls.api_client, + cls.services["service_offerings"]["small"] + ) + + #create a virtual machine + cls.virtual_machine = VirtualMachine.create( + cls.api_client, + cls.services["small"], + accountid=cls.account.account.name, + domainid=cls.account.account.domainid, + serviceofferingid=cls.small_offering.id, + mode=cls.services["mode"] + ) + cls._cleanup = [ + cls.small_offering, + cls.account + ] + + @classmethod + def tearDownClass(cls): + cls.api_client = super(TestStorageMotion, cls).getClsTestClient().getApiClient() + cleanup_resources(cls.api_client, cls._cleanup) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + def tearDown(self): + #Clean up, terminate the created ISOs + cleanup_resources(self.apiclient, self.cleanup) + return + + @attr(tags = ["advanced", "basic", "multicluster", "storagemotion", "xenserver"]) + def test_01_migrate_vm_with_volume(self): + """Test migrate virtual machine with its volumes + """ + # Validate the following + # 1. List hosts for migration of a vm. Pick a host that + # requires storage motion too. + # 2. Migrate vm to a host. + # 3. listVM command should return this VM.State of this VM + # should be "Running" and the host should be the host + # to which the VM was migrated to in a different cluster + + hosts = Host.listForMigration( + self.apiclient, + virtualmachineid=self.virtual_machine.id + ) + + self.assertEqual( + isinstance(hosts, list), + True, + "Check the number of hosts in the zone" + ) + + # Migrate to a host that requires storage motion + hosts[:] = [host for host in hosts if host.requiresStorageMotion] + + host = hosts[0] + self.debug("Migrating VM-ID: %s to Host: %s" % ( + self.virtual_machine.id, + host.id + )) + + cmd = migrateVirtualMachineWithVolume.migrateVirtualMachineWithVolumeCmd() + cmd.hostid = host.id + cmd.virtualmachineid = self.virtual_machine.id + self.apiclient.migrateVirtualMachineWithVolume(cmd) + + list_vm_response = list_virtual_machines( + self.apiclient, + id=self.virtual_machine.id + ) + self.assertEqual( + isinstance(list_vm_response, list), + True, + "Check list response returns a valid list" + ) + + self.assertNotEqual( + list_vm_response, + None, + "Check virtual machine is listVirtualMachines" + ) + + vm_response = list_vm_response[0] + + self.assertEqual( + vm_response.id, + self.virtual_machine.id, + "Check virtual machine ID of migrated VM" + ) + + self.assertEqual( + vm_response.hostid, + host.id, + "Check destination hostID of migrated VM" + ) + + self.assertEqual( + vm_response.state, + 'Running', + "Check the state of VM" + ) + return + + @attr(tags = ["advanced", "basic", "multipool", "storagemotion", "xenserver"]) + def test_02_migrate_volume(self): + """Test migrate volume of a running vm + """ + # Validate the following + # 1. List all the volumes of a vm. For each volume do step 2 to 4. + # 2. List storage pools for migrating volume of a vm. Multiple + # storage pools should be present in the cluster. + # 3. Migrate volume of the vm to another pool. + # 4. Check volume is present in the new pool and is in Ready state. + + list_volumes_response = list_volumes( + self.apiclient, + virtualmachineid=self.virtual_machine.id, + listall=True + ) + self.assertEqual( + isinstance(list_volumes_response, list), + True, + "Check list volumes response for valid list" + ) + self.assertNotEqual( + list_volumes_response, + None, + "Check if volume exists in ListVolumes" + ) + + for volume in list_volumes_response: + pools = StoragePool.listForMigration( + self.apiclient, + id=volume.id + ) + pool = pools[0] + self.debug("Migrating Volume-ID: %s to Pool: %s" % ( + volume.id, + pool.id + )) + Volume.migrate( + self.apiclient, + volumeid=volume.id, + storageid=pool.id, + livemigrate='true' + ) + migrated_volume_response = list_volumes( + self.apiclient, + id=volume.id + ) + self.assertEqual( + isinstance(migrated_volume_response, list), + True, + "Check list volumes response for valid list" + ) + self.assertNotEqual( + migrated_volume_response, + None, + "Check if volume exists in ListVolumes" + ) + migrated_volume = migrated_volume_response[0] + self.assertEqual( + migrated_volume.state, + 'Ready', + "Check migrated volume is in Ready state" + ) + self.assertEqual( + migrated_volume.storage, + pool.name, + "Check volume is on migrated pool" + ) + + return \ No newline at end of file diff --git a/tools/marvin/marvin/integration/lib/base.py b/tools/marvin/marvin/integration/lib/base.py index 0185c87fed9..915d4783a12 100755 --- a/tools/marvin/marvin/integration/lib/base.py +++ b/tools/marvin/marvin/integration/lib/base.py @@ -539,6 +539,13 @@ class Volume: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.resizeVolume(cmd)) + @classmethod + def migrate(cls, apiclient, **kwargs): + """Migrate a volume""" + cmd = migrateVolume.migrateVolumeCmd() + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.migrateVolume(cmd)) + class Snapshot: """Manage Snapshot Lifecycle """ @@ -1493,6 +1500,14 @@ class Host: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listHosts(cmd)) + @classmethod + def listForMigration(cls, apiclient, **kwargs): + """List all Hosts for migration matching criteria""" + + cmd = findHostsForMigration.findHostsForMigrationCmd() + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.findHostsForMigration(cmd)) + class StoragePool: """Manage Storage pools (Primary Storage)""" @@ -1554,6 +1569,13 @@ class StoragePool: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listStoragePools(cmd)) + @classmethod + def listForMigration(cls, apiclient, **kwargs): + """List all storage pools for migration matching criteria""" + + cmd = findStoragePoolsForMigration.findStoragePoolsForMigrationCmd() + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.findStoragePoolsForMigration(cmd)) class Network: """Manage Network pools"""