Skip to content

Commit 3f900e8

Browse files
committed
add support for live migration
1 parent 43e9384 commit 3f900e8

File tree

9 files changed

+439
-12
lines changed

9 files changed

+439
-12
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;
@@ -3238,6 +3239,22 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy
32383239
logger.warn("Error while checking the vm {} on host {}", vm, dest.getHost(), e);
32393240
}
32403241
migrated = true;
3242+
try {
3243+
logger.info("Executing post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostId);
3244+
final PostMigrationCommand postMigrationCommand = new PostMigrationCommand(to, vm.getInstanceName());
3245+
final Answer postMigrationAnswer = _agentMgr.send(dstHostId, postMigrationCommand);
3246+
3247+
if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) {
3248+
final String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned";
3249+
logger.warn("Post-migration tasks failed for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.",
3250+
vm.getInstanceName(), dstHostId, details);
3251+
} else {
3252+
logger.info("Successfully completed post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostId);
3253+
}
3254+
} catch (Exception e) {
3255+
logger.warn("Exception during post-migration tasks for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.",
3256+
vm.getInstanceName(), dstHostId, e.getMessage(), e);
3257+
}
32413258
} finally {
32423259
if (!migrated) {
32433260
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
@@ -6523,4 +6523,198 @@ public String getHypervisorPath() {
65236523
public String getGuestCpuArch() {
65246524
return guestCpuArch;
65256525
}
6526+
6527+
/**
6528+
* CLVM volume state for migration operations on source host
6529+
*/
6530+
public enum ClvmVolumeState {
6531+
/** Shared mode (-asy) - used before migration to allow both hosts to access volume */
6532+
SHARED("-asy", "shared", "Before migration: activating in shared mode"),
6533+
6534+
/** Deactivate (-an) - used after successful migration to release volume on source */
6535+
DEACTIVATE("-an", "deactivated", "After successful migration: deactivating volume"),
6536+
6537+
/** Exclusive mode (-aey) - used after failed migration to revert to original exclusive state */
6538+
EXCLUSIVE("-aey", "exclusive", "After failed migration: reverting to exclusive mode");
6539+
6540+
private final String lvchangeFlag;
6541+
private final String description;
6542+
private final String logMessage;
6543+
6544+
ClvmVolumeState(String lvchangeFlag, String description, String logMessage) {
6545+
this.lvchangeFlag = lvchangeFlag;
6546+
this.description = description;
6547+
this.logMessage = logMessage;
6548+
}
6549+
6550+
public String getLvchangeFlag() {
6551+
return lvchangeFlag;
6552+
}
6553+
6554+
public String getDescription() {
6555+
return description;
6556+
}
6557+
6558+
public String getLogMessage() {
6559+
return logMessage;
6560+
}
6561+
}
6562+
6563+
public static void modifyClvmVolumesStateForMigration(List<DiskDef> disks, LibvirtComputingResource resource,
6564+
VirtualMachineTO vmSpec, ClvmVolumeState state) {
6565+
for (DiskDef disk : disks) {
6566+
if (isClvmVolume(disk, resource, vmSpec)) {
6567+
String volumePath = disk.getDiskPath();
6568+
try {
6569+
LOGGER.info("[CLVM Migration] {} for volume [{}]",
6570+
state.getLogMessage(), volumePath);
6571+
6572+
Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER);
6573+
cmd.add(state.getLvchangeFlag());
6574+
cmd.add(volumePath);
6575+
6576+
String result = cmd.execute();
6577+
if (result != null) {
6578+
LOGGER.error("[CLVM Migration] Failed to set volume [{}] to {} state. Command result: {}",
6579+
volumePath, state.getDescription(), result);
6580+
} else {
6581+
LOGGER.info("[CLVM Migration] Successfully set volume [{}] to {} state.",
6582+
volumePath, state.getDescription());
6583+
}
6584+
} catch (Exception e) {
6585+
LOGGER.error("[CLVM Migration] Exception while setting volume [{}] to {} state: {}",
6586+
volumePath, state.getDescription(), e.getMessage(), e);
6587+
}
6588+
}
6589+
}
6590+
}
6591+
6592+
/**
6593+
* Determines if a disk is on a CLVM storage pool by checking the actual pool type from VirtualMachineTO.
6594+
* This is the most reliable method as it uses CloudStack's own storage pool information.
6595+
*
6596+
* @param disk The disk definition to check
6597+
* @param resource The LibvirtComputingResource instance (unused but kept for compatibility)
6598+
* @param vmSpec The VirtualMachineTO specification containing disk and pool information
6599+
* @return true if the disk is on a CLVM storage pool, false otherwise
6600+
*/
6601+
private static boolean isClvmVolume(DiskDef disk, LibvirtComputingResource resource, VirtualMachineTO vmSpec) {
6602+
String diskPath = disk.getDiskPath();
6603+
if (diskPath == null || vmSpec == null) {
6604+
return false;
6605+
}
6606+
6607+
try {
6608+
if (vmSpec.getDisks() != null) {
6609+
for (DiskTO diskTO : vmSpec.getDisks()) {
6610+
if (diskTO.getData() instanceof VolumeObjectTO) {
6611+
VolumeObjectTO volumeTO = (VolumeObjectTO) diskTO.getData();
6612+
if (diskPath.equals(volumeTO.getPath()) || diskPath.equals(diskTO.getPath())) {
6613+
DataStoreTO dataStore = volumeTO.getDataStore();
6614+
if (dataStore instanceof PrimaryDataStoreTO) {
6615+
PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) dataStore;
6616+
boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType();
6617+
LOGGER.debug("Disk {} identified as CLVM={} via VirtualMachineTO pool type: {}",
6618+
diskPath, isClvm, primaryStore.getPoolType());
6619+
return isClvm;
6620+
}
6621+
}
6622+
}
6623+
}
6624+
}
6625+
6626+
// Fallback: Check VG attributes using vgs command (reliable)
6627+
// CLVM VGs have the 'c' (clustered) or 's' (shared) flag in their attributes
6628+
// Example: 'wz--ns' = shared, 'wz--n-' = not clustered
6629+
if (diskPath.startsWith("/dev/") && !diskPath.contains("/dev/mapper/")) {
6630+
String vgName = extractVolumeGroupFromPath(diskPath);
6631+
if (vgName != null) {
6632+
boolean isClustered = checkIfVolumeGroupIsClustered(vgName);
6633+
LOGGER.debug("Disk {} VG {} identified as clustered={} via vgs attribute check",
6634+
diskPath, vgName, isClustered);
6635+
return isClustered;
6636+
}
6637+
}
6638+
6639+
} catch (Exception e) {
6640+
LOGGER.error("Error determining if volume {} is CLVM: {}", diskPath, e.getMessage(), e);
6641+
}
6642+
6643+
return false;
6644+
}
6645+
6646+
/**
6647+
* Extracts the volume group name from a device path.
6648+
*
6649+
* @param devicePath The device path (e.g., /dev/vgname/lvname)
6650+
* @return The volume group name, or null if cannot be determined
6651+
*/
6652+
static String extractVolumeGroupFromPath(String devicePath) {
6653+
if (devicePath == null || !devicePath.startsWith("/dev/")) {
6654+
return null;
6655+
}
6656+
6657+
// Format: /dev/<vgname>/<lvname>
6658+
String[] parts = devicePath.split("/");
6659+
if (parts.length >= 3) {
6660+
return parts[2]; // ["", "dev", "vgname", ...]
6661+
}
6662+
6663+
return null;
6664+
}
6665+
6666+
/**
6667+
* Checks if a volume group is clustered (CLVM) by examining its attributes.
6668+
* Uses 'vgs' command to check for the clustered/shared flag in VG attributes.
6669+
*
6670+
* VG Attr format (6 characters): wz--nc or wz--ns
6671+
* Position 6: Clustered flag - 'c' = CLVM (clustered), 's' = shared (lvmlockd), '-' = not clustered
6672+
*
6673+
* @param vgName The volume group name
6674+
* @return true if the VG is clustered or shared, false otherwise
6675+
*/
6676+
static boolean checkIfVolumeGroupIsClustered(String vgName) {
6677+
if (vgName == null) {
6678+
return false;
6679+
}
6680+
6681+
try {
6682+
// Use vgs with --noheadings and -o attr to get VG attributes
6683+
OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
6684+
Script vgsCmd = new Script("vgs", 5000, LOGGER);
6685+
vgsCmd.add("--noheadings");
6686+
vgsCmd.add("--unbuffered");
6687+
vgsCmd.add("-o");
6688+
vgsCmd.add("vg_attr");
6689+
vgsCmd.add(vgName);
6690+
6691+
String result = vgsCmd.execute(parser);
6692+
6693+
if (result == null && parser.getLines() != null) {
6694+
String output = parser.getLines();
6695+
if (output != null && !output.isEmpty()) {
6696+
// Parse VG attributes (format: wz--nc or wz--ns or wz--n-)
6697+
// Position 6 (0-indexed 5) indicates clustering/sharing:
6698+
// 'c' = clustered (CLVM) or 's' = shared (lvmlockd) or '-' = not clustered/shared
6699+
String vgAttr = output.trim();
6700+
if (vgAttr.length() >= 6) {
6701+
char clusterFlag = vgAttr.charAt(5); // Position 6 (0-indexed 5)
6702+
boolean isClustered = (clusterFlag == 'c' || clusterFlag == 's');
6703+
LOGGER.debug("VG {} has attributes '{}', cluster/shared flag '{}' = {}",
6704+
vgName, vgAttr, clusterFlag, isClustered);
6705+
return isClustered;
6706+
} else {
6707+
LOGGER.warn("VG {} attributes '{}' have unexpected format (expected 6+ chars)", vgName, vgAttr);
6708+
}
6709+
}
6710+
} else {
6711+
LOGGER.warn("Failed to get VG attributes for {}: {}", vgName, result);
6712+
}
6713+
6714+
} catch (Exception e) {
6715+
LOGGER.debug("Error checking if VG {} is clustered: {}", vgName, e.getMessage());
6716+
}
6717+
6718+
return false;
6719+
}
65266720
}

0 commit comments

Comments
 (0)