Skip to content

Commit 4984ee5

Browse files
committed
add support for migrating lvm lock
1 parent c9dd7ed commit 4984ee5

File tree

8 files changed

+802
-10
lines changed

8 files changed

+802
-10
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.storage.command;
19+
20+
import com.cloud.agent.api.Command;
21+
22+
/**
23+
* Command to transfer CLVM (Clustered LVM) exclusive lock between hosts.
24+
* This enables lightweight volume migration for CLVM storage pools where volumes
25+
* reside in the same Volume Group (VG) but need to be accessed from different hosts.
26+
*
27+
* <p>Instead of copying volume data (traditional migration), this command simply
28+
* deactivates the LV on the source host and activates it exclusively on the destination host.
29+
*
30+
* <p>This is significantly faster (10-100x) than traditional migration and uses no network bandwidth.
31+
*/
32+
public class ClvmLockTransferCommand extends Command {
33+
34+
/**
35+
* Operation to perform on the CLVM volume.
36+
* Maps to lvchange flags for LVM operations.
37+
*/
38+
public enum Operation {
39+
/** Deactivate the volume on this host (-an) */
40+
DEACTIVATE("-an", "deactivate"),
41+
42+
/** Activate the volume exclusively on this host (-aey) */
43+
ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"),
44+
45+
/** Activate the volume in shared mode on this host (-asy) */
46+
ACTIVATE_SHARED("-asy", "activate in shared mode");
47+
48+
private final String lvchangeFlag;
49+
private final String description;
50+
51+
Operation(String lvchangeFlag, String description) {
52+
this.lvchangeFlag = lvchangeFlag;
53+
this.description = description;
54+
}
55+
56+
public String getLvchangeFlag() {
57+
return lvchangeFlag;
58+
}
59+
60+
public String getDescription() {
61+
return description;
62+
}
63+
}
64+
65+
private String lvPath;
66+
private Operation operation;
67+
private String volumeUuid;
68+
69+
public ClvmLockTransferCommand() {
70+
// For serialization
71+
}
72+
73+
public ClvmLockTransferCommand(Operation operation, String lvPath, String volumeUuid) {
74+
this.operation = operation;
75+
this.lvPath = lvPath;
76+
this.volumeUuid = volumeUuid;
77+
// Execute in sequence to ensure lock safety
78+
setWait(30);
79+
}
80+
81+
public String getLvPath() {
82+
return lvPath;
83+
}
84+
85+
public Operation getOperation() {
86+
return operation;
87+
}
88+
89+
public String getVolumeUuid() {
90+
return volumeUuid;
91+
}
92+
93+
@Override
94+
public boolean executeInSequence() {
95+
return true;
96+
}
97+
}

engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@
3131

