Skip to content

Commit e6dccd4

Browse files
Pearl1594weizhouapache
authored andcommitted
add support for live migration
1 parent 71f47d6 commit e6dccd4

File tree

8 files changed

+441
-11
lines changed

8 files changed

+441
-11
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
20+
package com.cloud.agent.api;
21+
22+
/**
23+
* Answer for PostMigrationCommand.
24+
* Indicates success or failure of post-migration operations on the destination host.
25+
*/
26+
public class PostMigrationAnswer extends Answer {
27+
28+
protected PostMigrationAnswer() {
29+
}
30+
31+
public PostMigrationAnswer(PostMigrationCommand cmd, String detail) {
32+
super(cmd, false, detail);
33+
}
34+
35+
public PostMigrationAnswer(PostMigrationCommand cmd, Exception ex) {
36+
super(cmd, ex);
37+
}
38+
39+
public PostMigrationAnswer(PostMigrationCommand cmd) {
40+
super(cmd, true, null);
41+
}
42+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
20+
package com.cloud.agent.api;
21+
22+
import com.cloud.agent.api.to.VirtualMachineTO;
23+
24+
/**
25+
* PostMigrationCommand is sent to the destination host after a successful VM migration.
26+
* It performs post-migration tasks such as:
27+
* - Claiming exclusive locks on CLVM volumes (converting from shared to exclusive mode)
28+
* - Other post-migration cleanup operations
29+
*/
30+
public class PostMigrationCommand extends Command {
31+
private VirtualMachineTO vm;
32+
private String vmName;
33+
34+
protected PostMigrationCommand() {
35+
}
36+
37+
public PostMigrationCommand(VirtualMachineTO vm, String vmName) {
38+
this.vm = vm;
39+
this.vmName = vmName;
40+
}
41+
42+
public VirtualMachineTO getVirtualMachine() {
43+
return vm;
44+
}
45+
46+
public String getVmName() {
47+
return vmName;
48+
}
49+
50+
@Override
51+
public boolean executeInSequence() {
52+
return true;
53+
}
54+
}

engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import javax.persistence.EntityExistsException;
5151

5252

53+
import com.cloud.agent.api.PostMigrationCommand;
5354
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
5455
import org.apache.cloudstack.annotation.AnnotationService;
5556
import org.apache.cloudstack.annotation.dao.AnnotationDao;
@@ -3223,6 +3224,22 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy
32233224
logger.warn("Error while checking the vm {} on host {}", vm, dest.getHost(), e);
32243225
}
32253226
migrated = true;
3227+
try {
3228+
logger.info("Executing post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostId);
3229+
final PostMigrationCommand postMigrationCommand = new PostMigrationCommand(to, vm.getInstanceName());
3230+
final Answer postMigrationAnswer = _agentMgr.send(dstHostId, postMigrationCommand);
3231+
3232+
if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) {
3233+
final String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned";
3234+
logger.warn("Post-migration tasks failed for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.",
3235+
vm.getInstanceName(), dstHostId, details);
3236+
} else {
3237+
logger.info("Successfully completed post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostId);
3238+
}
3239+
} catch (Exception e) {
3240+
logger.warn("Exception during post-migration tasks for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.",
3241+
vm.getInstanceName(), dstHostId, e.getMessage(), e);
3242+
}
32263243
} finally {
32273244
if (!migrated) {
32283245
logger.info("Migration was unsuccessful. Cleaning up: {}", vm);

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6482,4 +6482,198 @@ public String getHypervisorPath() {
64826482
public String getGuestCpuArch() {
64836483
return guestCpuArch;
64846484
}
6485+
6486+
/**
6487+
* CLVM volume state for migration operations on source host
6488+
*/
6489+
public enum ClvmVolumeState {
6490+
/** Shared mode (-asy) - used before migration to allow both hosts to access volume */
6491+
SHARED("-asy", "shared", "Before migration: activating in shared mode"),
6492+
6493+
/** Deactivate (-an) - used after successful migration to release volume on source */
6494+
DEACTIVATE("-an", "deactivated", "After successful migration: deactivating volume"),
6495+
6496+
/** Exclusive mode (-aey) - used after failed migration to revert to original exclusive state */
6497+
EXCLUSIVE("-aey", "exclusive", "After failed migration: reverting to exclusive mode");
6498+
6499+
private final String lvchangeFlag;
6500+
private final String description;
6501+
private final String logMessage;
6502+
6503+
ClvmVolumeState(String lvchangeFlag, String description, String logMessage) {
6504+
this.lvchangeFlag = lvchangeFlag;
6505+
this.description = description;
6506+
this.logMessage = logMessage;
6507+
}
6508+
6509+
public String getLvchangeFlag() {
6510+
return lvchangeFlag;
6511+
}
6512+
6513+
public String getDescription() {
6514+
return description;
6515+
}
6516+
6517+
public String getLogMessage() {
6518+
return logMessage;
6519+
}
6520+
}
6521+
6522+
public static void modifyClvmVolumesStateForMigration(List<DiskDef> disks, LibvirtComputingResource resource,
6523+
VirtualMachineTO vmSpec, ClvmVolumeState state) {
6524+
for (DiskDef disk : disks) {
6525+
if (isClvmVolume(disk, resource, vmSpec)) {
6526+
String volumePath = disk.getDiskPath();
6527+
try {
6528+
LOGGER.info("[CLVM Migration] {} for volume [{}]",
6529+
state.getLogMessage(), volumePath);
6530+
6531+
Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER);
6532+
cmd.add(state.getLvchangeFlag());
6533+
cmd.add(volumePath);
6534+
6535+
String result = cmd.execute();
6536+
if (result != null) {
6537+
LOGGER.error("[CLVM Migration] Failed to set volume [{}] to {} state. Command result: {}",
6538+
volumePath, state.getDescription(), result);
6539+
} else {
6540+
LOGGER.info("[CLVM Migration] Successfully set volume [{}] to {} state.",
6541+
volumePath, state.getDescription());
6542+
}
6543+
} catch (Exception e) {
6544+
LOGGER.error("[CLVM Migration] Exception while setting volume [{}] to {} state: {}",
6545+
volumePath, state.getDescription(), e.getMessage(), e);
6546+
}
6547+
}
6548+
}
6549+
}
6550+
6551+
/**
6552+
* Determines if a disk is on a CLVM storage pool by checking the actual pool type from VirtualMachineTO.
6553+
* This is the most reliable method as it uses CloudStack's own storage pool information.
6554+
*
6555+
* @param disk The disk definition to check
6556+
* @param resource The LibvirtComputingResource instance (unused but kept for compatibility)
6557+
* @param vmSpec The VirtualMachineTO specification containing disk and pool information
6558+
* @return true if the disk is on a CLVM storage pool, false otherwise
6559+
*/
6560+
private static boolean isClvmVolume(DiskDef disk, LibvirtComputingResource resource, VirtualMachineTO vmSpec) {
6561+
String diskPath = disk.getDiskPath();
6562+
if (diskPath == null || vmSpec == null) {
6563+
return false;
6564+
}
6565+
6566+
try {
6567+
if (vmSpec.getDisks() != null) {
6568+
for (DiskTO diskTO : vmSpec.getDisks()) {
6569+
if (diskTO.getData() instanceof VolumeObjectTO) {
6570+
VolumeObjectTO volumeTO = (VolumeObjectTO) diskTO.getData();
6571+
if (diskPath.equals(volumeTO.getPath()) || diskPath.equals(diskTO.getPath())) {
6572+
DataStoreTO dataStore = volumeTO.getDataStore();
6573+
if (dataStore instanceof PrimaryDataStoreTO) {
6574+
PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) dataStore;
6575+
boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType();
6576+
LOGGER.debug("Disk {} identified as CLVM={} via VirtualMachineTO pool type: {}",
6577+
diskPath, isClvm, primaryStore.getPoolType());
6578+
return isClvm;
6579+
}
6580+
}
6581+
}
6582+
}
6583+
}
6584+
6585+
// Fallback: Check VG attributes using vgs command (reliable)
6586+
// CLVM VGs have the 'c' (clustered) or 's' (shared) flag in their attributes
6587+
// Example: 'wz--ns' = shared, 'wz--n-' = not clustered
6588+
if (diskPath.startsWith("/dev/") && !diskPath.contains("/dev/mapper/")) {
6589+
String vgName = extractVolumeGroupFromPath(diskPath);
6590+
if (vgName != null) {
6591+
boolean isClustered = checkIfVolumeGroupIsClustered(vgName);
6592+
LOGGER.debug("Disk {} VG {} identified as clustered={} via vgs attribute check",
6593+
diskPath, vgName, isClustered);
6594+
return isClustered;
6595+
}
6596+
}
6597+
6598+
} catch (Exception e) {
6599+
LOGGER.error("Error determining if volume {} is CLVM: {}", diskPath, e.getMessage(), e);
6600+
}
6601+
6602+
return false;
6603+
}
6604+
6605+
/**
6606+
* Extracts the volume group name from a device path.
6607+
*
6608+
* @param devicePath The device path (e.g., /dev/vgname/lvname)
6609+
* @return The volume group name, or null if cannot be determined
6610+
*/
6611+
static String extractVolumeGroupFromPath(String devicePath) {
6612+
if (devicePath == null || !devicePath.startsWith("/dev/")) {
6613+
return null;
6614+
}
6615+
6616+
// Format: /dev/<vgname>/<lvname>
6617+
String[] parts = devicePath.split("/");
6618+
if (parts.length >= 3) {
6619+
return parts[2]; // ["", "dev", "vgname", ...]
6620+
}
6621+
6622+
return null;
6623+
}
6624+
6625+
/**
6626+
* Checks if a volume group is clustered (CLVM) by examining its attributes.
6627+
* Uses 'vgs' command to check for the clustered/shared flag in VG attributes.
6628+
*
6629+
* VG Attr format (6 characters): wz--nc or wz--ns
6630+
* Position 6: Clustered flag - 'c' = CLVM (clustered), 's' = shared (lvmlockd), '-' = not clustered
6631+
*
6632+
* @param vgName The volume group name
6633+
* @return true if the VG is clustered or shared, false otherwise
6634+
*/
6635+
static boolean checkIfVolumeGroupIsClustered(String vgName) {
6636+
if (vgName == null) {
6637+
return false;
6638+
}
6639+
6640+
try {
6641+
// Use vgs with --noheadings and -o attr to get VG attributes
6642+
OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
6643+
Script vgsCmd = new Script("vgs", 5000, LOGGER);
6644+
vgsCmd.add("--noheadings");
6645+
vgsCmd.add("--unbuffered");
6646+
vgsCmd.add("-o");
6647+
vgsCmd.add("vg_attr");
6648+
vgsCmd.add(vgName);
6649+
6650+
String result = vgsCmd.execute(parser);
6651+
6652+
if (result == null && parser.getLines() != null) {
6653+
String output = parser.getLines();
6654+
if (output != null && !output.isEmpty()) {
6655+
// Parse VG attributes (format: wz--nc or wz--ns or wz--n-)
6656+
// Position 6 (0-indexed 5) indicates clustering/sharing:
6657+
// 'c' = clustered (CLVM) or 's' = shared (lvmlockd) or '-' = not clustered/shared
6658+
String vgAttr = output.trim();
6659+
if (vgAttr.length() >= 6) {
6660+
char clusterFlag = vgAttr.charAt(5); // Position 6 (0-indexed 5)
6661+
boolean isClustered = (clusterFlag == 'c' || clusterFlag == 's');
6662+
LOGGER.debug("VG {} has attributes '{}', cluster/shared flag '{}' = {}",
6663+
vgName, vgAttr, clusterFlag, isClustered);
6664+
return isClustered;
6665+
} else {
6666+
LOGGER.warn("VG {} attributes '{}' have unexpected format (expected 6+ chars)", vgName, vgAttr);
6667+
}
6668+
}
6669+
} else {
6670+
LOGGER.warn("Failed to get VG attributes for {}: {}", vgName, result);
6671+
}
6672+
6673+
} catch (Exception e) {
6674+
LOGGER.debug("Error checking if VG {} is clustered: {}", vgName, e.getMessage());
6675+
}
6676+
6677+
return false;
6678+
}
64856679
}

0 commit comments

Comments
 (0)