add support for migrating lvm lock

This commit is contained in:
Pearl Dsilva 2026-02-18 15:05:46 -05:00
parent c9dd7ed43f
commit 4984ee5ff4
8 changed files with 802 additions and 10 deletions

View File

@ -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;
}
}

View File

@ -31,6 +31,18 @@ import java.util.Set;
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);
@ -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);
}

View File

@ -745,6 +745,15 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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;
@ -1851,6 +1860,20 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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);
}
}

View File

@ -32,8 +32,10 @@ import javax.inject.Inject;
import com.cloud.dc.DedicatedResourceVO;
import com.cloud.dc.dao.DedicatedResourceDao;
import com.cloud.storage.Storage;
import com.cloud.user.Account;
import com.cloud.utils.Pair;
import com.cloud.utils.db.QueryBuilder;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@ -46,6 +48,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.LocalHostEndpoint;
import org.apache.cloudstack.storage.RemoteHostEndPoint;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.springframework.stereotype.Component;
@ -59,8 +62,8 @@ import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Storage.TemplateType;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.QueryBuilder;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
@ -75,6 +78,8 @@ public class DefaultEndPointSelector implements EndPointSelector {
private HostDao hostDao;
@Inject
private DedicatedResourceDao dedicatedResourceDao;
@Inject
private PrimaryDataStoreDao _storagePoolDao;
private static final String VOL_ENCRYPT_COLUMN_NAME = "volume_encryption_support";
private final String findOneHostOnPrimaryStorage = "select t.id from "
@ -264,6 +269,16 @@ public class DefaultEndPointSelector implements EndPointSelector {
@Override
public EndPoint select(DataObject srcData, DataObject destData, boolean volumeEncryptionSupportRequired) {
// FOR CLVM: Check if destination is a volume with destinationHostId hint
// This ensures template-to-volume copy is routed to the correct host for optimal lock placement
if (destData instanceof VolumeInfo) {
EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) destData, "template-to-volume copy");
if (clvmEndpoint != null) {
return clvmEndpoint;
}
}
// Default behavior for non-CLVM or when no destination host is set
DataStore srcStore = srcData.getDataStore();
DataStore destStore = destData.getDataStore();
if (moveBetweenPrimaryImage(srcStore, destStore)) {
@ -388,9 +403,59 @@ public class DefaultEndPointSelector implements EndPointSelector {
return sc.list();
}
/**
* Selects endpoint for CLVM volumes with destination host hint.
* This ensures volumes are created on the correct host with exclusive locks.
*
* @param volume The volume to check for CLVM routing
* @param operation Description of the operation (for logging)
* @return EndPoint for the destination host if CLVM routing applies, null otherwise
*/
private EndPoint selectClvmEndpointIfApplicable(VolumeInfo volume, String operation) {
DataStore store = volume.getDataStore();
if (store.getRole() != DataStoreRole.Primary) {
return null;
}
// Check if this is a CLVM pool
StoragePoolVO pool = _storagePoolDao.findById(store.getId());
if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM) {
return null;
}
// Check if destination host hint is set
Long destHostId = volume.getDestinationHostId();
if (destHostId == null) {
return null;
}
logger.info("CLVM {}: routing volume {} to destination host {} for optimal exclusive lock placement",
operation, volume.getUuid(), destHostId);
EndPoint ep = getEndPointFromHostId(destHostId);
if (ep != null) {
return ep;
}
logger.warn("Could not get endpoint for destination host {}, falling back to default selection", destHostId);
return null;
}
@Override
public EndPoint select(DataObject object, boolean encryptionSupportRequired) {
DataStore store = object.getDataStore();
// For CLVM volumes with destination host hint, route to that specific host
// This ensures volumes are created on the correct host with exclusive locks
if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) {
EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) object, "volume creation");
if (clvmEndpoint != null) {
return clvmEndpoint;
}
}
// Default behavior for non-CLVM or when no destination host is set
if (store.getRole() == DataStoreRole.Primary) {
return findEndPointInScope(store.getScope(), findOneHostOnPrimaryStorage, store.getId(), encryptionSupportRequired);
}

