Skip to content
Draft
42 changes: 42 additions & 0 deletions core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// 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;

/**
* Answer for PostMigrationCommand.
* Indicates success or failure of post-migration operations on the destination host.
*/
public class PostMigrationAnswer extends Answer {

protected PostMigrationAnswer() {
}

public PostMigrationAnswer(PostMigrationCommand cmd, String detail) {
super(cmd, false, detail);
}

public PostMigrationAnswer(PostMigrationCommand cmd, Exception ex) {
super(cmd, ex);
}

public PostMigrationAnswer(PostMigrationCommand cmd) {
super(cmd, true, null);
}
}
54 changes: 54 additions & 0 deletions core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// 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;

/**
* PostMigrationCommand is sent to the destination host after a successful VM migration.
* It performs post-migration tasks such as:
* - Claiming exclusive locks on CLVM volumes (converting from shared to exclusive mode)
* - Other post-migration cleanup operations
*/
public class PostMigrationCommand extends Command {
private VirtualMachineTO vm;
private String vmName;

protected PostMigrationCommand() {
}

public PostMigrationCommand(VirtualMachineTO vm, String vmName) {
this.vm = vm;
this.vmName = vmName;
}

public VirtualMachineTO getVirtualMachine() {
return vm;
}

public String getVmName() {
return vmName;
}

@Override
public boolean executeInSequence() {
return true;
}
}
56 changes: 56 additions & 0 deletions core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// 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;

