Copilot commented on code in PR #12617:
URL: https://github.com/apache/cloudstack/pull/12617#discussion_r2962444971
##########
engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java:
##########
@@ -139,6 +141,18 @@ public boolean hostConnect(long hostId, long poolId)
throws StorageConflictExcep
Map<String, String> nfsMountOpts =
storageManager.getStoragePoolNFSMountOpts(pool, null).first();
Optional.ofNullable(nfsMountOpts).ifPresent(detailsMap::putAll);
+
+ // Propagate CLVM secure zero-fill setting to the host
+ // Note: This is done during host connection (agent start, MS restart,
host reconnection)
+ // so the setting is non-dynamic. Changes require host reconnection to
take effect.
+ if (ClvmLockManager.isClvmPoolType(pool.getPoolType())) {
+ Boolean clvmSecureZeroFill =
VolumeApiServiceImpl.CLVMSecureZeroFill.valueIn(poolId);
+ if (clvmSecureZeroFill != null) {
+ detailsMap.put("clvmsecurezerofill",
String.valueOf(clvmSecureZeroFill));
+ logger.debug("Added CLVM secure zero-fill setting: {} for
storage pool: {}", clvmSecureZeroFill, pool);
+ }
Review Comment:
This code hard-codes the detail key string `"clvmsecurezerofill"`. The same
key is also defined as `KVMStoragePool.CLVM_SECURE_ZERO_FILL`; having two
sources of truth risks drift/typos between management server and agent.
Consider moving this key to a shared constant accessible from both modules (or
otherwise centralizing it) and reference that constant here.
##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java:
##########
@@ -693,11 +848,248 @@ public KVMPhysicalDisk getPhysicalDisk(String
volumeUuid, KVMStoragePool pool) {
}
return disk;
} catch (LibvirtException e) {
- logger.debug("Failed to get physical disk:", e);
+ logger.debug("Failed to get volume from libvirt: " +
e.getMessage());
+ if (isClvmPool) {
+ return getPhysicalDiskWithClvmFallback(volumeUuid, pool,
libvirtPool);
+ }
+
throw new CloudRuntimeException(e.toString());
}
}
+ /**
+ * CLVM fallback: First tries to refresh libvirt pool to make volume
visible,
+ * if that fails, accesses volume directly via block device path.
+ */
+ private KVMPhysicalDisk getPhysicalDiskWithClvmFallback(String volumeUuid,
KVMStoragePool pool, LibvirtStoragePool libvirtPool) {
+ logger.info("CLVM volume not visible to libvirt, attempting pool
refresh for volume: {}", volumeUuid);
+
+ try {
+ logger.debug("Refreshing libvirt storage pool: {}",
pool.getUuid());
+ libvirtPool.getPool().refresh(0);
+
+ StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid);
+ if (vol != null) {
+ logger.info("Volume found after pool refresh: {}", volumeUuid);
+ KVMPhysicalDisk disk;
+ LibvirtStorageVolumeDef voldef =
getStorageVolumeDef(libvirtPool.getPool().getConnect(), vol);
+ disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool);
+ disk.setSize(vol.getInfo().allocation);
+ disk.setVirtualSize(vol.getInfo().capacity);
+ disk.setFormat(voldef.getFormat() ==
LibvirtStorageVolumeDef.VolumeFormat.QCOW2 ?
+ PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW);
+ return disk;
+ }
+ } catch (LibvirtException refreshEx) {
+ logger.debug("Pool refresh failed or volume still not found: {}",
refreshEx.getMessage());
+ }
+
+ logger.info("Falling back to direct block device access for volume:
{}", volumeUuid);
+ return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool);
+ }
+
+ private String getVgName(String sourceDir) {
+ String vgName = sourceDir;
+ if (vgName.startsWith("/")) {
+ String[] parts = vgName.split("/");
+ List<String> tokens = Arrays.stream(parts)
+ .filter(s -> !s.isEmpty()).collect(Collectors.toList());
+
+ vgName = tokens.size() > 1 ? tokens.get(1)
+ : tokens.size() == 1 ? tokens.get(0)
+ : "";
+ }
+ return vgName;
+ }
+
+ /**
+ * For CLVM volumes that exist in LVM but are not visible to libvirt,
+ * access them directly via block device path.
+ */
+ private KVMPhysicalDisk getPhysicalDiskViaDirectBlockDevice(String
volumeUuid, KVMStoragePool pool) {
+ try {
+ String vgName = extractVgNameFromPool(pool);
+ verifyLvExistsInVg(volumeUuid, vgName);
+
+ logger.info("Volume {} exists in LVM but not visible to libvirt,
accessing directly", volumeUuid);
+
+ String lvPath = findAccessibleDeviceNode(volumeUuid, vgName, pool);
+ long size = getClvmVolumeSize(lvPath);
+
+ KVMPhysicalDisk disk = createPhysicalDiskFromClvmLv(lvPath,
volumeUuid, pool, size);
+
+ ensureTemplateAccessibility(volumeUuid, lvPath, pool);
+
+ return disk;
+
+ } catch (CloudRuntimeException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ logger.error("Failed to access CLVM volume via direct block
device: {}", volumeUuid, ex);
+ throw new CloudRuntimeException(String.format("Could not find
volume %s: %s ", volumeUuid, ex.getMessage()));
+ }
+ }
+
+ private String extractVgNameFromPool(KVMStoragePool pool) {
+ String sourceDir = pool.getLocalPath();
+ if (sourceDir == null || sourceDir.isEmpty()) {
+ throw new CloudRuntimeException("CLVM pool sourceDir is not set,
cannot determine VG name");
+ }
+ String vgName = getVgName(sourceDir);
+ logger.debug("Using VG name: {} (from sourceDir: {})", vgName,
sourceDir);
+ return vgName;
+ }
+
+ private void verifyLvExistsInVg(String volumeUuid, String vgName) {
+ logger.debug("Checking if volume {} exists in VG {}", volumeUuid,
vgName);
+ Script checkLvCmd = new Script("/usr/sbin/lvs", 5000, logger);
+ checkLvCmd.add("--noheadings");
+ checkLvCmd.add("--unbuffered");
+ checkLvCmd.add(vgName + "/" + volumeUuid);
+
+ String checkResult = checkLvCmd.execute();
+ if (checkResult != null) {
+ logger.debug("Volume {} does not exist in VG {}: {}", volumeUuid,
vgName, checkResult);
+ throw new CloudRuntimeException(String.format("Storage volume not
found: no storage vol with matching name '%s'", volumeUuid));
+ }
+ }
+
+ private String findAccessibleDeviceNode(String volumeUuid, String vgName,
KVMStoragePool pool) {
+ String lvPath = "/dev/" + vgName + "/" + volumeUuid;
+ File lvDevice = new File(lvPath);
+
+ if (!lvDevice.exists()) {
+ lvPath = tryDeviceMapperPath(volumeUuid, vgName, lvDevice);
+
+ if (!lvDevice.exists()) {
+ lvPath = handleMissingDeviceNode(volumeUuid, vgName, pool);
+ }
+ }
+
+ return lvPath;
+ }
+
+ private String tryDeviceMapperPath(String volumeUuid, String vgName, File
lvDevice) {
+ String vgNameEscaped = vgName.replace("-", "--");
+ String volumeUuidEscaped = volumeUuid.replace("-", "--");
+ String mapperPath = "/dev/mapper/" + vgNameEscaped + "-" +
volumeUuidEscaped;
+ File mapperDevice = new File(mapperPath);
+
+ if (!mapperDevice.exists()) {
+ lvDevice = mapperDevice;
+ }
+
+ return mapperPath;
+ }
Review Comment:
`findAccessibleDeviceNode` attempts to fall back to `/dev/mapper/...` if
`/dev/<vg>/<lv>` doesn’t exist, but the logic in `tryDeviceMapperPath` is
broken: it checks `if (!mapperDevice.exists())` (reversed) and assigning to
`lvDevice` has no effect on the caller. As a result, even if the mapper node
exists, the method will still think it’s missing and may throw incorrectly.
Rework this to check the mapper path existence and return the correct path (or
return a `File`/boolean) so the fallback actually works.
##########
server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java:
##########
@@ -2631,6 +2662,351 @@ private Volume orchestrateAttachVolumeToVM(Long vmId,
Long volumeId, Long device
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;
+ }
Review Comment:
The new CLVM attachment/migration helpers only recognize
`StoragePoolType.CLVM` (e.g., `areBothPoolsClvmType` and `isClvmPool`). Since
this PR introduces `CLVM_NG` and `ClvmLockManager.isClvmPoolType` already
treats both as CLVM-like, these helpers will silently skip the lightweight
lock-transfer path for CLVM_NG volumes. Update the helper predicates to include
`CLVM_NG` (or reuse a shared helper) so behavior is consistent for both pool
types.
##########
pom.xml:
##########
@@ -55,6 +55,10 @@
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.exclusions>engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java</sonar.exclusions>
<sonar.exclusions>api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java</sonar.exclusions>
+
<sonar.exclusions>core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java</sonar.exclusions>
+
<sonar.exclusions>core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java</sonar.exclusions>
+
<sonar.exclusions>core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java</sonar.exclusions>
+
<sonar.exclusions>core/src/main/java/org/apache/cloudstack/storage/command/ClvmLockTransferCommand.java</sonar.exclusions>
Review Comment:
`sonar.exclusions` is declared multiple times. In Maven, later property
declarations override earlier ones, so only the last exclusion will actually be
used. Combine all exclusions into a single `sonar.exclusions` property
(comma-separated) so none of the earlier exclusions are lost.
##########
engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java:
##########
@@ -32,8 +32,13 @@
import com.cloud.dc.DedicatedResourceVO;
import com.cloud.dc.dao.DedicatedResourceDao;
+import com.cloud.storage.ClvmLockManager;
+import com.cloud.storage.Storage;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.VolumeDetailsDao;
Review Comment:
This module (`cloud-engine-storage`) does not depend on `cloud-server` (see
the commented-out dependency in `engine/storage/pom.xml`), but this file
imports `com.cloud.storage.ClvmLockManager` which is defined under `server/`.
That will break compilation for this module. Consider removing this dependency
(e.g., inline the pool-type check here), or move `ClvmLockManager` (or at least
its `isClvmPoolType` helper) into a module that `cloud-engine-storage` depends
on (api/core/engine).
##########
engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java:
##########
@@ -388,18 +406,91 @@ private List<HostVO>
listUpAndConnectingSecondaryStorageVmHost(Long dcId) {
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 ||
Review Comment:
The CLVM pool type check is logically incorrect: `(poolType != CLVM ||
poolType != CLVM_NG)` is always true, so this method will always return `null`
and CLVM routing will never be applied. Change this to an AND check (or use a
helper like `isClvmPoolType`) so CLVM/CLVM_NG volumes can actually select the
intended endpoint.
```suggestion
(pool.getPoolType() != Storage.StoragePoolType.CLVM &&
```
##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java:
##########
@@ -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 {}:
{}}",
Review Comment:
Log message placeholder has an extra `}` (`{}}`), resulting in malformed
output. Replace `{}}` with `{}` (and similarly fix the other affected log lines
in this class).
```suggestion
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 {}:
{}",
```
##########
engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java:
##########
@@ -37,12 +37,14 @@
import com.cloud.network.dao.NetworkVO;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.offerings.dao.NetworkOfferingDao;
+import com.cloud.storage.ClvmLockManager;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.StorageService;
+import com.cloud.storage.VolumeApiServiceImpl;
import com.cloud.storage.dao.StoragePoolHostDao;
Review Comment:
`cloud-engine-storage-volume` does not have a dependency on `cloud-server`,
but this class imports `com.cloud.storage.ClvmLockManager` and
`com.cloud.storage.VolumeApiServiceImpl` (both live under `server/`). This will
fail compilation for the engine module. To fix, avoid referencing server-layer
classes here (e.g., move `CLVMSecureZeroFill` ConfigKey and the CLVM pool-type
helper into an api/core module, or query the config via configuration DAOs
available in this module).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]