3232
public interface VolumeInfo extends DownloadableDataInfo, Volume {
3333

34+
/**
35+
* Constant for the volume detail key that stores the destination host ID for CLVM volume creation routing.
36+
* This helps ensure volumes are created on the correct host with exclusive locks.
37+
*/
38+
String DESTINATION_HOST_ID = "destinationHostId";
39+
40+
/**
41+
* Constant for the volume detail key that stores the host ID currently holding the CLVM exclusive lock.
42+
* This is used during lightweight lock migration to determine the source host for lock transfer.
43+
*/
44+
String CLVM_LOCK_HOST_ID = "clvmLockHostId";
45+
3446
boolean isAttachedVM();
3547

3648
void addPayload(Object data);
@@ -103,4 +115,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume {
103115
List<String> getCheckpointPaths();
104116

105117
Set<String> getCheckpointImageStoreUrls();
118+
119+
/**
120+
* Gets the destination host ID hint for CLVM volume creation.
121+
* This is used to route volume creation commands to the specific host where the VM will be deployed.
122+
* Only applicable for CLVM storage pools to avoid shared mode activation.
123+
*
124+
* @return The host ID where the volume should be created, or null if not set
125+
*/
126+
Long getDestinationHostId();
127+
128+
/**
129+
* Sets the destination host ID hint for CLVM volume creation.
130+
* This should be set before volume creation when the destination host is known.
131+
*
132+
* @param hostId The host ID where the volume should be created
133+
*/
134+
void setDestinationHostId(Long hostId);
106135
}

engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,15 @@ public VolumeInfo createVolume(VolumeInfo volumeInfo, VirtualMachine vm, Virtual
745745
logger.debug("Trying to create volume [{}] on storage pool [{}].",
746746
volumeToString, poolToString);
747747
DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary);
748+
749+
// For CLVM pools, set the destination host hint so volume is created on the correct host
750+
// This avoids the need for shared mode activation and improves performance
751+
if (pool.getPoolType() == Storage.StoragePoolType.CLVM && hostId != null) {
752+
logger.info("CLVM pool detected. Setting destination host {} for volume {} to route creation to correct host",
753+
hostId, volumeInfo.getUuid());
754+
volumeInfo.setDestinationHostId(hostId);
755+
}
756+
748757
for (int i = 0; i < 2; i++) {
749758
// retry one more time in case of template reload is required for Vmware case
750759
AsyncCallFuture<VolumeApiResult> future = null;
@@ -1851,6 +1860,20 @@ private Pair<VolumeVO, DataStore> recreateVolume(VolumeVO vol, VirtualMachinePro
18511860

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

engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232

3333
import com.cloud.dc.DedicatedResourceVO;
3434
import com.cloud.dc.dao.DedicatedResourceDao;
35+
import com.cloud.storage.Storage;
3536
import com.cloud.user.Account;
3637
import com.cloud.utils.Pair;
38+
import com.cloud.utils.db.QueryBuilder;
3739
import org.apache.cloudstack.context.CallContext;
3840
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
3941
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@@ -46,6 +48,7 @@
4648
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
4749
import org.apache.cloudstack.storage.LocalHostEndpoint;
4850
import org.apache.cloudstack.storage.RemoteHostEndPoint;
51+
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
4952
import org.apache.logging.log4j.Logger;
5053
import org.apache.logging.log4j.LogManager;
5154
import org.springframework.stereotype.Component;
@@ -59,8 +62,8 @@
5962
import com.cloud.storage.DataStoreRole;
6063
import com.cloud.storage.ScopeType;
6164
import com.cloud.storage.Storage.TemplateType;
65+
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
6266
import com.cloud.utils.db.DB;
63-
import com.cloud.utils.db.QueryBuilder;
6467
import com.cloud.utils.db.SearchCriteria.Op;
6568
import com.cloud.utils.db.TransactionLegacy;
6669
import com.cloud.utils.exception.CloudRuntimeException;
@@ -75,6 +78,8 @@ public class DefaultEndPointSelector implements EndPointSelector {
7578
private HostDao hostDao;
7679
@Inject
7780
private DedicatedResourceDao dedicatedResourceDao;
81+
@Inject
82+
private PrimaryDataStoreDao _storagePoolDao;
7883

7984
private static final String VOL_ENCRYPT_COLUMN_NAME = "volume_encryption_support";
8085
private final String findOneHostOnPrimaryStorage = "select t.id from "
@@ -264,6 +269,16 @@ public EndPoint select(DataObject srcData, DataObject destData) {
264269

265270
@Override
266271
public EndPoint select(DataObject srcData, DataObject destData, boolean volumeEncryptionSupportRequired) {
272+
// FOR CLVM: Check if destination is a volume with destinationHostId hint
273+
// This ensures template-to-volume copy is routed to the correct host for optimal lock placement
274+
if (destData instanceof VolumeInfo) {
275+
EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) destData, "template-to-volume copy");
276+
if (clvmEndpoint != null) {
277+
return clvmEndpoint;
278+
}
279+
}
280+
281+
// Default behavior for non-CLVM or when no destination host is set
267282
DataStore srcStore = srcData.getDataStore();
268283
DataStore destStore = destData.getDataStore();
269284
if (moveBetweenPrimaryImage(srcStore, destStore)) {
@@ -388,9 +403,59 @@ private List<HostVO> listUpAndConnectingSecondaryStorageVmHost(Long dcId) {
388403
return sc.list();
389404
}
390405

406+
/**
407+
* Selects endpoint for CLVM volumes with destination host hint.
408+
* This ensures volumes are created on the correct host with exclusive locks.
409+
*
410+
* @param volume The volume to check for CLVM routing
411+
* @param operation Description of the operation (for logging)
412+
* @return EndPoint for the destination host if CLVM routing applies, null otherwise
413+
*/
414+
private EndPoint selectClvmEndpointIfApplicable(VolumeInfo volume, String operation) {
415+
DataStore store = volume.getDataStore();
416+
417+
if (store.getRole() != DataStoreRole.Primary) {
418+
return null;
419+
}
420+
421+
// Check if this is a CLVM pool
422+
StoragePoolVO pool = _storagePoolDao.findById(store.getId());
423+
if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM) {
424+
return null;
425+
}
426+
427+
// Check if destination host hint is set
428+
Long destHostId = volume.getDestinationHostId();
429+
if (destHostId == null) {
430+
return null;
431+
}
432+
433+
logger.info("CLVM {}: routing volume {} to destination host {} for optimal exclusive lock placement",
434+
operation, volume.getUuid(), destHostId);
435+
436+
EndPoint ep = getEndPointFromHostId(destHostId);
437+
if (ep != null) {
438+
return ep;
439+
}
440+
441+
logger.warn("Could not get endpoint for destination host {}, falling back to default selection", destHostId);
442+
return null;
443+
}
444+
391445
@Override
392446
public EndPoint select(DataObject object, boolean encryptionSupportRequired) {
393447
DataStore store = object.getDataStore();
448+
449+
// For CLVM volumes with destination host hint, route to that specific host
450+
// This ensures volumes are created on the correct host with exclusive locks
451+
if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) {
452+
EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) object, "volume creation");
453+
if (clvmEndpoint != null) {
454+
return clvmEndpoint;
455+
}
456+
}
457+
458+
// Default behavior for non-CLVM or when no destination host is set
394459
if (store.getRole() == DataStoreRole.Primary) {
395460
return findEndPointInScope(store.getScope(), findOneHostOnPrimaryStorage, store.getId(), encryptionSupportRequired);
396461
}

engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public class VolumeObject implements VolumeInfo {
126126
private boolean directDownload;
127127
private String vSphereStoragePolicyId;
128128
private boolean followRedirects;
129+
private Long destinationHostId; // For CLVM: hints where volume should be created
129130

130131
private List<String> checkpointPaths;
131132
private Set<String> checkpointImageStoreUrls;
@@ -361,6 +362,27 @@ public void setDirectDownload(boolean directDownload) {
361362
this.directDownload = directDownload;
362363
}
363364

365+
@Override
366+
public Long getDestinationHostId() {
367+
// If not in memory, try to load from database (volume_details table)
368+
if (destinationHostId == null && volumeVO != null) {
369+
VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeVO.getId(), DESTINATION_HOST_ID);
370+
if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) {
371+
try {
372+
destinationHostId = Long.parseLong(detail.getValue());
373+
} catch (NumberFormatException e) {
374+
logger.warn("Invalid destinationHostId value in volume_details for volume {}: {}", volumeVO.getUuid(), detail.getValue());
375+
}
376+
}
377+
}
378+
return destinationHostId;
379+
}
380+
381+
@Override
382+
public void setDestinationHostId(Long hostId) {
383+
this.destinationHostId = hostId;
384+
}
385+
364386
public void update() {
365387
volumeDao.update(volumeVO.getId(), volumeVO);
366388
volumeVO = volumeDao.findById(volumeVO.getId());

0 commit comments

Comments
 (0)