/**
* PreMigrationCommand is sent to the source host before VM migration starts.
* It performs pre-migration tasks such as:
* - Converting CLVM volume exclusive locks to shared mode so destination host can access them
* - Other pre-migration preparation operations on the source host
*
* This command runs on the SOURCE host before PrepareForMigrationCommand runs on the DESTINATION host.
*/
public class PreMigrationCommand extends Command {
private VirtualMachineTO vm;
private String vmName;

protected PreMigrationCommand() {
}

public PreMigrationCommand(VirtualMachineTO vm, String vmName) {
this.vm = vm;
this.vmName = vmName;
}

public VirtualMachineTO getVirtualMachine() {
return vm;
}

public String getVmName() {
return vmName;
}

@Override
public boolean executeInSequence() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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.command;

import com.cloud.agent.api.Command;

/**
* Command to transfer CLVM (Clustered LVM) exclusive lock between hosts.
* This enables lightweight volume migration for CLVM storage pools where volumes
* reside in the same Volume Group (VG) but need to be accessed from different hosts.
*
* <p>Instead of copying volume data (traditional migration), this command simply
* deactivates the LV on the source host and activates it exclusively on the destination host.
*
* <p>This is significantly faster (10-100x) than traditional migration and uses no network bandwidth.
*/
public class ClvmLockTransferCommand extends Command {

/**
* Operation to perform on the CLVM volume.
* Maps to lvchange flags for LVM operations.
*/
public enum Operation {
/** Deactivate the volume on this host (-an) */
DEACTIVATE("-an", "deactivate"),

/** Activate the volume exclusively on this host (-aey) */
ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"),

/** Activate the volume in shared mode on this host (-asy) */
ACTIVATE_SHARED("-asy", "activate in shared mode");

private final String lvchangeFlag;
private final String description;

Operation(String lvchangeFlag, String description) {
this.lvchangeFlag = lvchangeFlag;
this.description = description;
}

public String getLvchangeFlag() {
return lvchangeFlag;
}

public String getDescription() {
return description;
}
}

private String lvPath;
private Operation operation;
private String volumeUuid;

public ClvmLockTransferCommand() {
// For serialization
}

public ClvmLockTransferCommand(Operation operation, String lvPath, String volumeUuid) {
this.operation = operation;
this.lvPath = lvPath;
this.volumeUuid = volumeUuid;
// Execute in sequence to ensure lock safety
setWait(30);
}

public String getLvPath() {
return lvPath;
}

public Operation getOperation() {
return operation;
}

public String getVolumeUuid() {
return volumeUuid;
}

@Override
public boolean executeInSequence() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@

public interface VolumeInfo extends DownloadableDataInfo, Volume {

/**
* Constant for the volume detail key that stores the host ID currently holding the CLVM exclusive lock.
* This is used during lightweight lock migration to determine the source host for lock transfer.
*/
String CLVM_LOCK_HOST_ID = "clvmLockHostId";

boolean isAttachedVM();

void addPayload(Object data);
Expand Down Expand Up @@ -103,4 +109,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume {
List<String> getCheckpointPaths();

Set<String> getCheckpointImageStoreUrls();

/**
* Gets the destination host ID hint for CLVM volume creation.
* This is used to route volume creation commands to the specific host where the VM will be deployed.
* Only applicable for CLVM storage pools to avoid shared mode activation.
*
* @return The host ID where the volume should be created, or null if not set
*/
Long getDestinationHostId();

/**
* Sets the destination host ID hint for CLVM volume creation.
* This should be set before volume creation when the destination host is known.
*
* @param hostId The host ID where the volume should be created
*/
void setDestinationHostId(Long hostId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import javax.persistence.EntityExistsException;


import com.cloud.agent.api.PostMigrationCommand;
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
Expand Down Expand Up @@ -135,6 +136,7 @@
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
import com.cloud.agent.api.PrepareForMigrationAnswer;
import com.cloud.agent.api.PrepareForMigrationCommand;
import com.cloud.agent.api.PreMigrationCommand;
import com.cloud.agent.api.RebootAnswer;
import com.cloud.agent.api.RebootCommand;
import com.cloud.agent.api.RecreateCheckpointsCommand;
Expand Down Expand Up @@ -3107,6 +3109,24 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy
updateOverCommitRatioForVmProfile(profile, dest.getHost().getClusterId());

final VirtualMachineTO to = toVmTO(profile);

logger.info("Sending PreMigrationCommand to source host {} for VM {}", srcHostId, vm.getInstanceName());
final PreMigrationCommand preMigCmd = new PreMigrationCommand(to, vm.getInstanceName());
Answer preMigAnswer = null;
try {
preMigAnswer = _agentMgr.send(srcHostId, preMigCmd);
if (preMigAnswer == null || !preMigAnswer.getResult()) {
final String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned";
final String msg = "Failed to prepare source host for migration: " + details;
logger.error("Failed to prepare source host {} for migration of VM {}: {}", srcHostId, vm.getInstanceName(), details);
throw new CloudRuntimeException(msg);
}
logger.info("Successfully prepared source host {} for migration of VM {}", srcHostId, vm.getInstanceName());
} catch (final AgentUnavailableException | OperationTimedoutException e) {
logger.error("Failed to send PreMigrationCommand to source host {}: {}", srcHostId, e.getMessage(), e);
throw new CloudRuntimeException("Failed to prepare source host for migration: " + e.getMessage(), e);
}

final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to);
setVmNetworkDetails(vm, to);

Expand Down Expand Up @@ -3238,6 +3258,22 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy
logger.warn("Error while checking the vm {} on host {}", vm, dest.getHost(), e);
}
migrated = true;
try {
logger.info("Executing post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostId);
final PostMigrationCommand postMigrationCommand = new PostMigrationCommand(to, vm.getInstanceName());
final Answer postMigrationAnswer = _agentMgr.send(dstHostId, postMigrationCommand);

if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) {
final String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned";
logger.warn("Post-migration tasks failed for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.",
vm.getInstanceName(), dstHostId, details);
} else {
logger.info("Successfully completed post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostId);
}
} catch (Exception e) {
logger.warn("Exception during post-migration tasks for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.",
vm.getInstanceName(), dstHostId, e.getMessage(), e);
}
} finally {
if (!migrated) {
logger.info("Migration was unsuccessful. Cleaning up: {}", vm);
Expand Down Expand Up @@ -4897,6 +4933,27 @@ private void orchestrateMigrateForScale(final String vmUuid, final long srcHostI
volumeMgr.prepareForMigration(profile, dest);

final VirtualMachineTO to = toVmTO(profile);

// Step 1: Send PreMigrationCommand to source host to convert CLVM volumes to shared mode
// This must happen BEFORE PrepareForMigrationCommand on destination to avoid lock conflicts
logger.info("Sending PreMigrationCommand to source host {} for VM {}", srcHostId, vm.getInstanceName());
final PreMigrationCommand preMigCmd = new PreMigrationCommand(to, vm.getInstanceName());
Answer preMigAnswer = null;
try {
preMigAnswer = _agentMgr.send(srcHostId, preMigCmd);
if (preMigAnswer == null || !preMigAnswer.getResult()) {
final String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned";
final String msg = "Failed to prepare source host for migration: " + details;
logger.error("Failed to prepare source host {} for migration of VM {}: {}", srcHostId, vm.getInstanceName(), details);
throw new CloudRuntimeException(msg);
}
logger.info("Successfully prepared source host {} for migration of VM {}", srcHostId, vm.getInstanceName());
} catch (final AgentUnavailableException | OperationTimedoutException e) {
logger.error("Failed to send PreMigrationCommand to source host {}: {}", srcHostId, e.getMessage(), e);
throw new CloudRuntimeException("Failed to prepare source host for migration: " + e.getMessage(), e);
}

// Step 2: Send PrepareForMigrationCommand to destination host
final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to);

ItWorkVO work = new ItWorkVO(UUID.randomUUID().toString(), _nodeId, State.Migrating, vm.getType(), vm.getId());
Expand Down
Loading
Loading