View File

@ -126,6 +126,7 @@ public class VolumeObject implements VolumeInfo {
private boolean directDownload;
private String vSphereStoragePolicyId;
private boolean followRedirects;
private Long destinationHostId; // For CLVM: hints where volume should be created
private List<String> checkpointPaths;
private Set<String> checkpointImageStoreUrls;
@ -361,6 +362,27 @@ public class VolumeObject implements VolumeInfo {
this.directDownload = directDownload;
}
@Override
public Long getDestinationHostId() {
// If not in memory, try to load from database (volume_details table)
if (destinationHostId == null && volumeVO != null) {
VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeVO.getId(), DESTINATION_HOST_ID);
if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) {
try {
destinationHostId = Long.parseLong(detail.getValue());
} catch (NumberFormatException e) {
logger.warn("Invalid destinationHostId value in volume_details for volume {}: {}", volumeVO.getUuid(), detail.getValue());
}
}
}
return destinationHostId;
}
@Override
public void setDestinationHostId(Long hostId) {
this.destinationHostId = hostId;
}
public void update() {
volumeDao.update(volumeVO.getId(), volumeVO);
volumeVO = volumeDao.findById(volumeVO.getId());

View File

@ -0,0 +1,90 @@
// 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.hypervisor.kvm.resource.wrapper;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import com.cloud.agent.api.Answer;
import org.apache.cloudstack.storage.command.ClvmLockTransferCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = ClvmLockTransferCommand.class)
public class LibvirtClvmLockTransferCommandWrapper
extends CommandWrapper<ClvmLockTransferCommand, Answer, LibvirtComputingResource> {
protected Logger logger = LogManager.getLogger(getClass());
@Override
public Answer execute(ClvmLockTransferCommand cmd, LibvirtComputingResource serverResource) {
String lvPath = cmd.getLvPath();
ClvmLockTransferCommand.Operation operation = cmd.getOperation();
String volumeUuid = cmd.getVolumeUuid();
logger.info(String.format("Executing CLVM lock transfer: operation=%s, lv=%s, volume=%s",
operation, lvPath, volumeUuid));
try {
String lvchangeOpt;
String operationDesc;
switch (operation) {
case DEACTIVATE:
lvchangeOpt = "-an";
operationDesc = "deactivated";
break;
case ACTIVATE_EXCLUSIVE:
lvchangeOpt = "-aey";
operationDesc = "activated exclusively";
break;
case ACTIVATE_SHARED:
lvchangeOpt = "-asy";
operationDesc = "activated in shared mode";
break;
default:
return new Answer(cmd, false, "Unknown operation: " + operation);
}
Script script = new Script("/usr/sbin/lvchange", 30000, logger);
script.add(lvchangeOpt);
script.add(lvPath);
String result = script.execute();
if (result != null) {
logger.error("CLVM lock transfer failed for volume {}: {}}",
volumeUuid, result);
return new Answer(cmd, false,
String.format("lvchange %s %s failed: %s", lvchangeOpt, lvPath, result));
}
logger.info("Successfully executed CLVM lock transfer: {} {}} for volume {}}",
lvchangeOpt, lvPath, volumeUuid);
return new Answer(cmd, true,
String.format("Successfully %s CLVM volume %s", operationDesc, volumeUuid));
} catch (Exception e) {
logger.error("Exception during CLVM lock transfer for volume {}: {}}",
volumeUuid, e.getMessage(), e);
return new Answer(cmd, false, "Exception: " + e.getMessage());
}
}
}

View File

