mirror of https://github.com/apache/cloudstack.git
add support for migrating lvm lock
This commit is contained in:
parent
c9dd7ed43f
commit
4984ee5ff4
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue