evist lock tracking - use lvs command to get lock host than DB

This commit is contained in:
Pearl Dsilva 2026-04-07 12:31:31 -04:00
parent a29f823f13
commit 4903339aa3
15 changed files with 900 additions and 85 deletions

View File

@ -0,0 +1,95 @@
// 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.Answer;
/**
* Answer for ClvmLockTransferCommand, containing lock state information.
* This answer includes the current lock holder information when querying lock state.
*/
public class ClvmLockTransferAnswer extends Answer {
private String currentLockHostname;
private boolean isActive;
private boolean isExclusive;
private String lvAttributes;
public ClvmLockTransferAnswer(ClvmLockTransferCommand cmd, boolean result, String details) {
super(cmd, result, details);
}
public ClvmLockTransferAnswer(ClvmLockTransferCommand cmd, boolean result, String details,
String currentLockHostname, boolean isActive, boolean isExclusive,
String lvAttributes) {
super(cmd, result, details);
this.currentLockHostname = currentLockHostname;
this.isActive = isActive;
this.isExclusive = isExclusive;
this.lvAttributes = lvAttributes;
}
/**
* Get the hostname of the host currently holding the lock (if any).
* This is parsed from the LVM "lv_host" field.
*
* @return hostname or null if no lock is held
*/
public String getCurrentLockHostname() {
return currentLockHostname;
}
public void setCurrentLockHostname(String currentLockHostname) {
this.currentLockHostname = currentLockHostname;
}
/**
* Whether the volume is currently active on any host.
*
* @return true if active, false otherwise
*/
public boolean isActive() {
return isActive;
}
public void setActive(boolean active) {
isActive = active;
}
/**
* Whether the lock is exclusive (as opposed to shared).
* Only meaningful if isActive() is true.
*
* @return true if exclusive lock, false if shared
*/
public boolean isExclusive() {
return isExclusive;
}
public void setExclusive(boolean exclusive) {
isExclusive = exclusive;
}
public String getLvAttributes() {
return lvAttributes;
}
public void setLvAttributes(String lvAttributes) {
this.lvAttributes = lvAttributes;
}
}

View File