@ -1206,6 +1206,12 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
volName = vol.getName();
volAllocation = vol.getInfo().allocation;
volCapacity = vol.getInfo().capacity;
// For CLVM volumes, activate in shared mode so all cluster hosts can access it
if (pool.getType() == StoragePoolType.CLVM) {
logger.info("Activating CLVM volume {} in shared mode for cluster-wide access", volPath);
activateClvmVolumeInSharedMode(volPath);
}
} catch (LibvirtException e) {
throw new CloudRuntimeException(e.toString());
}
@ -1217,6 +1223,30 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
return disk;
}
/**
* Activates a CLVM volume in shared mode so all hosts in the cluster can access it.
* This is necessary after volume creation since libvirt creates LVs with exclusive activation by default.
*
* @param volumePath The full path to the LV (e.g., /dev/vgname/volume-uuid)
*/
private void activateClvmVolumeInSharedMode(String volumePath) {
try {
Script cmd = new Script("lvchange", 5000, logger);
cmd.add("-asy"); // Activate in shared mode
cmd.add(volumePath);
String result = cmd.execute();
if (result != null) {
logger.error("Failed to activate CLVM volume {} in shared mode. Result: {}", volumePath, result);
throw new CloudRuntimeException("Failed to activate CLVM volume in shared mode: " + result);
}
logger.info("Successfully activated CLVM volume {} in shared mode", volumePath);
} catch (Exception e) {
logger.error("Exception while activating CLVM volume {} in shared mode: {}", volumePath, e.getMessage(), e);
throw new CloudRuntimeException("Failed to activate CLVM volume in shared mode: " + e.getMessage(), e);
}
}
private KVMPhysicalDisk createPhysicalDiskByQemuImg(String name, KVMStoragePool pool, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size,
byte[] passphrase) {

View File

@ -130,6 +130,7 @@ import org.joda.time.DateTimeZone;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import org.apache.cloudstack.storage.command.ClvmLockTransferCommand;
import com.cloud.agent.api.ModifyTargetsCommand;
import com.cloud.agent.api.to.DataTO;
import com.cloud.agent.api.to.DiskTO;
@ -152,6 +153,7 @@ import com.cloud.event.UsageEventUtils;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.StorageUnavailableException;
@ -2602,21 +2604,42 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
logger.trace(String.format("is it needed to move the volume: %b?", moveVolumeNeeded));
}
if (moveVolumeNeeded) {
// Check if CLVM lock transfer is needed (even if moveVolumeNeeded is false)
// This handles the case where the volume is already on the correct storage pool
// but the VM is running on a different host, requiring only a lock transfer
boolean isClvmLockTransferNeeded = !moveVolumeNeeded &&
isClvmLockTransferRequired(newVolumeOnPrimaryStorage, existingVolumeOfVm, vm);
if (isClvmLockTransferNeeded) {
// CLVM lock transfer - no data copy, no pool change needed
newVolumeOnPrimaryStorage = executeClvmLightweightMigration(
newVolumeOnPrimaryStorage, vm, existingVolumeOfVm,
"CLVM lock transfer", "same pool to different host");
} else if (moveVolumeNeeded) {
PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)newVolumeOnPrimaryStorage.getDataStore();
if (primaryStore.isLocal()) {
throw new CloudRuntimeException(
"Failed to attach local data volume " + volumeToAttach.getName() + " to VM " + vm.getDisplayName() + " as migration of local data volume is not allowed");
}
StoragePoolVO vmRootVolumePool = _storagePoolDao.findById(existingVolumeOfVm.getPoolId());
try {
HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId());
newVolumeOnPrimaryStorage = _volumeMgr.moveVolume(newVolumeOnPrimaryStorage, vmRootVolumePool.getDataCenterId(), vmRootVolumePool.getPodId(), vmRootVolumePool.getClusterId(),
volumeToAttachHyperType);
} catch (ConcurrentOperationException | StorageUnavailableException e) {
logger.debug("move volume failed", e);
throw new CloudRuntimeException("move volume failed", e);
boolean isClvmLightweightMigration = isClvmLightweightMigrationNeeded(
newVolumeOnPrimaryStorage, existingVolumeOfVm, vm);
if (isClvmLightweightMigration) {
newVolumeOnPrimaryStorage = executeClvmLightweightMigration(
newVolumeOnPrimaryStorage, vm, existingVolumeOfVm,
"CLVM lightweight migration", "different pools, same VG");
} else {
StoragePoolVO vmRootVolumePool = _storagePoolDao.findById(existingVolumeOfVm.getPoolId());
try {
HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId());
newVolumeOnPrimaryStorage = _volumeMgr.moveVolume(newVolumeOnPrimaryStorage, vmRootVolumePool.getDataCenterId(), vmRootVolumePool.getPodId(), vmRootVolumePool.getClusterId(),
volumeToAttachHyperType);
} catch (ConcurrentOperationException | StorageUnavailableException e) {
logger.debug("move volume failed", e);
throw new CloudRuntimeException("move volume failed", e);
}
}
}
VolumeVO newVol = _volsDao.findById(newVolumeOnPrimaryStorage.getId());
@ -2631,6 +2654,419 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
return newVol;
}
/**
* Helper method to get storage pools for volume and VM.
*
* @param volumeToAttach The volume being attached
* @param vmExistingVolume The VM's existing volume
* @return Pair of StoragePoolVO objects (volumePool, vmPool), or null if either pool is missing
*/
private Pair<StoragePoolVO, StoragePoolVO> getStoragePoolsForVolumeAttachment(VolumeInfo volumeToAttach, VolumeVO vmExistingVolume) {
if (volumeToAttach == null || vmExistingVolume == null) {
return null;
}
StoragePoolVO volumePool = _storagePoolDao.findById(volumeToAttach.getPoolId());
StoragePoolVO vmPool = _storagePoolDao.findById(vmExistingVolume.getPoolId());
if (volumePool == null || vmPool == null) {
return null;
}
return new Pair<>(volumePool, vmPool);
}
/**
* Checks if both storage pools are CLVM type.
*
* @param volumePool Storage pool for the volume
* @param vmPool Storage pool for the VM
* @return true if both pools are CLVM type
*/
private boolean areBothPoolsClvmType(StoragePoolVO volumePool, StoragePoolVO vmPool) {
return volumePool.getPoolType() == StoragePoolType.CLVM &&
vmPool.getPoolType() == StoragePoolType.CLVM;
}
/**
* Checks if a storage pool is CLVM type.
*
* @param pool Storage pool to check
* @return true if pool is CLVM type
*/
private boolean isClvmPool(StoragePoolVO pool) {
return pool != null && pool.getPoolType() == StoragePoolType.CLVM;
}
/**
* Extracts the Volume Group (VG) name from a CLVM storage pool path.
* For CLVM, the path is typically: /vgname
*
* @param poolPath The storage pool path
* @return VG name, or null if path is null
*/
private String extractVgNameFromPath(String poolPath) {
if (poolPath == null) {
return null;
}
return poolPath.startsWith("/") ? poolPath.substring(1) : poolPath;
}
/**
* Checks if two CLVM storage pools are in the same Volume Group.
*
* @param volumePool Storage pool for the volume
* @param vmPool Storage pool for the VM
* @return true if both pools are in the same VG
*/
private boolean arePoolsInSameVolumeGroup(StoragePoolVO volumePool, StoragePoolVO vmPool) {
String volumeVgName = extractVgNameFromPath(volumePool.getPath());
String vmVgName = extractVgNameFromPath(vmPool.getPath());
return volumeVgName != null && volumeVgName.equals(vmVgName);
}
/**
* Determines if a CLVM volume needs lightweight lock migration instead of full data copy.
*
* Lightweight migration is needed when:
* 1. Volume is on CLVM storage
* 2. Source and destination are in the same Volume Group
* 3. Only the host/lock needs to change (not the storage pool)
*
* @param volumeToAttach The volume being attached
* @param vmExistingVolume The VM's existing volume (typically root volume)
* @param vm The VM to attach the volume to
* @return true if lightweight CLVM lock migration should be used
*/
private boolean isClvmLightweightMigrationNeeded(VolumeInfo volumeToAttach, VolumeVO vmExistingVolume, UserVmVO vm) {
Pair<StoragePoolVO, StoragePoolVO> pools = getStoragePoolsForVolumeAttachment(volumeToAttach, vmExistingVolume);
if (pools == null) {
return false;
}
StoragePoolVO volumePool = pools.first();
StoragePoolVO vmPool = pools.second();
if (!areBothPoolsClvmType(volumePool, vmPool)) {
return false;
}
if (arePoolsInSameVolumeGroup(volumePool, vmPool)) {
String vgName = extractVgNameFromPath(volumePool.getPath());
logger.info("CLVM lightweight migration detected: Volume {} is in same VG ({}) as VM {} volumes, " +
"only lock transfer needed (no data copy)",
volumeToAttach.getUuid(), vgName, vm.getUuid());
return true;
}
return false;
}
/**
* Determines if a CLVM volume requires lock transfer when already on the correct storage pool.
*
* Lock transfer is needed when:
* 1. Volume is already on the same CLVM storage pool as VM's volumes
* 2. But the volume lock is held by a different host than where the VM is running
* 3. Only the lock needs to change (no pool change, no data copy)
*
* @param volumeToAttach The volume being attached
* @param vmExistingVolume The VM's existing volume (typically root volume)
* @param vm The VM to attach the volume to
* @return true if CLVM lock transfer is needed (but not full migration)
*/
private boolean isClvmLockTransferRequired(VolumeInfo volumeToAttach, VolumeVO vmExistingVolume, UserVmVO vm) {
if (vm == null) {
return false;
}
Pair<StoragePoolVO, StoragePoolVO> pools = getStoragePoolsForVolumeAttachment(volumeToAttach, vmExistingVolume);
if (pools == null) {
return false;
}
StoragePoolVO volumePool = pools.first();
StoragePoolVO vmPool = pools.second();
if (!isClvmPool(volumePool)) {
return false;
}
if (volumePool.getId() != vmPool.getId()) {
return false;
}
Long volumeLockHostId = findClvmVolumeLockHost(volumeToAttach);
Long vmHostId = vm.getHostId();
if (vmHostId == null) {
vmHostId = vm.getLastHostId();
}
if (volumeLockHostId == null) {
VolumeVO volumeVO = _volsDao.findById(volumeToAttach.getId());
if (volumeVO != null && volumeVO.getState() == Volume.State.Ready && volumeVO.getInstanceId() == null) {
logger.debug("CLVM volume {} is detached on same pool as VM {}, lock transfer may be needed",
volumeToAttach.getUuid(), vm.getUuid());
return true;
}
}
if (volumeLockHostId != null && vmHostId != null && !volumeLockHostId.equals(vmHostId)) {
logger.info("CLVM lock transfer required: Volume {} lock is on host {} but VM {} is on host {}",
volumeToAttach.getUuid(), volumeLockHostId, vm.getUuid(), vmHostId);
return true;
}
return false;
}
/**
* Determines the destination host for CLVM lock migration.
*
* If VM is running, uses the VM's current host.
* If VM is stopped, picks an available UP host from the storage pool's cluster.
*
* @param vm The VM
* @param vmExistingVolume The VM's existing volume (to determine cluster)
* @return Host ID, or null if cannot be determined
*/
private Long determineClvmLockDestinationHost(UserVmVO vm, VolumeVO vmExistingVolume) {
Long destHostId = vm.getHostId();
if (destHostId != null) {
return destHostId;
}
if (vmExistingVolume != null && vmExistingVolume.getPoolId() != null) {
StoragePoolVO pool = _storagePoolDao.findById(vmExistingVolume.getPoolId());
if (pool != null && pool.getClusterId() != null) {
List<HostVO> hosts = _hostDao.findByClusterId(pool.getClusterId());
if (hosts != null && !hosts.isEmpty()) {
// Pick first available UP host
for (HostVO host : hosts) {
if (host.getStatus() == Status.Up) {
destHostId = host.getId();
logger.debug("VM {} is stopped, selected host {} from cluster {} for CLVM lock migration",
vm.getUuid(), destHostId, pool.getClusterId());
return destHostId;
}
}
}
}
}
return null;
}
/**
* Executes CLVM lightweight migration with consistent logging and error handling.
*
* This helper method wraps the actual migration logic to eliminate code duplication
* between different CLVM migration scenarios (lock transfer vs. lightweight migration).
*
* @param volume The volume to migrate locks for
* @param vm The VM to attach the volume to
* @param vmExistingVolume The VM's existing volume (to determine target host)
* @param operationType Description of the operation type for logging (e.g., "CLVM lock transfer")
* @param scenarioDescription Description of the scenario for logging (e.g., "same pool to different host")
* @return Updated VolumeInfo after lock migration
* @throws CloudRuntimeException if migration fails
*/
private VolumeInfo executeClvmLightweightMigration(VolumeInfo volume, UserVmVO vm, VolumeVO vmExistingVolume,
String operationType, String scenarioDescription) {
logger.info("Performing {} for volume {} to VM {} ({})",
operationType, volume.getUuid(), vm.getUuid(), scenarioDescription);
try {
return performClvmLightweightMigration(volume, vm, vmExistingVolume);
} catch (Exception e) {
logger.error("{} failed for volume {}: {}",
operationType, volume.getUuid(), e.getMessage(), e);
throw new CloudRuntimeException(operationType + " failed", e);
}
}
/**
* Performs lightweight CLVM lock migration for volume attachment.
*
* This transfers the LVM exclusive lock from the current host to the VM's host
* without copying data (since CLVM volumes are on cluster-wide shared storage).
*
* @param volume The volume to migrate locks for
* @param vm The VM to attach the volume to
* @param vmExistingVolume The VM's existing volume (to determine target host)
* @return Updated VolumeInfo after lock migration
* @throws Exception if lock migration fails
*/
private VolumeInfo performClvmLightweightMigration(VolumeInfo volume, UserVmVO vm, VolumeVO vmExistingVolume) throws Exception {
String volumeUuid = volume.getUuid();
Long vmId = vm.getId();
logger.info("Starting CLVM lightweight lock migration for volume {} (id: {}) to VM {} (id: {})",
volumeUuid, volume.getId(), vm.getUuid(), vmId);
Long destHostId = determineClvmLockDestinationHost(vm, vmExistingVolume);
if (destHostId == null) {
throw new CloudRuntimeException(
"Cannot determine destination host for CLVM lock migration - VM has no host and no available cluster hosts");
}
Long sourceHostId = findClvmVolumeLockHost(volume);
if (sourceHostId == null) {
logger.warn("Could not determine source host for CLVM volume {} lock, " +
"assuming volume is not exclusively locked", volumeUuid);
sourceHostId = destHostId;
}
if (sourceHostId.equals(destHostId)) {
logger.info("CLVM volume {} already has lock on destination host {}, no migration needed",
volumeUuid, destHostId);
return volume;
}
logger.info("Migrating CLVM volume {} lock from host {} to host {}",
volumeUuid, sourceHostId, destHostId);
boolean success = transferClvmVolumeLock(volume, sourceHostId, destHostId);
if (!success) {
throw new CloudRuntimeException(
String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s",
volumeUuid, sourceHostId, destHostId));
}
logger.info("Successfully migrated CLVM volume {} lock from host {} to host {}",
volumeUuid, sourceHostId, destHostId);
return volFactory.getVolume(volume.getId());
}
/**
* Finds which host currently has the exclusive lock on a CLVM volume.
*
* @param volume The CLVM volume
* @return Host ID that has the exclusive lock, or null if cannot be determined
*/
private Long findClvmVolumeLockHost(VolumeInfo volume) {
// Strategy 1: Check volume_details for a host hint we may have stored
VolumeDetailVO detail = _volsDetailsDao.findDetail(volume.getId(), VolumeInfo.CLVM_LOCK_HOST_ID);
if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) {
try {
return Long.parseLong(detail.getValue());
} catch (NumberFormatException e) {
logger.warn("Invalid clvmLockHostId in volume_details for volume {}: {}",
volume.getUuid(), detail.getValue());
}
}
// Strategy 2: If volume was attached to a VM, use that VM's last host
Long instanceId = volume.getInstanceId();
if (instanceId != null) {
VMInstanceVO vmInstance = _vmInstanceDao.findById(instanceId);
if (vmInstance != null && vmInstance.getHostId() != null) {
return vmInstance.getHostId();
}
}
// Strategy 3: Check any host in the pool's cluster
StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId());
if (pool != null && pool.getClusterId() != null) {
List<HostVO> hosts = _hostDao.findByClusterId(pool.getClusterId());
if (hosts != null && !hosts.isEmpty()) {
// Return first available UP host
for (HostVO host : hosts) {
if (host.getStatus() == Status.Up) {
return host.getId();
}
}
}
}
return null;
}
/**
* Transfers CLVM volume exclusive lock from source host to destination host.
*
* @param volume The volume to transfer lock for
* @param sourceHostId Host currently holding the lock
* @param destHostId Host to transfer lock to
* @return true if successful, false otherwise
*/
private boolean transferClvmVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId) {
String volumeUuid = volume.getUuid();
// Get storage pool info
StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId());
if (pool == null) {
logger.error("Cannot find storage pool for volume {}", volumeUuid);
return false;
}
String vgName = pool.getPath();
if (vgName.startsWith("/")) {
vgName = vgName.substring(1);
}
// Full LV path: /dev/vgname/volume-uuid
String lvPath = String.format("/dev/%s/%s", vgName, volumeUuid);
try {
// Step 1: Deactivate on source host (if different from dest)
if (!sourceHostId.equals(destHostId)) {
logger.debug("Deactivating CLVM volume {} on source host {}", volumeUuid, sourceHostId);
ClvmLockTransferCommand deactivateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
lvPath,
volumeUuid
);
Answer deactivateAnswer = _agentMgr.send(sourceHostId, deactivateCmd);
if (deactivateAnswer == null || !deactivateAnswer.getResult()) {
String error = deactivateAnswer != null ? deactivateAnswer.getDetails() : "null answer";
logger.warn("Failed to deactivate CLVM volume {} on source host {}: {}. " +
"Will attempt to activate on destination anyway.",
volumeUuid, sourceHostId, error);
}
}
// Step 2: Activate exclusively on destination host
logger.debug("Activating CLVM volume {} exclusively on destination host {}", volumeUuid, destHostId);
ClvmLockTransferCommand activateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
lvPath,
volumeUuid
);
Answer activateAnswer = _agentMgr.send(destHostId, activateCmd);
if (activateAnswer == null || !activateAnswer.getResult()) {
String error = activateAnswer != null ? activateAnswer.getDetails() : "null answer";
logger.error("Failed to activate CLVM volume {} exclusively on dest host {}: {}",
volumeUuid, destHostId, error);
return false;
}
// Step 3: Store the new lock host in volume_details for future reference
_volsDetailsDao.addDetail(volume.getId(), VolumeInfo.CLVM_LOCK_HOST_ID, String.valueOf(destHostId), false);
logger.info("Successfully transferred CLVM lock for volume {} from host {} to host {}",
volumeUuid, sourceHostId, destHostId);
return true;
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.error("Exception during CLVM lock transfer for volume {}: {}", volumeUuid, e.getMessage(), e);
return false;
}
}
public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS) {
Account caller = CallContext.current().getCallingAccount();