Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
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,18 @@

public interface VolumeInfo extends DownloadableDataInfo, Volume {

/**
* Constant for the volume detail key that stores the destination host ID for CLVM volume creation routing.
* This helps ensure volumes are created on the correct host with exclusive locks.
*/
String DESTINATION_HOST_ID = "destinationHostId";

/**
* 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 +115,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 @@ -3238,6 +3239,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
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,15 @@ public VolumeInfo createVolume(VolumeInfo volumeInfo, VirtualMachine vm, Virtual
logger.debug("Trying to create volume [{}] on storage pool [{}].",
volumeToString, poolToString);
DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary);

// For CLVM pools, set the destination host hint so volume is created on the correct host
// This avoids the need for shared mode activation and improves performance
if (pool.getPoolType() == Storage.StoragePoolType.CLVM && hostId != null) {
logger.info("CLVM pool detected. Setting destination host {} for volume {} to route creation to correct host",
hostId, volumeInfo.getUuid());
volumeInfo.setDestinationHostId(hostId);
}

for (int i = 0; i < 2; i++) {
// retry one more time in case of template reload is required for Vmware case
AsyncCallFuture<VolumeApiResult> future = null;
Expand Down Expand Up @@ -1851,6 +1860,20 @@ private Pair<VolumeVO, DataStore> recreateVolume(VolumeVO vol, VirtualMachinePro

future = volService.createManagedStorageVolumeFromTemplateAsync(volume, destPool.getId(), templ, hostId);
} else {
// For CLVM pools, set the destination host hint so volume is created on the correct host
// This avoids the need for shared mode activation and improves performance
StoragePoolVO poolVO = _storagePoolDao.findById(destPool.getId());
if (poolVO != null && poolVO.getPoolType() == Storage.StoragePoolType.CLVM) {
Long hostId = vm.getVirtualMachine().getHostId();
if (hostId != null) {
// Store in both memory and database so it persists across VolumeInfo recreations
volume.setDestinationHostId(hostId);
_volDetailDao.addDetail(volume.getId(), VolumeInfo.DESTINATION_HOST_ID, String.valueOf(hostId), false);
logger.info("CLVM pool detected during volume creation from template. Setting destination host {} for volume {} (persisted to DB) to route creation to correct host",
hostId, volume.getUuid());
}
}

future = volService.createVolumeFromTemplateAsync(volume, destPool.getId(), templ);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public boolean configure(String name, Map<String, Object> params) throws Configu

idStateNeqSearch = createSearchBuilder();
idStateNeqSearch.and(SNAPSHOT_ID, idStateNeqSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ);
idStateNeqSearch.and(STATE, idStateNeqSearch.entity().getState(), SearchCriteria.Op.NEQ);
idStateNeqSearch.and(STATE, idStateNeqSearch.entity().getState(), SearchCriteria.Op.NIN);
idStateNeqSearch.done();

snapshotVOSearch = snapshotDao.createSearchBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.Snapshot;
import com.cloud.storage.Storage;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
Expand Down Expand Up @@ -468,6 +469,13 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {

@Override
public StrategyPriority canHandle(VMSnapshot vmSnapshot) {
UserVmVO vm = userVmDao.findById(vmSnapshot.getVmId());
String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm);

if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) {
return StrategyPriority.CANT_HANDLE;
}

return StrategyPriority.DEFAULT;
}

Expand All @@ -493,10 +501,31 @@ public boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot, boolean unmanage) {
return vmSnapshotDao.remove(vmSnapshot.getId());
}

protected boolean isRunningVMVolumeOnCLVMStorage(UserVmVO vm, String cantHandleLog) {
Long vmId = vm.getId();
if (State.Running.equals(vm.getState())) {
List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
for (VolumeVO volume : volumes) {
StoragePool pool = primaryDataStoreDao.findById(volume.getPoolId());
if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) {
logger.warn("Rejecting VM snapshot request: {} - VM is running on CLVM storage (pool: {}, poolType: CLVM)",
cantHandleLog, pool.getName());
return true;
}
}
}
return false;
}

@Override
public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) {
UserVmVO vm = userVmDao.findById(vmId);
String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm);

if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Pearl1594
what's the image format on CLVM ? RAW or QCOW2 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return StrategyPriority.CANT_HANDLE;
}

if (State.Running.equals(vm.getState()) && !snapshotMemory) {
logger.debug("{} as it is running and its memory will not be affected.", cantHandleLog, vm);
return StrategyPriority.CANT_HANDLE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,13 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) {
}
}

Long vmId = vmSnapshot.getVmId();
UserVmVO vm = userVmDao.findById(vmId);
String cantHandleLog = String.format("Storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm);
if (vm != null && isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) {
return StrategyPriority.CANT_HANDLE;
}

if ( SnapshotManager.VmStorageSnapshotKvm.value() && userVm.getHypervisorType() == Hypervisor.HypervisorType.KVM
&& vmSnapshot.getType() == VMSnapshot.Type.Disk) {
return StrategyPriority.HYPERVISOR;
Expand Down
Loading
Loading