@ -43,7 +43,10 @@ public class ClvmLockTransferCommand extends Command {
ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"),
/** Activate the volume in shared mode on this host (-asy) */
ACTIVATE_SHARED("-asy", "activate in shared mode");
ACTIVATE_SHARED("-asy", "activate in shared mode"),
/** Query the current lock state (lvs -o lv_attr,lv_host) */
QUERY_LOCK_STATE("query", "query lock state");
private final String lvchangeFlag;
private final String description;

View File

@ -861,9 +861,15 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId());
if (pool != null && ClvmLockManager.isClvmPoolType(pool.getPoolType())) {
Long lockHostId = clvmLockManager.getClvmLockHostId(volume.getId(), volume.getUuid());
Long lockHostId = clvmLockManager.getClvmLockHostId(
volume.getId(),
volume.getUuid(),
volume.getPath(),
pool,
true
);
if (lockHostId != null) {
logger.debug("Found CLVM lock host {} from existing volume {} of VM {}",
logger.debug("Found actual CLVM lock host {} from volume {} of VM {} via LVM query",
lockHostId, volume.getUuid(), vmId);
return lockHostId;
}
@ -888,7 +894,13 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
continue;
}
Long currentLockHost = clvmLockManager.getClvmLockHostId(volume.getId(), volume.getUuid());
Long currentLockHost = clvmLockManager.getClvmLockHostId(
volume.getId(),
volume.getUuid(),
volume.getPath(),
pool,
true
);
if (currentLockHost == null) {
clvmLockManager.setClvmLockHostId(volume.getId(), destHostId);

View File

@ -671,8 +671,10 @@ public class VolumeOrchestratorTest {
Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME);
ClvmLockManager clvmLockManager = Mockito.mock(ClvmLockManager.class);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString())).thenReturn(currentHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(102L), Mockito.anyString())).thenReturn(currentHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(),
Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(102L), Mockito.anyString(),
Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId);
Mockito.when(clvmLockManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(),
Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true);
@ -741,7 +743,8 @@ public class VolumeOrchestratorTest {
VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class);
ClvmLockManager clvmLockManager = Mockito.mock(ClvmLockManager.class);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class))).thenReturn(destHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class),
ArgumentMatchers.nullable(String.class), Mockito.any(), Mockito.eq(true))).thenReturn(destHostId);
Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool);
@ -773,7 +776,8 @@ public class VolumeOrchestratorTest {
method.invoke(volumeOrchestrator, new ArrayList<VolumeVO>(), destHostId, vmInstance);
Mockito.verify(clvmLockManager, Mockito.never()).getClvmLockHostId(Mockito.anyLong(), Mockito.anyString());
Mockito.verify(clvmLockManager, Mockito.never()).getClvmLockHostId(Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.any(), Mockito.anyBoolean());
}
@Test
@ -813,7 +817,8 @@ public class VolumeOrchestratorTest {
VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class);
ClvmLockManager clvmLockManager = Mockito.mock(ClvmLockManager.class);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class))).thenReturn(null);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class),
ArgumentMatchers.nullable(String.class), Mockito.any(), Mockito.eq(true))).thenReturn(null);
Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool);
@ -858,7 +863,8 @@ public class VolumeOrchestratorTest {
Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME);
ClvmLockManager clvmLockManager = Mockito.mock(ClvmLockManager.class);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString())).thenReturn(currentHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(),
Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId);
Mockito.when(clvmLockManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(),
Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true);
@ -898,7 +904,8 @@ public class VolumeOrchestratorTest {
Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME);
ClvmLockManager clvmLockManager = Mockito.mock(ClvmLockManager.class);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString())).thenReturn(currentHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(),
Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId);
Mockito.when(clvmLockManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(),
Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(false);

View File

@ -342,7 +342,13 @@ public class AncientDataMotionStrategy implements DataMotionStrategy {
StoragePool destPool = (StoragePool) volObj.getDataStore();
if (destPool != null && ClvmLockManager.isClvmPoolType(destPool.getPoolType())) {
Long hostId = ep.getId();
Long existingHostId = clvmLockManager.getClvmLockHostId(volumeInfo.getId(), volumeInfo.getUuid());
Long existingHostId = clvmLockManager.getClvmLockHostId(
volumeInfo.getId(),
volumeInfo.getUuid(),
volumeInfo.getPath(),
destPool,
true
);
if (existingHostId == null) {
clvmLockManager.setClvmLockHostId(volumeInfo.getId(), hostId);
logger.debug("Set lock host ID {} for CLVM volume {} being created from snapshot", hostId, volumeInfo.getId());

View File

@ -318,8 +318,10 @@ public class AncientDataMotionStrategyTest {
Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore);
Mockito.when(volumeInfo.getId()).thenReturn(volumeId);
Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid);
Mockito.when(volumeInfo.getPath()).thenReturn("test-volume-path");
Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(clvmLockManager.getClvmLockHostId(volumeId, volumeUuid)).thenReturn(null);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid),
Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null);
method.invoke(strategy, endPoint, volumeInfo);
@ -351,8 +353,10 @@ public class AncientDataMotionStrategyTest {
Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore);
Mockito.when(volumeInfo.getId()).thenReturn(volumeId);
Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid);
Mockito.when(volumeInfo.getPath()).thenReturn("test-clvm-ng-volume-path");
Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM_NG);
Mockito.when(clvmLockManager.getClvmLockHostId(volumeId, volumeUuid)).thenReturn(null);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid),
Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null);
try {
method.invoke(strategy, endPoint, volumeInfo);
@ -388,7 +392,8 @@ public class AncientDataMotionStrategyTest {
method.invoke(strategy, endPoint, volumeInfo);
Mockito.verify(clvmLockManager, never()).setClvmLockHostId(any(Long.class), any(Long.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class),
any(String.class), any(StoragePool.class), Mockito.anyBoolean());
}
@Test
@ -417,13 +422,16 @@ public class AncientDataMotionStrategyTest {
Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore);
Mockito.when(volumeInfo.getId()).thenReturn(volumeId);
Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid);
Mockito.when(volumeInfo.getPath()).thenReturn("existing-lock-volume-path");
Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(clvmLockManager.getClvmLockHostId(volumeId, volumeUuid)).thenReturn(existingHostId);
Mockito.when(clvmLockManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid),
Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(existingHostId);
method.invoke(strategy, endPoint, volumeInfo);
Mockito.verify(clvmLockManager, never()).setClvmLockHostId(any(Long.class), any(Long.class));
Mockito.verify(clvmLockManager).getClvmLockHostId(volumeId, volumeUuid);
Mockito.verify(clvmLockManager).getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid),
Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true));
}
@Test
@ -446,7 +454,8 @@ public class AncientDataMotionStrategyTest {
method.invoke(strategy, null, volumeInfo);
Mockito.verify(clvmLockManager, never()).setClvmLockHostId(any(Long.class), any(Long.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class),
any(String.class), any(StoragePool.class), Mockito.anyBoolean());
}
@Test
@ -471,7 +480,8 @@ public class AncientDataMotionStrategyTest {
method.invoke(strategy, endPoint, snapshotInfo);
Mockito.verify(clvmLockManager, never()).setClvmLockHostId(any(Long.class), any(Long.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class),
any(String.class), any(StoragePool.class), Mockito.anyBoolean());
}
@Test
@ -496,6 +506,7 @@ public class AncientDataMotionStrategyTest {
method.invoke(strategy, endPoint, volumeInfo);
Mockito.verify(clvmLockManager, never()).setClvmLockHostId(any(Long.class), any(Long.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class));
Mockito.verify(clvmLockManager, never()).getClvmLockHostId(any(Long.class), any(String.class),
any(String.class), any(StoragePool.class), Mockito.anyBoolean());
}
}

View File

@ -33,7 +33,6 @@ import javax.inject.Inject;
import com.cloud.dc.DedicatedResourceVO;
import com.cloud.dc.dao.DedicatedResourceDao;
import com.cloud.storage.ClvmLockManager;
import com.cloud.storage.VolumeDetailVO;
import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.user.Account;
import com.cloud.utils.Pair;
@ -84,6 +83,8 @@ public class DefaultEndPointSelector implements EndPointSelector {
private PrimaryDataStoreDao _storagePoolDao;
@Inject
private VolumeDetailsDao _volDetailsDao;
@Inject
private ClvmLockManager clvmLockManager;
private static final String VOL_ENCRYPT_COLUMN_NAME = "volume_encryption_support";
private final String findOneHostOnPrimaryStorage = "select t.id from "
@ -280,6 +281,28 @@ public class DefaultEndPointSelector implements EndPointSelector {
}
}
// Check if SOURCE is a CLVM volume with active lock (for operations copying FROM CLVM to secondary storage)
if (srcData instanceof VolumeInfo) {
VolumeInfo srcVolume = (VolumeInfo) srcData;
DataStore srcStore = srcVolume.getDataStore();
if (srcStore.getRole() == DataStoreRole.Primary) {
StoragePoolVO pool = _storagePoolDao.findById(srcStore.getId());
if (pool != null && ClvmLockManager.isClvmPoolType(pool.getPoolType())) {
Long lockHostId = getClvmLockHostId(srcVolume);
if (lockHostId != null) {
logger.info("Routing CLVM volume {} copy operation to source lock holder host {}",
srcVolume.getUuid(), lockHostId);
EndPoint ep = getEndPointFromHostId(lockHostId);
if (ep != null) {
return ep;
}
logger.warn("Could not get endpoint for CLVM lock host {}, falling back to default selection",
lockHostId);
}
}
}
}
// Default behavior for non-CLVM or when no destination host is set
DataStore srcStore = srcData.getDataStore();
DataStore destStore = destData.getDataStore();
@ -715,22 +738,27 @@ public class DefaultEndPointSelector implements EndPointSelector {
}
/**
* Retrieves the host ID that currently holds the exclusive lock on a CLVM volume.
* This is tracked in volume_details table for proper routing of delete operations.
/**
* Gets the CLVM lock host ID for a volume by querying actual LVM state.
*
* @param volume The CLVM volume
* @return Host ID holding the lock, or null if not tracked
* @return Host ID holding the lock, or null if not found
*/
private Long getClvmLockHostId(VolumeInfo volume) {
VolumeDetailVO detail = _volDetailsDao.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 CLVM lock host ID in volume_details for volume {}: {}",
volume.getUuid(), detail.getValue());
}
protected Long getClvmLockHostId(VolumeInfo volume) {
StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId());
Long lockHostId = clvmLockManager.getClvmLockHostId(
volume.getId(),
volume.getUuid(),
volume.getPath(),
pool,
true
);
if (lockHostId != null) {
logger.debug("Found actual lock host {} for volume {} via LVM query", lockHostId, volume.getUuid());
}
return null;
return lockHostId;
}
}

View File

@ -17,7 +17,10 @@
package org.apache.cloudstack.storage.endpoint;
import com.cloud.host.Host;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.ClvmLockManager;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.storage.VolumeDetailVO;
@ -30,18 +33,23 @@ import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.StorageAction;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.RemoteHostEndPoint;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mockStatic;
@RunWith(MockitoJUnitRunner.class)
public class DefaultEndPointSelectorTest {
@ -73,6 +81,14 @@ public class DefaultEndPointSelectorTest {
@Mock
private EndPoint endPointMock;
@Mock
ClvmLockManager clvmLockManager;
@Mock
HostDao hostDao;
static MockedStatic<RemoteHostEndPoint> remoteHostEndPointMock;
@InjectMocks
private DefaultEndPointSelector defaultEndPointSelectorSpy = Mockito.spy(new DefaultEndPointSelector());
@ -82,6 +98,16 @@ public class DefaultEndPointSelectorTest {
private static final Long STORE_ID = 100L;
private static final String VOLUME_UUID = "test-volume-uuid";
@BeforeClass
public static void init() {
remoteHostEndPointMock = mockStatic(RemoteHostEndPoint.class);
}
@AfterClass
public static void close() {
remoteHostEndPointMock.close();
}
@Before
public void setup() {
Mockito.doReturn(volumeInfoMock).when(snapshotInfoMock).getBaseVolume();
@ -331,8 +357,7 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(volumeDetailVOMock);
Mockito.when(volumeDetailVOMock.getValue()).thenReturn(String.valueOf(HOST_ID));
Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock);
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false);
@ -350,8 +375,7 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(volumeDetailVOMock);
Mockito.when(volumeDetailVOMock.getValue()).thenReturn(String.valueOf(HOST_ID));
Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock);
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false);
@ -369,7 +393,6 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(null);
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false);
@ -403,8 +426,7 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(volumeDetailVOMock);
Mockito.when(volumeDetailVOMock.getValue()).thenReturn(String.valueOf(HOST_ID));
Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock);
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock);
@ -421,8 +443,7 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(volumeDetailVOMock);
Mockito.when(volumeDetailVOMock.getValue()).thenReturn(String.valueOf(HOST_ID));
Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock);
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock);
@ -439,8 +460,10 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(null);
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock);
RemoteHostEndPoint ep = Mockito.mock(RemoteHostEndPoint.class);
Host lockHost = Mockito.mock(Host.class);
remoteHostEndPointMock.when(() -> RemoteHostEndPoint.getHypervisorHostEndPoint(lockHost)).thenReturn(ep);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock);
@ -456,8 +479,6 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(volumeDetailVOMock);
Mockito.when(volumeDetailVOMock.getValue()).thenReturn("invalid-host-id");
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock);
@ -474,8 +495,6 @@ public class DefaultEndPointSelectorTest {
Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock);
Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock);
Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM);
Mockito.when(_volDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(volumeDetailVOMock);
Mockito.when(volumeDetailVOMock.getValue()).thenReturn("");
Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock);
EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock);

View File

@ -2990,9 +2990,18 @@ public class VolumeServiceImpl implements VolumeService {
return null;
}
Long lockHostId = clvmLockManager.getClvmLockHostId(volume.getId(), volume.getUuid());
StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId());
Long lockHostId = clvmLockManager.getClvmLockHostId(
volume.getId(),
volume.getUuid(),
volume.getPath(),
pool,
true
);
if (lockHostId != null) {
logger.debug("Found explicit lock host {} for volume {}", lockHostId, volume.getUuid());
logger.debug("Found actual lock host {} for volume {}", lockHostId, volume.getUuid());
return lockHostId;
}
@ -3006,7 +3015,6 @@ public class VolumeServiceImpl implements VolumeService {
}
}
StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId());
if (pool != null && pool.getClusterId() != null) {
List<HostVO> hosts = _hostDao.findByClusterId(pool.getClusterId());
if (hosts != null && !hosts.isEmpty()) {

View File

@ -22,6 +22,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.eq;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
@ -99,6 +100,7 @@ public class VolumeServiceImplClvmTest {
public void setup() {
when(volumeInfoMock.getId()).thenReturn(VOLUME_ID);
when(volumeInfoMock.getUuid()).thenReturn("test-volume-uuid");
when(volumeInfoMock.getPath()).thenReturn("test-volume-path");
volumeService.storagePoolDao = storagePoolDao;
volumeService._hostDao = _hostDao;
@ -390,7 +392,10 @@ public class VolumeServiceImplClvmTest {
@Test
public void testFindVolumeLockHost_ExplicitLockFound() {
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid"))
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true)))
.thenReturn(HOST_ID_1);
Long result = volumeService.findVolumeLockHost(volumeInfoMock);
@ -399,7 +404,10 @@ public class VolumeServiceImplClvmTest {
@Test
public void testFindVolumeLockHost_FromAttachedVM() {
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid"))
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true)))
.thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(100L);
when(vmDao.findById(100L)).thenReturn(vmInstanceVOMock);
@ -412,11 +420,12 @@ public class VolumeServiceImplClvmTest {
@Test
public void testFindVolumeLockHost_FallbackToClusterHost() {
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid"))
.thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true)))
.thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(storagePoolVOMock.getClusterId()).thenReturn(10L);
when(hostVOMock.getId()).thenReturn(HOST_ID_1);
when(hostVOMock.getStatus()).thenReturn(com.cloud.host.Status.Up);
@ -428,11 +437,12 @@ public class VolumeServiceImplClvmTest {
@Test
public void testFindVolumeLockHost_NoHostFound() {
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid"))
.thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true)))
.thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(storagePoolVOMock.getClusterId()).thenReturn(10L);
when(_hostDao.findByClusterId(10L)).thenReturn(java.util.Collections.emptyList());
@ -445,8 +455,10 @@ public class VolumeServiceImplClvmTest {
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(volumeInfoMock.getId()).thenReturn(VOLUME_ID);
when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1");
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid")).thenReturn(HOST_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("/dev/vg1/volume-1"), eq(storagePoolVOMock), eq(true)))
.thenReturn(HOST_ID_1);
when(storagePoolVOMock.getName()).thenReturn("test-pool");
when(clvmLockManager.transferClvmVolumeLock(
"test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2))
@ -459,7 +471,11 @@ public class VolumeServiceImplClvmTest {
@Test
public void testPerformLockMigration_SameHost() {
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid")).thenReturn(HOST_ID_1);
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true)))
.thenReturn(HOST_ID_1);
VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_1);
assertEquals(volumeInfoMock, result);
@ -469,10 +485,11 @@ public class VolumeServiceImplClvmTest {
public void testPerformLockMigration_SourceHostNull() {
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(volumeInfoMock.getId()).thenReturn(VOLUME_ID);
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid")).thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true)))
.thenReturn(null);
when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(storagePoolVOMock.getClusterId()).thenReturn(null);
VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_2);
@ -489,8 +506,10 @@ public class VolumeServiceImplClvmTest {
when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1);
when(volumeInfoMock.getId()).thenReturn(VOLUME_ID);
when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1");
when(clvmLockManager.getClvmLockHostId(VOLUME_ID, "test-volume-uuid")).thenReturn(HOST_ID_1);
when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock);
when(clvmLockManager.getClvmLockHostId(
eq(VOLUME_ID), eq("test-volume-uuid"), eq("/dev/vg1/volume-1"), eq(storagePoolVOMock), eq(true)))
.thenReturn(HOST_ID_1);
when(storagePoolVOMock.getName()).thenReturn("test-pool");
when(clvmLockManager.transferClvmVolumeLock(
"test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2))

View File

@ -22,10 +22,12 @@ import org.apache.logging.log4j.LogManager;
import com.cloud.agent.api.Answer;
import org.apache.cloudstack.storage.command.ClvmLockTransferCommand;
import org.apache.cloudstack.storage.command.ClvmLockTransferAnswer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
import com.cloud.utils.script.OutputInterpreter;
@ResourceWrapper(handles = ClvmLockTransferCommand.class)
public class LibvirtClvmLockTransferCommandWrapper
@ -43,6 +45,11 @@ public class LibvirtClvmLockTransferCommandWrapper
operation, lvPath, volumeUuid);
try {
if (operation == ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE) {
return handleQueryLockState(cmd, lvPath, volumeUuid);
}
String lvchangeOpt;
String operationDesc;
switch (operation) {
@ -59,7 +66,7 @@ public class LibvirtClvmLockTransferCommandWrapper
operationDesc = "activated in shared mode";
break;
default:
return new Answer(cmd, false, "Unknown operation: " + operation);
return new ClvmLockTransferAnswer(cmd, false, "Unknown operation: " + operation);
}
Script script = new Script("/usr/sbin/lvchange", 30000, logger);
@ -71,20 +78,78 @@ public class LibvirtClvmLockTransferCommandWrapper
if (result != null) {
logger.error("CLVM lock transfer failed for volume {}: {}",
volumeUuid, result);
return new Answer(cmd, false,
return new ClvmLockTransferAnswer(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,
return new ClvmLockTransferAnswer(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());
return new ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage());
}
}
/**
* Query which host currently holds the CLVM lock for a volume.
* Executes: lvs -o lv_attr,lv_host --noheadings <lvPath>
*
* This queries the actual CLVM lock state (source of truth).
* The lv_host attribute shows which host currently has the volume activated.
*
* @return ClvmLockTransferAnswer with lock holder hostname
*/
private Answer handleQueryLockState(ClvmLockTransferCommand cmd, String lvPath, String volumeUuid) {
try {
Script script = new Script("/usr/sbin/lvs", 10000, logger);
script.add("-o");
script.add("lv_attr,lv_host");
script.add("--noheadings");
script.add(lvPath);
OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser();
String result = script.execute(parser);
if (result != null) {
logger.error("Failed to query lock state for volume {}: {}", volumeUuid, result);
return new ClvmLockTransferAnswer(cmd, false,
String.format("lvs command failed: %s", result));
}
// Parse output: " -wi-a-e--- host5.example.com"
String output = parser.getLine();
if (output == null || output.trim().isEmpty()) {
return new ClvmLockTransferAnswer(cmd, false, "No output from lvs command");
}
String[] parts = output.trim().split("\\s+");
if (parts.length < 1) {
return new ClvmLockTransferAnswer(cmd, false, "Invalid lvs output format");
}
String lvAttr = parts[0];
String hostname = parts.length > 1 ? parts[1] : null;
boolean isActive = lvAttr.length() > 4 && lvAttr.charAt(4) == 'a';
boolean isExclusive = lvAttr.length() > 5 && lvAttr.charAt(5) == 'e';
logger.info("Queried lock state for volume {}: attr={}, hostname={}, active={}, exclusive={}",
volumeUuid, lvAttr, hostname, isActive, isExclusive);
return new ClvmLockTransferAnswer(cmd, true,
String.format("Lock state: active=%s, exclusive=%s, host=%s",
isActive, isExclusive, hostname != null ? hostname : "none"),
hostname, isActive, isExclusive, lvAttr);
} catch (Exception e) {
logger.error("Exception during lock state query for volume {}: {}",
volumeUuid, e.getMessage(), e);
return new ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage());
}
}
}

View File

@ -138,6 +138,7 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef.DeviceType;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef.DiscardType;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef.DiskProtocol;
import com.cloud.storage.ClvmLockManager;
import com.cloud.storage.JavaStorageLayer;
import com.cloud.storage.MigrationOptions;
import com.cloud.storage.ScopeType;
@ -619,7 +620,9 @@ public class KVMStorageProcessor implements StorageProcessor {
String path = details != null ? details.get(DiskTO.IQN) : null;
storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details);
if (!ClvmLockManager.isClvmPoolType(primaryStore.getPoolType())) {
storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details);
}
final String volumeName = UUID.randomUUID().toString();
@ -648,7 +651,9 @@ public class KVMStorageProcessor implements StorageProcessor {
final KVMPhysicalDisk newDisk = storagePoolMgr.copyPhysicalDisk(volume, path != null ? path : volumeName, primaryPool, cmd.getWaitInMillSeconds());
resource.createOrUpdateLogFileForCommand(cmd, Command.State.COMPLETED);
storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path);
if (!ClvmLockManager.isClvmPoolType(primaryStore.getPoolType())) {
storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path);
}
final VolumeObjectTO newVol = new VolumeObjectTO();

View File

@ -2260,9 +2260,12 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
} else {
destFile = new QemuImgFile(destPath, destFormat);
try {
boolean isQCOW2 = PhysicalDiskFormat.QCOW2.equals(sourceFormat);
boolean keepBitmaps = PhysicalDiskFormat.QCOW2.equals(sourceFormat);
if (destPool.getType() == StoragePoolType.CLVM) {
keepBitmaps = false;
}
qemu.convert(srcFile, destFile, null, null, new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null),
null, false, isQCOW2);
null, false, keepBitmaps);
Map<String, String> destInfo = qemu.info(destFile);
Long virtualSize = Long.parseLong(destInfo.get(QemuImg.VIRTUAL_SIZE));
newDisk.setVirtualSize(virtualSize);

View File

@ -19,19 +19,22 @@
package com.cloud.storage;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.dao.VolumeDetailsDao;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.command.ClvmLockTransferCommand;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.command.ClvmLockTransferAnswer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
@ -51,6 +54,14 @@ public class ClvmLockManager {
return Arrays.asList(Storage.StoragePoolType.CLVM, Storage.StoragePoolType.CLVM_NG).contains(poolType);
}
/**
* Gets the CLVM lock host ID for a volume, optionally querying actual LVM state.
*
* @param volumeId The volume ID
* @param volumeUuid The volume UUID
* @return Host ID that holds the lock, or null if not found
* @deprecated Use getClvmLockHostId(volumeId, volumeUuid, volumePath, pool, queryActual) instead
*/
public Long getClvmLockHostId(Long volumeId, String volumeUuid) {
VolumeDetailVO detail = _volsDetailsDao.findDetail(volumeId, VolumeInfo.CLVM_LOCK_HOST_ID);
if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) {
@ -64,6 +75,31 @@ public class ClvmLockManager {
return null;
}
/**
* Gets the CLVM lock host ID for a volume, optionally querying actual LVM state.
* This method can query the actual lock state from LVM (source of truth) instead of
* relying solely on potentially stale database records.
*
* @param volumeId The volume ID
* @param volumeUuid The volume UUID
* @param volumePath The LV path (required if queryActual is true)
* @param pool The storage pool (required if queryActual is true)
* @param queryActual If true, queries actual LVM state instead of database
* @return Host ID that holds the lock, or null if not found
*/
public Long getClvmLockHostId(Long volumeId, String volumeUuid, String volumePath,
StoragePool pool, boolean queryActual) {
if (queryActual) {
if (volumePath == null || pool == null) {
logger.warn("Cannot query actual CLVM lock state for volume {} - missing volumePath or pool", volumeUuid);
return getClvmLockHostId(volumeId, volumeUuid);
}
return queryCurrentLockHolder(volumeId, volumeUuid, volumePath, pool, true);
}
return getClvmLockHostId(volumeId, volumeUuid);
}
/**
* Safely sets or updates the CLVM_LOCK_HOST_ID detail for a volume.
* If the detail already exists, it will be updated. Otherwise, it will be created.
@ -83,6 +119,125 @@ public class ClvmLockManager {
logger.debug("Created CLVM_LOCK_HOST_ID for volume {} with host {}", volumeId, hostId);
}
/**
* Query LVM to find the actual current lock holder for a volume.
* This is the SOURCE OF TRUTH - it queries the actual LVM state via sanlock/lvmlockd.
*
* @param volumeId The volume ID
* @param volumeUuid The volume UUID
* @param volumePath The LV path (e.g., "vm-123-disk-0")
* @param pool The storage pool
* @param updateDatabase If true, updates the database with the actual value (for debugging/audit)
* @return Host ID of current lock holder, or null if no lock is held or query failed
*/
public Long queryCurrentLockHolder(Long volumeId, String volumeUuid, String volumePath,
StoragePool pool, boolean updateDatabase) {
if (pool == null) {
logger.error("Cannot query CLVM lock for volume {} - pool is null", volumeUuid);
return null;
}
List<HostVO> hosts = null;
Long clusterId = pool.getClusterId();
if (clusterId != null) {
hosts = _hostDao.findByClusterId(clusterId, Host.Type.Routing);
logger.debug("Found {} routing hosts in cluster {} for pool {}",
hosts != null ? hosts.size() : 0, clusterId, pool.getName());
}
if ((hosts == null || hosts.isEmpty()) && pool.getDataCenterId() > 0) {
logger.debug("Pool {} is zone-scoped or no hosts in cluster, checking zone {} for available routing hosts",
pool.getName(), pool.getDataCenterId());
hosts = _hostDao.findByDataCenterId(pool.getDataCenterId());
}
if (hosts == null || hosts.isEmpty()) {
logger.warn("No KVM routing hosts found to query CLVM lock state for volume {} (pool: {}, cluster: {}, zone: {})",
volumeUuid, pool.getName(), clusterId, pool.getDataCenterId());
return null;
}
logger.debug("Querying lock state for volume {} from {} available hosts", volumeUuid, hosts.size());
for (HostVO host : hosts) {
if (host.getStatus() != Status.Up ||
host.getType() != com.cloud.host.Host.Type.Routing ||
host.getHypervisorType() != Hypervisor.HypervisorType.KVM) {
continue;
}
String vgName = pool.getPath();
if (vgName.startsWith("/")) {
vgName = vgName.substring(1);
}
String lvPath = String.format("/dev/%s/%s", vgName, volumePath);
try {
ClvmLockTransferCommand queryCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE,
lvPath,
volumeUuid
);
Answer answer = _agentMgr.send(host.getId(), queryCmd);
if (answer == null || !answer.getResult()) {
logger.debug("Failed to query lock state from host {}: {}",
host.getId(), answer != null ? answer.getDetails() : "null answer");
continue;
}
if (!(answer instanceof ClvmLockTransferAnswer)) {
logger.warn("Unexpected answer type for query lock state: {}", answer.getClass());
continue;
}
ClvmLockTransferAnswer queryAnswer = (ClvmLockTransferAnswer) answer;
String hostname = queryAnswer.getCurrentLockHostname();
if (hostname == null || hostname.isEmpty()) {
logger.debug("Volume {} is not locked (no exclusive lock held)", volumeUuid);
if (updateDatabase) {
VolumeDetailVO detail = _volsDetailsDao.findDetail(volumeId, VolumeInfo.CLVM_LOCK_HOST_ID);
if (detail != null) {
_volsDetailsDao.remove(detail.getId());
}
}
return null;
}
HostVO lockHost = _hostDao.findByName(hostname);
if (lockHost == null) {
logger.warn("Could not resolve hostname {} to host ID for volume {}",
hostname, volumeUuid);
return null;
}
Long lockHostId = lockHost.getId();
logger.info("Queried CLVM lock state for volume {}: locked by host {} ({}), exclusive={}",
volumeUuid, lockHostId, hostname, queryAnswer.isExclusive());
if (updateDatabase) {
Long dbHostId = getClvmLockHostId(volumeId, volumeUuid);
if (dbHostId == null || !dbHostId.equals(lockHostId)) {
logger.info("Correcting database: volume {} lock host: {} -> {} (actual)",
volumeUuid, dbHostId, lockHostId);
setClvmLockHostId(volumeId, lockHostId);
}
}
return lockHostId;
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.debug("Could not query host {} for lock state: {}", host.getId(), e.getMessage());
}
}
logger.warn("Could not query CLVM lock state for volume {} from any host", volumeUuid);
return null;
}
/**
* Cleans up CLVM lock host tracking detail from volume_details table.
* Called after successful volume deletion to prevent orphaned records.
@ -103,7 +258,7 @@ public class ClvmLockManager {
}
public boolean transferClvmVolumeLock(String volumeUuid, Long volumeId, String volumePath,
StoragePoolVO pool, Long sourceHostId, Long destHostId) {
StoragePool pool, Long sourceHostId, Long destHostId) {
if (pool == null) {
logger.error("Cannot transfer CLVM lock for volume {} - pool is null", volumeUuid);
return false;
@ -117,25 +272,36 @@ public class ClvmLockManager {
String lvPath = String.format("/dev/%s/%s", vgName, volumePath);
try {
if (!sourceHostId.equals(destHostId)) {
HostVO sourceHost = _hostDao.findById(sourceHostId);
if (sourceHost != null && sourceHost.getStatus() == Status.Up) {
Long actualLockHostId = queryCurrentLockHolder(volumeId, volumeUuid, volumePath, pool, false);
Long hostToDeactivate = actualLockHostId != null ? actualLockHostId : sourceHostId;
logger.info("Transferring CLVM lock for volume {}: actual holder={}, provided source={}, destination={}",
volumeUuid, actualLockHostId, sourceHostId, destHostId);
if (hostToDeactivate != null && !hostToDeactivate.equals(destHostId)) {
HostVO deactivateHost = _hostDao.findById(hostToDeactivate);
if (deactivateHost != null && deactivateHost.getStatus() == Status.Up) {
ClvmLockTransferCommand deactivateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
lvPath,
volumeUuid
);
Answer deactivateAnswer = _agentMgr.send(sourceHostId, deactivateCmd);
Answer deactivateAnswer = _agentMgr.send(hostToDeactivate, deactivateCmd);
if (deactivateAnswer == null || !deactivateAnswer.getResult()) {
logger.warn("Failed to deactivate CLVM volume {} on source host {}. Will attempt activation on destination.",
volumeUuid, sourceHostId);
logger.warn("Failed to deactivate CLVM volume {} on host {}. Will attempt activation on destination.",
volumeUuid, hostToDeactivate);
} else {
logger.debug("Successfully deactivated volume {} on host {}", volumeUuid, hostToDeactivate);
}
} else {
logger.warn("Source host {} is down. Will attempt force claim on destination host {}",
sourceHostId, destHostId);
logger.warn("Host {} (current lock holder) is down. Will attempt force claim on destination host {}",
hostToDeactivate, destHostId);
}
} else if (actualLockHostId == null) {
logger.debug("Volume {} has no active lock, will directly activate on destination", volumeUuid);
}
ClvmLockTransferCommand activateCmd = new ClvmLockTransferCommand(
@ -156,7 +322,7 @@ public class ClvmLockManager {
setClvmLockHostId(volumeId, destHostId);
logger.info("Successfully transferred CLVM lock for volume {} from host {} to host {}",
volumeUuid, sourceHostId, destHostId);
volumeUuid, actualLockHostId != null ? actualLockHostId : "none", destHostId);
return true;

View File

@ -21,11 +21,14 @@ import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.dao.VolumeDetailsDao;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.command.ClvmLockTransferAnswer;
import org.apache.cloudstack.storage.command.ClvmLockTransferCommand;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.Assert;
@ -37,6 +40,9 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
@ -69,7 +75,6 @@ public class ClvmLockManagerTest {
@Before
public void setUp() {
// Reset mocks before each test
Mockito.reset(volsDetailsDao, agentMgr, hostDao);
}
@ -226,4 +231,367 @@ public class ClvmLockManagerTest {
Assert.assertFalse(result);
}
@Test
public void testQueryCurrentLockHolder_NullPool() {
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, null, false);
Assert.assertNull(result);
verify(hostDao, never()).findByClusterId(anyLong(), any());
}
@Test
public void testQueryCurrentLockHolder_NoHostsInCluster() {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getDataCenterId()).thenReturn(1L);
when(pool.getName()).thenReturn("test-pool");
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.emptyList());
when(hostDao.findByDataCenterId(1L)).thenReturn(Collections.emptyList());
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
verify(hostDao, times(1)).findByClusterId(10L, Host.Type.Routing);
verify(hostDao, times(1)).findByDataCenterId(1L);
}
@Test
public void testQueryCurrentLockHolder_ZoneScopedPool() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(null);
when(pool.getDataCenterId()).thenReturn(1L);
when(pool.getName()).thenReturn("zone-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByDataCenterId(1L)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("host1")).thenReturn(host);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(hostDao, never()).findByClusterId(anyLong(), any());
verify(hostDao, times(1)).findByDataCenterId(1L);
}
@Test
public void testQueryCurrentLockHolder_SuccessfulQuery() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("host1")).thenReturn(host);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
}
@Test
public void testQueryCurrentLockHolder_VolumeNotLocked() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_EmptyHostname() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "", false, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_HostnameNotResolved() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "unknown-host", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("unknown-host")).thenReturn(null);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_QueryFails() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
Answer failedAnswer = new Answer(null, false, "Query failed");
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(failedAnswer);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_NullAnswer() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(null);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_AgentUnavailableException() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class)))
.thenThrow(new AgentUnavailableException("Host unavailable", HOST_ID_1));
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_OperationTimedoutException() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class)))
.thenThrow(new OperationTimedoutException(null, HOST_ID_1, 0, 0, false));
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_MatchingValue() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(detail);
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("host1")).thenReturn(host);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_1, result);
verify(volsDetailsDao, never()).update(anyLong(), any());
verify(volsDetailsDao, never()).addDetail(anyLong(), any(), any(), Mockito.anyBoolean());
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_DifferentValue() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_2, "host2", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
VolumeDetailVO detail = Mockito.mock(VolumeDetailVO.class);
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(detail);
when(detail.getId()).thenReturn(99L);
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host2", true, false, null);
when(agentMgr.send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("host2")).thenReturn(host);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_2, result);
verify(detail, times(1)).setValue(String.valueOf(HOST_ID_2));
verify(volsDetailsDao, times(1)).update(eq(99L), eq(detail));
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_NoExistingDetail() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(volsDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(null);
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("host1")).thenReturn(host);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_1, result);
verify(volsDetailsDao, times(1)).addDetail(eq(VOLUME_ID), eq(VolumeInfo.CLVM_LOCK_HOST_ID),
eq(String.valueOf(HOST_ID_1)), eq(false));
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_RemoveDetailWhenUnlocked() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
VolumeDetailVO detail = Mockito.mock(VolumeDetailVO.class);
when(detail.getId()).thenReturn(99L);
when(volsDetailsDao.findDetail(VOLUME_ID, VolumeInfo.CLVM_LOCK_HOST_ID)).thenReturn(detail);
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertNull(result);
verify(volsDetailsDao, times(1)).remove(99L);
}
@Test
public void testQueryCurrentLockHolder_SkipsNonKVMHosts() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO xenHost = createMockHost(10L, "xen-host", Status.Up, Hypervisor.HypervisorType.XenServer);
HostVO kvmHost = createMockHost(HOST_ID_1, "kvm-host", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(xenHost, kvmHost));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "kvm-host", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("kvm-host")).thenReturn(kvmHost);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(agentMgr, never()).send(eq(10L), any(ClvmLockTransferCommand.class));
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
}
@Test
public void testQueryCurrentLockHolder_SkipsDownHosts() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO downHost = createMockHost(10L, "down-host", Status.Down, Hypervisor.HypervisorType.KVM);
HostVO upHost = createMockHost(HOST_ID_1, "up-host", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(downHost, upHost));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "up-host", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("up-host")).thenReturn(upHost);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(agentMgr, never()).send(eq(10L), any(ClvmLockTransferCommand.class));
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
}
@Test
public void testQueryCurrentLockHolder_PathWithLeadingSlash() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn("/" + VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
when(hostDao.findByName("host1")).thenReturn(host);
Long result = clvmLockManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
}
// Helper method to create mock hosts
private HostVO createMockHost(Long id, String name, Status status, Hypervisor.HypervisorType hypervisor) {
HostVO host = Mockito.mock(HostVO.class);
Mockito.lenient().when(host.getId()).thenReturn(id);
Mockito.lenient().when(host.getName()).thenReturn(name);
Mockito.lenient().when(host.getStatus()).thenReturn(status);
Mockito.lenient().when(host.getType()).thenReturn(Host.Type.Routing);
Mockito.lenient().when(host.getHypervisorType()).thenReturn(hypervisor);
return host;
}
}