diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 5b3e97698fd..ddf5978497b 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -170,6 +170,7 @@ public class Storage { ISO(false, false, EncryptionSupport.Unsupported), // for iso image LVM(false, false, EncryptionSupport.Unsupported), // XenServer local LVM SR CLVM(true, false, EncryptionSupport.Unsupported), + CLVM_NG(true, false, EncryptionSupport.Hypervisor), RBD(true, true, EncryptionSupport.Unsupported), // http://libvirt.org/storage.html#StorageBackendRBD SharedMountPoint(true, true, EncryptionSupport.Hypervisor), VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage diff --git a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java index 5ac4e9ae445..7196247ffc2 100644 --- a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java +++ b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java @@ -26,6 +26,7 @@ import java.util.Map; import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.storage.Storage; public class MigrateCommand extends Command { private String vmName; @@ -42,6 +43,7 @@ public class MigrateCommand extends Command { private Map dpdkInterfaceMapping = new HashMap<>(); private int newVmCpuShares; + private boolean clvmCrossPoolMigration; Map vlanToPersistenceMap = new HashMap<>(); @@ -149,6 +151,14 @@ public class MigrateCommand extends Command { this.newVmCpuShares = newVmCpuShares; } + public boolean isClvmCrossPoolMigration() { + return clvmCrossPoolMigration; + } + + public void setClvmCrossPoolMigration(boolean clvmCrossPoolMigration) { + this.clvmCrossPoolMigration = clvmCrossPoolMigration; + } + public static class MigrateDiskInfo { public enum DiskType { FILE, BLOCK; @@ -184,6 +194,8 @@ public class MigrateCommand extends Command { private final String sourceText; private final String backingStoreText; private boolean isSourceDiskOnStorageFileSystem; + private Storage.StoragePoolType sourcePoolType; + private Storage.StoragePoolType destPoolType; public MigrateDiskInfo(final String serialNumber, final DiskType diskType, final DriverType driverType, final Source source, final String sourceText) { this.serialNumber = serialNumber; @@ -232,6 +244,22 @@ public class MigrateCommand extends Command { public void setSourceDiskOnStorageFileSystem(boolean isDiskOnFileSystemStorage) { this.isSourceDiskOnStorageFileSystem = isDiskOnFileSystemStorage; } + + public Storage.StoragePoolType getSourcePoolType() { + return sourcePoolType; + } + + public void setSourcePoolType(Storage.StoragePoolType sourcePoolType) { + this.sourcePoolType = sourcePoolType; + } + + public Storage.StoragePoolType getDestPoolType() { + return destPoolType; + } + + public void setDestPoolType(Storage.StoragePoolType destPoolType) { + this.destPoolType = destPoolType; + } } @Override diff --git a/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java b/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java new file mode 100644 index 00000000000..24fdf840202 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java @@ -0,0 +1,42 @@ +// +// 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.agent.api; + +/** + * Answer for PostMigrationCommand. + * Indicates success or failure of post-migration operations on the destination host. + */ +public class PostMigrationAnswer extends Answer { + + protected PostMigrationAnswer() { + } + + public PostMigrationAnswer(PostMigrationCommand cmd, String detail) { + super(cmd, false, detail); + } + + public PostMigrationAnswer(PostMigrationCommand cmd, Exception ex) { + super(cmd, ex); + } + + public PostMigrationAnswer(PostMigrationCommand cmd) { + super(cmd, true, null); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java b/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java new file mode 100644 index 00000000000..938000c3593 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java @@ -0,0 +1,59 @@ +// +// 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.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + * PostMigrationCommand is sent to the destination host after a successful VM migration. + * It performs post-migration tasks such as: + * - Claiming exclusive locks on CLVM volumes (converting from shared to exclusive mode) + * - Other post-migration cleanup operations + */ +public class PostMigrationCommand extends Command { + private VirtualMachineTO vm; + private String vmName; + + protected PostMigrationCommand() { + } + + public PostMigrationCommand(VirtualMachineTO vm, String vmName) { + this.vm = vm; + this.vmName = vmName; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + @Override + public boolean isBypassHostMaintenance() { + return true; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java b/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java new file mode 100644 index 00000000000..3ff30391eae --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java @@ -0,0 +1,61 @@ +// +// 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.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + * PreMigrationCommand is sent to the source host before VM migration starts. + * It performs pre-migration tasks such as: + * - Converting CLVM volume exclusive locks to shared mode so destination host can access them + * - Other pre-migration preparation operations on the source host + * + * This command runs on the SOURCE host before PrepareForMigrationCommand runs on the DESTINATION host. + */ +public class PreMigrationCommand extends Command { + private VirtualMachineTO vm; + private String vmName; + + protected PreMigrationCommand() { + } + + public PreMigrationCommand(VirtualMachineTO vm, String vmName) { + this.vm = vm; + this.vmName = vmName; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + @Override + public boolean isBypassHostMaintenance() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java new file mode 100644 index 00000000000..f3c43c400b2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.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 org.apache.cloudstack.storage.clvm.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 isOpen; + 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 isOpen, + String lvAttributes) { + super(cmd, result, details); + this.currentLockHostname = currentLockHostname; + this.isActive = isActive; + this.isOpen = isOpen; + this.lvAttributes = lvAttributes; + } + + /** + * Get the hostname from lv_host. Retained for diagnostics only — + * do NOT use this to determine lock holder identity. + */ + public String getCurrentLockHostname() { + return currentLockHostname; + } + + public void setCurrentLockHostname(String currentLockHostname) { + this.currentLockHostname = currentLockHostname; + } + + /** + * Whether the LV is locally active on the queried host (lv_attr[4]=='a'). + * This is the authoritative signal for lock holder discovery via fan-out. + */ + public boolean isActive() { + return isActive; + } + + public void setActive(boolean active) { + isActive = active; + } + + /** + * Whether a process has the device file open on the queried host (lv_attr[5]=='o'). + * true means a VM is actively doing I/O on this host right now — do NOT deactivate. + */ + public boolean isOpen() { + return isOpen; + } + + public void setOpen(boolean open) { + isOpen = open; + } + + public String getLvAttributes() { + return lvAttributes; + } + + public void setLvAttributes(String lvAttributes) { + this.lvAttributes = lvAttributes; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java new file mode 100644 index 00000000000..218fb798869 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java @@ -0,0 +1,99 @@ +// 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.clvm.command; + +import com.cloud.agent.api.Command; + +/** + * Command to transfer CLVM (Clustered LVM) exclusive lock between hosts. + * This enables lightweight volume migration for CLVM storage pools where volumes + * reside in the same Volume Group (VG) but need to be accessed from different hosts. + * + *

Instead of copying volume data (traditional migration), this command simply + * deactivates the LV on the source host and activates it exclusively on the destination host. + * + *

This is significantly faster (10-100x) than traditional migration and uses no network bandwidth. + */ +public class ClvmLockTransferCommand extends Command { + + /** + * Operation to perform on the CLVM volume. + * Maps to lvchange flags for LVM operations. + */ + public enum Operation { + /** Deactivate the volume on this host (-an) */ + DEACTIVATE("-an", "deactivate"), + + /** Activate the volume exclusively on this host (-aey) */ + ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"), + + /** Activate the volume in shared mode on this host (-asy) */ + ACTIVATE_SHARED("-asy", "activate in shared mode"), + + /** 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; + + Operation(String lvchangeFlag, String description) { + this.lvchangeFlag = lvchangeFlag; + this.description = description; + } + + public String getLvchangeFlag() { + return lvchangeFlag; + } + + public String getDescription() { + return description; + } + } + + private String lvPath; + private Operation operation; + private String volumeUuid; + + public ClvmLockTransferCommand() { + // For serialization + } + + public ClvmLockTransferCommand(Operation operation, String lvPath, String volumeUuid) { + this.operation = operation; + this.lvPath = lvPath; + this.volumeUuid = volumeUuid; + setWait(65); + } + + public String getLvPath() { + return lvPath; + } + + public Operation getOperation() { + return operation; + } + + public String getVolumeUuid() { + return volumeUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java index 8b017187076..4937edd33d1 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java @@ -103,4 +103,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume { List getCheckpointPaths(); Set getCheckpointImageStoreUrls(); + + /** + * Gets the destination host ID hint for CLVM volume creation. + * This is used to route volume creation commands to the specific host where the VM will be deployed. + * Only applicable for CLVM storage pools to avoid shared mode activation. + * + * @return The host ID where the volume should be created, or null if not set + */ + Long getDestinationHostId(); + + /** + * Sets the destination host ID hint for CLVM volume creation. + * This should be set before volume creation when the destination host is known. + * + * @param hostId The host ID where the volume should be created + */ + void setDestinationHostId(Long hostId); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java index 682473ec94f..a7d82d0b962 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java @@ -30,6 +30,7 @@ import com.cloud.exception.StorageAccessException; import com.cloud.host.Host; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.offering.DiskOffering; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.Volume; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -123,4 +124,71 @@ public interface VolumeService { void checkAndRepairVolumeBasedOnConfig(DataObject dataObject, Host host); void validateChangeDiskOfferingEncryptionType(long existingDiskOfferingId, long newDiskOfferingId); + + /** + * Transfers exclusive lock for a volume on cluster-based storage (e.g., CLVM/CLVM_NG) from one host to another. + * This is used for storage that requires host-level lock management for volumes on shared storage pools. + * For non-CLVM pool types, this method returns false without taking action. + * + * @param volume The volume to transfer lock for + * @param sourceHostId Host currently holding the exclusive lock + * @param destHostId Host to receive the exclusive lock + * @return true if lock transfer succeeded or was not needed, false if it failed + */ + boolean transferVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId); + + /** + * Finds which host currently has the exclusive lock on a CLVM volume. + * Checks in order: explicit lock tracking, attached VM's host, or first available cluster host. + * + * @param volume The CLVM volume + * @return Host ID that has the exclusive lock, or null if cannot be determined + */ + Long findVolumeLockHost(VolumeInfo volume); + + /** + * Performs lightweight CLVM lock migration for a volume to a target host. + * This transfers the LVM exclusive lock without copying data (CLVM volumes are on shared cluster storage). + * If the volume already has the lock on the destination host, no action is taken. + * + * @param volume The volume to migrate lock for + * @param destHostId Destination host ID + * @return Updated VolumeInfo after lock migration + */ + VolumeInfo performLockMigration(VolumeInfo volume, Long destHostId); + + /** + * Checks if both storage pools are CLVM type (CLVM or CLVM_NG). + * + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM + * @return true if both pools are CLVM type (CLVM or CLVM_NG) + */ + boolean areBothPoolsClvmType(StoragePoolType volumePoolType, StoragePoolType vmPoolType); + + /** + * Determines if CLVM lock transfer is required when a volume is already on the correct storage pool. + * + * @param volumeToAttach The volume being attached + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM's existing volume + * @param volumePoolId Storage pool ID for the volume + * @param vmPoolId Storage pool ID for the VM's existing volume + * @param vmHostId VM's current host ID (or last host ID if stopped) + * @return true if CLVM lock transfer is needed + */ + boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType volumePoolType, StoragePoolType vmPoolType, + Long volumePoolId, Long vmPoolId, Long vmHostId); + + /** + * Determines if lightweight CLVM migration is needed instead of full data copy. + * + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM + * @param volumePoolPath Storage pool path for the volume + * @param vmPoolPath Storage pool path for the VM + * @return true if lightweight migration should be used + */ + boolean isLightweightMigrationNeeded(StoragePoolType volumePoolType, StoragePoolType vmPoolType, + String volumePoolPath, String vmPoolPath); } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index db50a5134d8..c27240823b1 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -50,6 +50,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import javax.persistence.EntityExistsException; +import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.hypervisor.KVMGuru; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; @@ -136,6 +138,7 @@ import com.cloud.agent.api.PrepareExternalProvisioningAnswer; import com.cloud.agent.api.PrepareExternalProvisioningCommand; import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.PreMigrationCommand; import com.cloud.agent.api.RebootAnswer; import com.cloud.agent.api.RebootCommand; import com.cloud.agent.api.RecreateCheckpointsCommand; @@ -267,6 +270,7 @@ import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; @@ -361,6 +365,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private VolumeDao _volsDao; @Inject + private VolumeDetailsDao _volsDetailsDao; + @Inject private HighAvailabilityManager _haMgr; @Inject private HostPodDao _podDao; @@ -463,6 +469,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac ExtensionsManager extensionsManager; @Inject ExtensionDetailsDao extensionDetailsDao; + @Inject + ClvmPoolManager clvmPoolManager; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -3150,6 +3158,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac updateOverCommitRatioForVmProfile(profile, dest.getHost().getClusterId()); final VirtualMachineTO to = toVmTO(profile); + + executePreMigrationCommand(vm, to, srcHostId); + final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to); setVmNetworkDetails(vm, to); @@ -3281,6 +3292,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac logger.warn("Error while checking the vm {} on host {}", vm, dest.getHost(), e); } migrated = true; + executePostMigrationCommand(vm, to, dstHostId); } finally { if (!migrated) { logger.info("Migration was unsuccessful. Cleaning up: {}", vm); @@ -3320,6 +3332,30 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } } + private void executePostMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long dstHostId) { + if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { + return; + } + final String dstHostUuid = _hostDao.findById(dstHostId).getUuid(); + try { + logger.info("Executing post-migration tasks for VM {} with CLVM volumes on destination host {}", vm.getInstanceName(), dstHostUuid); + final PostMigrationCommand postMigrationCommand = new PostMigrationCommand(to, vm.getInstanceName()); + final Answer postMigrationAnswer = _agentMgr.send(dstHostId, postMigrationCommand); + + if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) { + final String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned"; + logger.warn("Post-migration tasks failed for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.", + vm.getInstanceName(), dstHostUuid, details); + } else { + logger.info("Successfully completed post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostUuid); + } + } catch (Exception e) { + logger.warn("Exception during post-migration tasks for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.", + vm.getInstanceName(), dstHostUuid, e.getMessage(), e); + } + updateClvmLockHostForVmVolumes(vm.getId(), dstHostId); + } + /** * Create and set parameters for the {@link MigrateCommand} used in the migration and scaling of VMs. */ @@ -3366,6 +3402,27 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac _vmDao.persist(newVm); } + /** + * Updates CLVM_LOCK_HOST_ID for all CLVM volumes attached to a VM after VM migration. + * This ensures that subsequent operations on CLVM volumes are routed to the correct host. + * + * @param vmId The ID of the VM that was migrated + * @param destHostId The destination host ID where the VM now resides + */ + private void updateClvmLockHostForVmVolumes(long vmId, long destHostId) { + List volumes = _volsDao.findByInstance(vmId); + if (CollectionUtils.isEmpty(volumes)) { + return; + } + + for (VolumeVO volume : volumes) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + clvmPoolManager.setClvmLockHostId(volume.getId(), destHostId); + } + } + } + /** * We create the mapping of volumes and storage pool to migrate the VMs according to the information sent by the user. * If the user did not enter a complete mapping, the volumes that were left behind will be auto mapped using {@link #createStoragePoolMappingsForVolumes(VirtualMachineProfile, DataCenterDeployment, Map, List)} @@ -4922,6 +4979,12 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac volumeMgr.prepareForMigration(profile, dest); final VirtualMachineTO to = toVmTO(profile); + + // Step 1: Send PreMigrationCommand to source host to convert CLVM volumes to shared mode + // This must happen BEFORE PrepareForMigrationCommand on destination to avoid lock conflicts + executePreMigrationCommand(vm, to, srcHostId); + + // Step 2: Send PrepareForMigrationCommand to destination host final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to); ItWorkVO work = new ItWorkVO(UUID.randomUUID().toString(), _nodeId, State.Migrating, vm.getType(), vm.getId()); @@ -5006,6 +5069,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } migrated = true; + executePostMigrationCommand(vm, to, dstHostId); } finally { if (!migrated) { logger.info("Migration was unsuccessful. Cleaning up: {}", vm); @@ -6441,6 +6505,37 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac return findClusterAndHostIdForVm(vm, false); } + private boolean hasClvmVolumes(long vmId) { + List volumes = _volsDao.findByInstance(vmId); + return volumes.stream() + .map(v -> _storagePoolDao.findById(v.getPoolId())) + .anyMatch(pool -> pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())); + } + + private void executePreMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long srcHostId) { + if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { + return; + } + final String vmInstanceName = vm.getInstanceName(); + final String srcHostUuid = _hostDao.findById(srcHostId).getUuid(); + logger.info("Sending PreMigrationCommand to source host {} for VM {} with CLVM volumes", srcHostUuid, vmInstanceName); + final PreMigrationCommand preMigCmd = new PreMigrationCommand(to, vmInstanceName); + Answer preMigAnswer = null; + try { + preMigAnswer = _agentMgr.send(srcHostId, preMigCmd); + if (preMigAnswer == null || !preMigAnswer.getResult()) { + final String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned"; + final String msg = "Failed to prepare source host for migration: " + details; + logger.error("Failed to prepare source host {} for migration of VM {}: {}", srcHostUuid, vmInstanceName, details); + throw new CloudRuntimeException(msg); + } + logger.info("Successfully prepared source host {} for migration of VM {}", srcHostUuid, vmInstanceName); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + logger.error("Failed to send PreMigrationCommand to source host {}: {}", srcHostUuid, e.getMessage(), e); + throw new CloudRuntimeException("Failed to prepare source host for migration: " + e.getMessage(), e); + } + } + @Override public Pair findClusterAndHostIdForVm(long vmId) { VMInstanceVO vm = _vmDao.findById(vmId); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index bf3985d3ce7..c4a6a9dd684 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -38,8 +38,10 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.agent.AgentManager; import com.cloud.deploy.DeploymentClusterPlanner; import com.cloud.exception.ResourceAllocationException; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.resourcelimit.ReservationHelper; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.VMTemplateVO; @@ -275,6 +277,10 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati ConfigurationDao configurationDao; @Inject VMInstanceDao vmInstanceDao; + @Inject + ClvmPoolManager clvmPoolManager; + @Inject + AgentManager _agentMgr; @Inject protected SnapshotHelper snapshotHelper; @@ -747,6 +753,17 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati logger.debug("Trying to create volume [{}] on storage pool [{}].", volumeToString, poolToString); DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); + + // For CLVM pools, set the lock host hint so volume is created on the correct host + // This avoids the need for shared mode activation and improves performance + if (ClvmPoolManager.isClvmPoolType(pool.getPoolType()) && hostId != null) { + logger.info("CLVM pool detected. Setting lock host {} for volume {} to route creation to correct host", + hostId, volumeInfo.getUuid()); + volumeInfo.setDestinationHostId(hostId); + + clvmPoolManager.setClvmLockHostId(volumeInfo.getId(), hostId); + } + for (int i = 0; i < 2; i++) { // retry one more time in case of template reload is required for Vmware case AsyncCallFuture future = null; @@ -788,6 +805,122 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati return String.format("uuid: %s, name: %s", volume.getUuid(), volume.getName()); } + /** + * Updates the CLVM_LOCK_HOST_ID for a migrated volume if applicable. + * For CLVM volumes that are attached to a VM, this updates the lock host tracking + * to point to the VM's current host after volume migration. + * + * @param migratedVolume The volume that was migrated + * @param destPool The destination storage pool + * @param operationType Description of the operation (e.g., "migrated", "live-migrated") for logging + */ + private void updateClvmLockHostAfterMigration(Volume migratedVolume, StoragePool destPool, String operationType) { + if (migratedVolume == null || destPool == null) { + return; + } + + StoragePoolVO pool = _storagePoolDao.findById(destPool.getId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return; + } + + if (migratedVolume.getInstanceId() == null) { + return; + } + + VMInstanceVO vm = vmInstanceDao.findById(migratedVolume.getInstanceId()); + if (vm == null || vm.getHostId() == null) { + return; + } + + clvmPoolManager.setClvmLockHostId(migratedVolume.getId(), vm.getHostId()); + logger.debug("Updated CLVM_LOCK_HOST_ID for {} volume {} to host {} where VM {} is running", + operationType, migratedVolume.getUuid(), vm.getHostId(), vm.getInstanceName()); + } + + /** + * Retrieves the CLVM lock host ID from any existing volume of the specified VM. + * This is useful when attaching a new volume to a stopped VM - we want to maintain + * consistency by using the same host that manages the VM's other CLVM volumes. + * + * @param vmId The ID of the VM + * @return The host ID if found, null otherwise + */ + private Long getClvmLockHostFromVmVolumes(Long vmId) { + if (vmId == null) { + return null; + } + + List vmVolumes = _volsDao.findByInstance(vmId); + if (vmVolumes == null || vmVolumes.isEmpty()) { + return null; + } + + for (VolumeVO volume : vmVolumes) { + if (volume.getPoolId() == null) { + continue; + } + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + continue; + } + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + if (lockHostId != null) { + logger.debug("Found actual CLVM lock host {} from volume {} of VM {} via LVM query", + lockHostId, volume.getUuid(), vmId); + return lockHostId; + } + } + + return null; + } + + private void transferClvmLocksForVmStart(List volumes, Long destHostId, VMInstanceVO vm) { + if (volumes == null || volumes.isEmpty() || destHostId == null) { + return; + } + + for (VolumeVO volume : volumes) { + if (volume.getPoolId() == null) { + continue; + } + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + continue; + } + + Long currentLockHost = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (currentLockHost == null) { + clvmPoolManager.setClvmLockHostId(volume.getId(), destHostId); + } else if (!currentLockHost.equals(destHostId)) { + logger.info("CLVM volume {} is locked on host {} but VM {} starting on host {}. Transferring lock.", + volume.getUuid(), currentLockHost, vm.getInstanceName(), destHostId); + + if (!clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), + volume.getPath(), pool, currentLockHost, destHostId)) { + throw new CloudRuntimeException( + String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s", + volume.getUuid(), currentLockHost, destHostId)); + } + } + } + } + public String getRandomVolumeName() { return UUID.randomUUID().toString(); } @@ -1206,10 +1339,22 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati Long clusterId = storagePool.getClusterId(); logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); Long hostId = vm.getHostId(); - if (hostId == null && storagePool.isLocal()) { - List poolHosts = storagePoolHostDao.listByPoolId(storagePool.getId()); - if (poolHosts.size() > 0) { - hostId = poolHosts.get(0).getHostId(); + if (hostId == null && (storagePool.isLocal() || ClvmPoolManager.isClvmPoolType(storagePool.getPoolType()))) { + if (ClvmPoolManager.isClvmPoolType(storagePool.getPoolType())) { + hostId = getClvmLockHostFromVmVolumes(vm.getId()); + if (hostId != null) { + logger.debug("Using CLVM lock host {} from VM {}'s existing volumes for new volume creation", + hostId, vm.getUuid()); + } + } + + if (hostId == null) { + List poolHosts = storagePoolHostDao.listByPoolId(storagePool.getId()); + if (!poolHosts.isEmpty()) { + hostId = poolHosts.get(0).getHostId(); + logger.debug("Selected host {} from storage pool {} for stopped VM {} volume creation", + hostId, storagePool.getUuid(), vm.getUuid()); + } } } @@ -1454,6 +1599,9 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati _snapshotDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); _snapshotDataStoreDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); } + + // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after migration + updateClvmLockHostAfterMigration(result.getVolume(), destPool, "migrated"); } return result.getVolume(); } catch (InterruptedException | ExecutionException e) { @@ -1479,6 +1627,10 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati logger.error("Volume [{}] migration failed due to [{}].", volToString, result.getResult()); return null; } + + // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after live migration + updateClvmLockHostAfterMigration(result.getVolume(), destPool, "live-migrated"); + return result.getVolume(); } catch (InterruptedException | ExecutionException e) { logger.error("Volume [{}] migration failed due to [{}].", volToString, e.getMessage()); @@ -1521,6 +1673,22 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati logger.error(msg); throw new CloudRuntimeException(msg); } + for (Map.Entry entry : volumeToPool.entrySet()) { + Volume volume = entry.getKey(); + StoragePool destPool = entry.getValue(); + StoragePoolVO srcPool = _storagePoolDao.findById(volume.getPoolId()); + if (srcPool != null && srcPool.getId() == destPool.getId() && + ClvmPoolManager.isClvmPoolType(srcPool.getPoolType())) { + if (!clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), + volume.getPath(), srcPool, srcHost.getId(), destHost.getId())) { + throw new CloudRuntimeException(String.format( + "Failed to transfer CLVM lock for volume [%s] to destination host [%s].", + volume.getUuid(), destHost.getId())); + } + } else { + updateClvmLockHostAfterMigration(volume, destPool, "vm-migrated"); + } + } } catch (InterruptedException | ExecutionException e) { logger.error("Failed to migrate VM [{}] along with its volumes due to [{}].", vm, e.getMessage()); logger.debug("Exception: ", e); @@ -1853,6 +2021,19 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati future = volService.createManagedStorageVolumeFromTemplateAsync(volume, destPool.getId(), templ, hostId); } else { + // For CLVM pools, set the destination host hint so volume is created on the correct host + // This avoids the need for shared mode activation and improves performance + StoragePoolVO poolVO = _storagePoolDao.findById(destPool.getId()); + if (poolVO != null && ClvmPoolManager.isClvmPoolType(poolVO.getPoolType())) { + Long hostId = vm.getVirtualMachine().getHostId(); + if (hostId != null) { + volume.setDestinationHostId(hostId); + clvmPoolManager.setClvmLockHostId(volume.getId(), hostId); + logger.info("CLVM pool detected during volume creation from template. Setting lock host {} for volume {} (persisted to DB) to route creation to correct host", + hostId, volume.getUuid()); + } + } + future = volService.createVolumeFromTemplateAsync(volume, destPool.getId(), templ); } } @@ -1976,13 +2157,18 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati throw new CloudRuntimeException(msg); } - // don't allow to start vm that doesn't have a root volume if (_volsDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT).isEmpty()) { throw new CloudRuntimeException(String.format("ROOT volume is missing, unable to prepare volumes for the VM [%s].", vm.getVirtualMachine())); } List vols = _volsDao.findUsableVolumesForInstance(vm.getId()); + VirtualMachine vmInstance = vm.getVirtualMachine(); + VMInstanceVO vmInstanceVO = vmInstanceDao.findById(vmInstance.getId()); + if (vmInstance.getState() == State.Starting && dest.getHost() != null) { + transferClvmLocksForVmStart(vols, dest.getHost().getId(), vmInstanceVO); + } + List tasks = getTasks(vols, dest.getStorageForDisks(), vm); Volume vol = null; PrimaryDataStore store; diff --git a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml index 49c668f50e8..8f93ae5b35a 100644 --- a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml +++ b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml @@ -44,6 +44,8 @@ value="#{storagePoolAllocatorsRegistry.registered}" /> + + (), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).getClvmLockHostId(Mockito.anyLong(), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.anyBoolean()); + } + + @Test + public void testTransferClvmLocksForVmStart_NullPoolId() throws Exception { + Long destHostId = 2L; + + VolumeVO volumeWithoutPool = Mockito.mock(VolumeVO.class); + Mockito.when(volumeWithoutPool.getPoolId()).thenReturn(null); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(volumeWithoutPool), destHostId, vmInstance); + + Mockito.verify(storagePoolDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_SetInitialLockHost() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.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); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).setClvmLockHostId(101L, destHostId); + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_MixedVolumes() throws Exception { + Long destHostId = 2L; + Long currentHostId = 1L; + Long clvmPoolId = 10L; + Long nfsPoolId = 20L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(clvmPoolId); + Mockito.when(clvmVolume.getUuid()).thenReturn("clvm-vol-uuid"); + Mockito.when(clvmVolume.getPath()).thenReturn("clvm-vol-path"); + + VolumeVO nfsVolume = Mockito.mock(VolumeVO.class); + Mockito.when(nfsVolume.getPoolId()).thenReturn(nfsPoolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO nfsPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true); + + Mockito.when(storagePoolDao.findById(clvmPoolId)).thenReturn(clvmPool); + Mockito.when(storagePoolDao.findById(nfsPoolId)).thenReturn(nfsPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume, nfsVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("clvm-vol-uuid"), Mockito.eq(101L), Mockito.eq("clvm-vol-path"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + } + + @Test(expected = CloudRuntimeException.class) + public void testTransferClvmLocksForVmStart_TransferFails() throws Throwable { + Long destHostId = 2L; + Long currentHostId = 1L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume.getUuid()).thenReturn("vol-uuid"); + Mockito.when(clvmVolume.getPath()).thenReturn("vol-path"); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(false); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + try { + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private void setField(Object target, String fieldName, Object value) throws Exception { + Field field = findField(target.getClass(), fieldName); + if (field == null) { + throw new NoSuchFieldException("Field " + fieldName + " not found in " + target.getClass()); + } + field.setAccessible(true); + field.set(target, value); + } + + private Field findField(Class clazz, String fieldName) { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + return null; + } + } diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index 8145158dfa4..95362f44b13 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -27,6 +27,7 @@ import java.util.Objects; import javax.inject.Inject; import com.cloud.agent.api.to.DiskTO; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Storage; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -75,6 +76,7 @@ import com.cloud.storage.ScopeType; import com.cloud.storage.Snapshot.Type; import com.cloud.storage.SnapshotVO; import com.cloud.storage.StorageManager; +import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StoragePool; import com.cloud.storage.VolumeVO; @@ -108,6 +110,8 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { StorageCacheManager cacheMgr; @Inject VolumeDataStoreDao volumeDataStoreDao; + @Inject + ClvmPoolManager clvmPoolManager; @Inject StorageManager storageManager; @@ -309,6 +313,8 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { ep = selector.select(srcData, volObj); } + updateLockHostForVolume(ep, volObj); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volObj.getTO()), _createVolumeFromSnapshotWait, VirtualMachineManager.ExecuteInSequence.value()); Answer answer = null; @@ -331,6 +337,29 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { } } + private void updateLockHostForVolume(EndPoint ep, DataObject volObj) { + if (ep == null || !(volObj instanceof VolumeInfo)) { + return; + } + VolumeInfo volumeInfo = (VolumeInfo) volObj; + StoragePool destPool = (StoragePool) volObj.getDataStore(); + if (destPool == null || !ClvmPoolManager.isClvmPoolType(destPool.getPoolType())) { + return; + } + Long hostId = ep.getId(); + Long existingHostId = clvmPoolManager.getClvmLockHostId( + volumeInfo.getId(), + volumeInfo.getUuid(), + volumeInfo.getPath(), + destPool, + true + ); + if (existingHostId == null) { + clvmPoolManager.setClvmLockHostId(volumeInfo.getId(), hostId); + logger.debug("Set lock host ID {} for CLVM volume {} being created from snapshot", hostId, volumeInfo.getId()); + } + } + protected Answer cloneVolume(DataObject template, DataObject volume) { CopyCommand cmd = new CopyCommand(template.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volume.getTO()), 0, VirtualMachineManager.ExecuteInSequence.value()); try { @@ -581,6 +610,9 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { volumeVo.setPoolId(destPool.getId()); volumeVo.setPoolType(destPool.getPoolType()); volumeVo.setLastPoolId(oldPoolId); + if (destPool.getPoolType() == StoragePoolType.CLVM) { + volumeVo.setFormat(ImageFormat.RAW); + } // For SMB, pool credentials are also stored in the uri query string. We trim the query string // part here to make sure the credentials do not get stored in the db unencrypted. String folder = destPool.getPath(); diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java index 947b4af8f69..867470dac04 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java @@ -144,12 +144,16 @@ public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMot } /** - * Configures a {@link MigrateDiskInfo} object configured for migrating a File System volume and calls rootImageProvisioning. + * Configures a {@link MigrateDiskInfo} object configured for migrating a File System volume. */ @Override protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath, String backingPath) { - return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, - MigrateCommand.MigrateDiskInfo.Source.FILE, destPath, backingPath); + return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + destPath, + backingPath); } /** @@ -158,6 +162,17 @@ public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMot */ @Override protected String generateDestPath(Host destHost, StoragePoolVO destStoragePool, VolumeInfo destVolumeInfo) { + if (destStoragePool.getPoolType() == StoragePoolType.CLVM || destStoragePool.getPoolType() == StoragePoolType.CLVM_NG) { + String vgName = destStoragePool.getPath(); + if (StringUtils.isBlank(vgName)) { + throw new CloudRuntimeException(String.format("CLVM/CLVM_NG destination pool [%s] has empty VG path", destStoragePool.getUuid())); + } + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + return String.format("/dev/%s/%s", vgName, destVolumeInfo.getUuid()); + } + return new File(destStoragePool.getPath(), destVolumeInfo.getUuid()).getAbsolutePath(); } @@ -285,6 +300,7 @@ public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMot } protected Boolean supportStoragePoolType(StoragePoolType storagePoolType) { - return super.supportStoragePoolType(storagePoolType, StoragePoolType.Filesystem); + return super.supportStoragePoolType(storagePoolType, StoragePoolType.Filesystem, + StoragePoolType.CLVM, StoragePoolType.CLVM_NG); } } diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java index bcade3a371c..2c791d05980 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java @@ -36,6 +36,8 @@ import com.cloud.agent.api.CheckVirtualMachineAnswer; import com.cloud.agent.api.CheckVirtualMachineCommand; import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.resource.ResourceManager; +import com.cloud.storage.clvm.ClvmPoolManager; +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -87,6 +89,7 @@ import com.cloud.agent.api.MigrateCommand; import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; import com.cloud.agent.api.ModifyTargetsAnswer; import com.cloud.agent.api.ModifyTargetsCommand; +import com.cloud.agent.api.PreMigrationCommand; import com.cloud.agent.api.PrepareForMigrationCommand; import com.cloud.agent.api.storage.CopyVolumeAnswer; import com.cloud.agent.api.storage.CopyVolumeCommand; @@ -206,6 +209,8 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { private VolumeDataFactory _volFactory; @Inject ResourceManager resourceManager; + @Inject + private ClvmPoolManager clvmPoolManager; @Override public StrategyPriority canHandle(DataObject srcData, DataObject destData) { @@ -2023,6 +2028,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { String errMsg = null; boolean success = false; Map srcVolumeInfoToDestVolumeInfo = new HashMap<>(); + List samePoolClvmVolumes = new ArrayList<>(); try { if (srcHost.getHypervisorType() != HypervisorType.KVM) { @@ -2052,6 +2058,13 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { continue; } + if (sourceStoragePool.getId() == destStoragePool.getId() && + ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + logger.info("Same-pool CLVM migration for volume [{}]: skipping data copy.", srcVolumeInfo.getUuid()); + samePoolClvmVolumes.add(srcVolumeInfo); + continue; + } + if (!shouldMigrateVolume(sourceStoragePool, destHost, destStoragePool)) { continue; } @@ -2071,6 +2084,13 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { setVolumeMigrationOptions(srcVolumeInfo, destVolumeInfo, vmTO, srcHost, destStoragePool, migrationType); + if (ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + destVolumeInfo.setDestinationHostId(destHost.getId()); + clvmPoolManager.setClvmLockHostId(destVolume.getId(), destHost.getId()); + logger.info("Set CLVM lock host {} for volume {} during migration to ensure creation on destination host", + destHost.getId(), destVolumeInfo.getUuid()); + } + // create a volume on the destination storage destDataStore.getDriver().createAsync(destDataStore, destVolumeInfo, null); @@ -2096,7 +2116,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { MigrateCommand.MigrateDiskInfo migrateDiskInfo; - boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; + boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem, StoragePoolType.CLVM, StoragePoolType.CLVM_NG) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; if (isNonManagedToNfs) { migrateDiskInfo = new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, @@ -2106,9 +2126,12 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } else { String backingPath = generateBackingPath(destStoragePool, destVolumeInfo); migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath, backingPath); + migrateDiskInfo = updateMigrateDiskInfoForBlockDevice(migrateDiskInfo, destStoragePool); migrateDiskInfo.setSourceDiskOnStorageFileSystem(isStoragePoolTypeOfFile(sourceStoragePool)); migrateDiskInfoList.add(migrateDiskInfo); } + migrateDiskInfo.setSourcePoolType(sourceStoragePool.getPoolType()); + migrateDiskInfo.setDestPoolType(destVolumeInfo.getStoragePoolType()); prepareDiskWithSecretConsumerDetail(vmTO, srcVolumeInfo, destVolumeInfo.getPath()); migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo); @@ -2116,6 +2139,8 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo); } + prepareDisksForMigrationForClvm(vmTO, volumeDataStoreMap, srcHost); + PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); Answer pfma; @@ -2132,6 +2157,25 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { throw new AgentUnavailableException("Operation timed out", destHost.getId()); } + for (VolumeInfo vol : samePoolClvmVolumes) { + StoragePoolVO samePoolClvmPool = _storagePoolDao.findById(vol.getPoolId()); + String vgName = samePoolClvmPool.getPath(); + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + String lvPath = String.format("/dev/%s/%s", vgName, vol.getPath()); + logger.info("Activating CLVM volume [{}] in shared mode on dest host [{}] for same-pool migration.", + vol.getUuid(), destHost.getId()); + Answer activateAnswer = agentManager.send(destHost.getId(), + new ClvmLockTransferCommand(ClvmLockTransferCommand.Operation.ACTIVATE_SHARED, lvPath, vol.getUuid())); + if (activateAnswer == null || !activateAnswer.getResult()) { + throw new CloudRuntimeException(String.format( + "Failed to activate CLVM volume [%s] in shared mode on dest host [%s]: %s", + vol.getUuid(), destHost.getId(), + activateAnswer != null ? activateAnswer.getDetails() : "null answer")); + } + } + VMInstanceVO vm = _vmDao.findById(vmTO.getId()); boolean isWindows = _guestOsCategoryDao.findById(_guestOsDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); @@ -2141,6 +2185,9 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList); migrateCommand.setMigrateStorageManaged(managedStorageDestination); migrateCommand.setMigrateNonSharedInc(migrateNonSharedInc); + boolean hasClvmCrossPoolVolume = migrateStorage.values().stream() + .anyMatch(info -> ClvmPoolManager.isClvmPoolType(info.getSourcePoolType())); + migrateCommand.setClvmCrossPoolMigration(hasClvmCrossPoolVolume); Integer newVmCpuShares = ((PrepareForMigrationAnswer) pfma).getNewVmCpuShares(); if (newVmCpuShares != null) { @@ -2171,7 +2218,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } } - handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost); + handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, srcHost, destHost); if (!success) { if (migrateAnswer == null) { @@ -2211,10 +2258,43 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } } + private void prepareDisksForMigrationForClvm(VirtualMachineTO vmTO, Map volumeDataStoreMap, Host srcHost) { + // For CLVM/CLVM_NG source pools, convert volumes from exclusive to shared mode + // on the source host BEFORE PrepareForMigrationCommand on the destination. + boolean hasClvmSource = volumeDataStoreMap.keySet().stream() + .map(v -> _storagePoolDao.findById(v.getPoolId())) + .anyMatch(p -> p != null && (p.getPoolType() == StoragePoolType.CLVM || p.getPoolType() == StoragePoolType.CLVM_NG)); + if (hasClvmSource && srcHost.getHypervisorType() == HypervisorType.KVM) { + logger.info("CLVM/CLVM_NG source pool detected for VM [{}], sending PreMigrationCommand to source host [{}] to convert volumes to shared mode.", vmTO.getName(), srcHost.getId()); + PreMigrationCommand preMigCmd = new PreMigrationCommand(vmTO, vmTO.getName()); + try { + Answer preMigAnswer = agentManager.send(srcHost.getId(), preMigCmd); + if (preMigAnswer == null || !preMigAnswer.getResult()) { + String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned"; + logger.warn("PreMigrationCommand failed for CLVM/CLVM_NG VM [{}] on source host [{}]: {}. Migration will continue but may fail if volumes are exclusively locked.", vmTO.getName(), srcHost.getId(), details); + } else { + logger.info("Successfully converted CLVM/CLVM_NG volumes to shared mode on source host [{}] for VM [{}].", srcHost.getId(), vmTO.getName()); + } + } catch (Exception e) { + logger.warn("Failed to send PreMigrationCommand to source host [{}] for VM [{}]: {}. Migration will continue but may fail if volumes are exclusively locked.", srcHost.getId(), vmTO.getName(), e.getMessage()); + } + } else if (hasClvmSource) { + logger.debug("Skipping PreMigrationCommand for non-KVM hypervisor type: {} on host [{}]", srcHost.getHypervisorType(), srcHost.getId()); + } + } + private MigrationOptions.Type decideMigrationTypeAndCopyTemplateIfNeeded(Host destHost, VMInstanceVO vmInstance, VolumeInfo srcVolumeInfo, StoragePoolVO sourceStoragePool, StoragePoolVO destStoragePool, DataStore destDataStore) { VMTemplateVO vmTemplate = _vmTemplateDao.findById(vmInstance.getTemplateId()); String srcVolumeBackingFile = getVolumeBackingFile(srcVolumeInfo); + + // Check if source is CLVM/CLVM_NG (block device storage) + // LinkedClone (VIR_MIGRATE_NON_SHARED_INC) only works for file → file migrations + // For block device sources, use FullClone (VIR_MIGRATE_NON_SHARED_DISK) + boolean sourceIsBlockDevice = sourceStoragePool.getPoolType() == StoragePoolType.CLVM || + sourceStoragePool.getPoolType() == StoragePoolType.CLVM_NG; + if (StringUtils.isNotBlank(srcVolumeBackingFile) && supportStoragePoolType(destStoragePool.getPoolType(), StoragePoolType.Filesystem) && + !sourceIsBlockDevice && srcVolumeInfo.getTemplateId() != null && Objects.nonNull(vmTemplate) && !Arrays.asList(KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME, VM_IMPORT_DEFAULT_TEMPLATE_NAME).contains(vmTemplate.getName())) { @@ -2222,8 +2302,12 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { copyTemplateToTargetFilesystemStorageIfNeeded(srcVolumeInfo, sourceStoragePool, destDataStore, destStoragePool, destHost); return MigrationOptions.Type.LinkedClone; } - logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a " + - "template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId())); + + if (sourceIsBlockDevice) { + logger.debug(String.format("Source storage pool [%s] is block device (CLVM/CLVM_NG). Using FullClone migration for volume [%s] to target storage pool [%s]. Template copy skipped as entire volume will be copied.", sourceStoragePool.getId(), srcVolumeInfo.getId(), destStoragePool.getId())); + } else { + logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId())); + } return MigrationOptions.Type.FullClone; } @@ -2289,6 +2373,39 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { MigrateCommand.MigrateDiskInfo.Source.DEV, destPath, backingPath); } + /** + * UpdatesMigrateDiskInfo for CLVM/CLVM_NG block devices by returning a new instance with corrected disk type, driver type, and source. + * For CLVM/CLVM_NG destinations, returns a new MigrateDiskInfo with BLOCK disk type, DEV source, and appropriate driver type (QCOW2 for CLVM_NG, RAW for CLVM). + * For other storage types, returns the original MigrateDiskInfo unchanged. + * + * @param migrateDiskInfo The original MigrateDiskInfo object + * @param destStoragePool The destination storage pool + * @return A new MigrateDiskInfo with updated values for CLVM/CLVM_NG, or the original for other storage types + */ + protected MigrateCommand.MigrateDiskInfo updateMigrateDiskInfoForBlockDevice(MigrateCommand.MigrateDiskInfo migrateDiskInfo, + StoragePoolVO destStoragePool) { + if (ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + + MigrateCommand.MigrateDiskInfo.DriverType driverType = + (destStoragePool.getPoolType() == StoragePoolType.CLVM_NG) ? + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2 : + MigrateCommand.MigrateDiskInfo.DriverType.RAW; + + logger.debug("Updating MigrateDiskInfo for {} destination: setting BLOCK disk type, DEV source, and {} driver type", + destStoragePool.getPoolType(), driverType); + + return new MigrateCommand.MigrateDiskInfo( + migrateDiskInfo.getSerialNumber(), + MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, + driverType, + MigrateCommand.MigrateDiskInfo.Source.DEV, + migrateDiskInfo.getSourceText(), + migrateDiskInfo.getBackingStoreText()); + } + + return migrateDiskInfo; + } + /** * Sets the volume path as the iScsi name in case of a configured iScsi. */ @@ -2320,7 +2437,26 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { return null; } - private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host destHost) { + private void sendClvmLockCommand(long hostId, StoragePoolVO pool, VolumeInfo volumeInfo, + ClvmLockTransferCommand.Operation operation) { + String vgName = pool.getPath(); + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + String lvPath = String.format("/dev/%s/%s", vgName, volumeInfo.getPath()); + try { + Answer answer = agentManager.send(hostId, + new ClvmLockTransferCommand(operation, lvPath, volumeInfo.getUuid())); + if (answer == null || !answer.getResult()) { + String details = answer != null ? answer.getDetails() : "null answer"; + logger.warn("CLVM lock command [{}] failed for LV [{}] on host [{}]: {}", operation, lvPath, hostId, details); + } + } catch (AgentUnavailableException | OperationTimedoutException e) { + logger.warn("Exception sending CLVM lock command [{}] for LV [{}] on host [{}]: {}", operation, lvPath, hostId, e.getMessage()); + } + } + + private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host srcHost, Host destHost) { if (!success) { try { PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); @@ -2339,6 +2475,17 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { catch (Exception e) { logger.debug("Failed to disconnect one or more (original) dest volumes", e); } + + if (srcHost != null && srcHost.getHypervisorType() == HypervisorType.KVM) { + for (VolumeInfo srcVolumeInfo : srcVolumeInfoToDestVolumeInfo.keySet()) { + StoragePoolVO srcPool = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + if (srcPool == null || !ClvmPoolManager.isClvmPoolType(srcPool.getPoolType())) { + continue; + } + sendClvmLockCommand(srcHost.getId(), srcPool, srcVolumeInfo, + ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE); + } + } } for (Map.Entry entry : srcVolumeInfoToDestVolumeInfo.entrySet()) { @@ -2349,12 +2496,13 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { if (success) { VolumeVO volumeVO = _volumeDao.findById(destVolumeInfo.getId()); - volumeVO.setFormat(ImageFormat.QCOW2); + StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); + volumeVO.setFormat(destPoolVO != null && destPoolVO.getPoolType() == StoragePoolType.CLVM + ? ImageFormat.RAW : ImageFormat.QCOW2); volumeVO.setLastId(srcVolumeInfo.getId()); if (Objects.equals(srcVolumeInfo.getDiskOfferingId(), destVolumeInfo.getDiskOfferingId())) { - StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); - StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); if (srcPoolVO != null && destPoolVO != null && ((srcPoolVO.isShared() && destPoolVO.isLocal()) || (srcPoolVO.isLocal() && destPoolVO.isShared()))) { Long offeringId = getSuitableDiskOfferingForVolumeOnPool(volumeVO, destPoolVO); @@ -2365,6 +2513,12 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy { } _volumeDao.update(volumeVO.getId(), volumeVO); + if (destPoolVO != null && ClvmPoolManager.isClvmPoolType(destPoolVO.getPoolType()) + && (srcPoolVO == null || srcPoolVO.getId() != destPoolVO.getId())) { + sendClvmLockCommand(destHost.getId(), destPoolVO, destVolumeInfo, + ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE); + clvmPoolManager.setClvmLockHostId(destVolumeInfo.getId(), destHost.getId()); + } _volumeService.copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(Event.OperationSucceeded, null, srcVolumeInfo, destVolumeInfo, false); diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java index e167cc0a965..e84163656b1 100755 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java @@ -28,14 +28,18 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.never; import static org.mockito.Mockito.any; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -95,6 +99,14 @@ public class AncientDataMotionStrategyTest { f.set(configKey, value); } + private ClvmPoolManager injectMockedClvmPoolManager() throws Exception { + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Field clvmPoolManagerField = AncientDataMotionStrategy.class.getDeclaredField("clvmPoolManager"); + clvmPoolManagerField.setAccessible(true); + clvmPoolManagerField.set(strategy, clvmPoolManager); + return clvmPoolManager; + } + @Test public void testAddFullCloneFlagOnVMwareDest(){ strategy.addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(dataTO); @@ -288,4 +300,185 @@ public class AncientDataMotionStrategyTest { canBypassSecondaryStorage = (boolean) method.invoke(strategy, destVolumeInfo, srcVolumeInfo); Assert.assertTrue(canBypassSecondaryStorage); } + + @Test + public void testUpdateLockHostForVolume_CLVMPool_SetsLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 123L; + Long volumeId = 456L; + String volumeUuid = "test-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + 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(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager).setClvmLockHostId(volumeId, hostId); + } + + @Test + public void testUpdateLockHostForVolume_CLVM_NG_Pool_SetsLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 789L; + Long volumeId = 101L; + String volumeUuid = "test-clvm-ng-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + 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(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null); + + try { + method.invoke(strategy, endPoint, volumeInfo); + } catch (InvocationTargetException e) { + e.getCause().printStackTrace(); + throw e; + } + + Mockito.verify(clvmPoolManager).setClvmLockHostId(volumeId, hostId); + } + + @Test + public void testUpdateLockHostForVolume_NonCLVMPool_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + // Create mock that implements both DataStore and StoragePool interfaces + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_ExistingLockHost_DoesNotOverwrite() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 555L; + Long existingHostId = 666L; + Long volumeId = 777L; + String volumeUuid = "existing-lock-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + 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(clvmPoolManager.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(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager).getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true)); + } + + @Test + public void testUpdateLockHostForVolume_NullEndPoint_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, null, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_NonVolumeDataObject_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + SnapshotInfo snapshotInfo = Mockito.mock(SnapshotInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, endPoint, snapshotInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_NullPool_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } } diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java index 808c319b40f..6f0776b27c8 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java @@ -168,6 +168,8 @@ public class KvmNonManagedStorageSystemDataMotionTest { supportedTypes.add(StoragePoolType.Filesystem); supportedTypes.add(StoragePoolType.NetworkFilesystem); supportedTypes.add(StoragePoolType.SharedMountPoint); + supportedTypes.add(StoragePoolType.CLVM); + supportedTypes.add(StoragePoolType.CLVM_NG); return supportedTypes.contains(storagePoolType); } @@ -505,6 +507,8 @@ public class KvmNonManagedStorageSystemDataMotionTest { supportedTypes.add(StoragePoolType.Filesystem); supportedTypes.add(StoragePoolType.NetworkFilesystem); supportedTypes.add(StoragePoolType.SharedMountPoint); + supportedTypes.add(StoragePoolType.CLVM); + supportedTypes.add(StoragePoolType.CLVM_NG); for (StoragePoolType poolType : StoragePoolType.values()) { boolean isSupported = kvmNonManagedStorageDataMotionStrategy.supportStoragePoolType(poolType); diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java index 45357fa64b2..fc51eeba7b6 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java @@ -369,4 +369,286 @@ public class StorageSystemDataMotionStrategyTest { assertFalse(strategy.isStoragePoolTypeInList(StoragePoolType.SharedMountPoint, listTypes)); } + + /** + * Test updateMigrateDiskInfoForBlockDevice with CLVM destination pool + * Should set driver type to RAW for CLVM + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_ClvmDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial123", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + null + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.CLVM); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, updatedDiskInfo.getSource()); + Assert.assertEquals("serial123", updatedDiskInfo.getSerialNumber()); + Assert.assertEquals("/source/path", updatedDiskInfo.getSourceText()); + } + + /** + * Test updateMigrateDiskInfoForBlockDevice with CLVM_NG destination pool + * Should set driver type to QCOW2 for CLVM_NG + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_ClvmNgDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial456", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.RAW, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + "/backing/path" + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.CLVM_NG); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, updatedDiskInfo.getSource()); + Assert.assertEquals("serial456", updatedDiskInfo.getSerialNumber()); + Assert.assertEquals("/source/path", updatedDiskInfo.getSourceText()); + Assert.assertEquals("/backing/path", updatedDiskInfo.getBackingStoreText()); + } + + /** + * Test updateMigrateDiskInfoForBlockDevice with non-CLVM destination pool + * Should return original DiskInfo unchanged + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_NonClvmDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial789", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + null + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.NetworkFilesystem); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertSame(originalDiskInfo, updatedDiskInfo); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.FILE, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.FILE, updatedDiskInfo.getSource()); + } + + /** + * Test supportStoragePoolType with CLVM and CLVM_NG types + */ + @Test + public void testSupportStoragePoolType_ClvmTypes() { + assertTrue(strategy.supportStoragePoolType(StoragePoolType.CLVM, StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + assertTrue(strategy.supportStoragePoolType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + + assertFalse(strategy.supportStoragePoolType(StoragePoolType.CLVM)); + assertFalse(strategy.supportStoragePoolType(StoragePoolType.CLVM_NG)); + } + + /** + * Test configureMigrateDiskInfo with CLVM destination + */ + @Test + public void testConfigureMigrateDiskInfo_ForClvm() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("/dev/vg/volume-path").when(srcVolumeInfo).getPath(); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "/dev/vg/dest-path", null); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("/dev/vg/dest-path", migrateDiskInfo.getSourceText()); + Assert.assertEquals("/dev/vg/volume-path", migrateDiskInfo.getSerialNumber()); + } + + /** + * Test configureMigrateDiskInfo with CLVM_NG destination and backing file + */ + @Test + public void testConfigureMigrateDiskInfo_ForClvmNgWithBacking() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("/dev/vg/volume-path").when(srcVolumeInfo).getPath(); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "/dev/vg/dest-path", "/dev/vg/backing-template"); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("/dev/vg/dest-path", migrateDiskInfo.getSourceText()); + Assert.assertEquals("/dev/vg/backing-template", migrateDiskInfo.getBackingStoreText()); + Assert.assertEquals("/dev/vg/volume-path", migrateDiskInfo.getSerialNumber()); + } + + /** + * Test isStoragePoolTypeInList with CLVM types + */ + @Test + public void testIsStoragePoolTypeInList_WithClvmTypes() { + StoragePoolType[] clvmTypes = new StoragePoolType[] { + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.Filesystem + }; + + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.CLVM, clvmTypes)); + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.CLVM_NG, clvmTypes)); + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.Filesystem, clvmTypes)); + assertFalse(strategy.isStoragePoolTypeInList(StoragePoolType.NetworkFilesystem, clvmTypes)); + } + + /** + * Test supportStoragePoolType with mixed CLVM and NFS types + */ + @Test + public void testSupportStoragePoolType_MixedClvmAndNfs() { + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.CLVM, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.NetworkFilesystem + )); + + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.CLVM_NG, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.NetworkFilesystem + )); + + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.NetworkFilesystem, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG + )); + } + + /** + * Test internalCanHandle with CLVM source and managed destination + */ + @Test + public void testInternalCanHandle_ClvmSourceManagedDestination() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with CLVM_NG source and managed destination + */ + @Test + public void testInternalCanHandle_ClvmNgSourceManagedDestination() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with both CLVM source and CLVM_NG destination + */ + @Test + public void testInternalCanHandle_ClvmToClvmNg() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(destPool).getPoolType(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with CLVM_NG to CLVM migration + */ + @Test + public void testInternalCanHandle_ClvmNgToClvm() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(destPool).getPoolType(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java index 88f479c0904..d85e862bfb6 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java @@ -60,6 +60,7 @@ import com.cloud.event.UsageEventUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.CreateSnapshotPayload; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Snapshot; @@ -643,6 +644,10 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase { return StrategyPriority.DEFAULT; } + if (isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)) { + return StrategyPriority.DEFAULT; + } + return StrategyPriority.CANT_HANDLE; } if (zoneId != null && SnapshotOperation.DELETE.equals(op)) { @@ -691,4 +696,32 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase { dataStoreMgr.getStoreZoneId(s.getDataStoreId(), s.getRole()), volumeVO.getDataCenterId())); } + /** + * Checks if a CLVM volume snapshot is stored on secondary storage in the same zone. + * CLVM snapshots are backed up to secondary storage and removed from primary storage. + */ + protected boolean isSnapshotStoredOnSecondaryForCLVMVolume(Snapshot snapshot, VolumeVO volumeVO) { + if (volumeVO == null) { + return false; + } + + Long poolId = volumeVO.getPoolId(); + if (poolId == null) { + return false; + } + + StoragePool pool = (StoragePool) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return false; + } + + List snapshotStores = snapshotStoreDao.listReadyBySnapshot(snapshot.getId(), DataStoreRole.Image); + if (CollectionUtils.isEmpty(snapshotStores)) { + return false; + } + + return snapshotStores.stream().anyMatch(s -> Objects.equals( + dataStoreMgr.getStoreZoneId(s.getDataStoreId(), s.getRole()), volumeVO.getDataCenterId())); + } + } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java index b71d6cf3afa..665c3a4659c 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java @@ -27,6 +27,7 @@ import javax.naming.ConfigurationException; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.Snapshot; +import com.cloud.storage.Storage; import com.cloud.storage.dao.SnapshotDao; import com.cloud.vm.snapshot.VMSnapshotDetailsVO; import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; @@ -468,6 +469,13 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot @Override public StrategyPriority canHandle(VMSnapshot vmSnapshot) { + UserVmVO vm = userVmDao.findById(vmSnapshot.getVmId()); + String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm); + + if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + return StrategyPriority.DEFAULT; } @@ -493,10 +501,31 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot return vmSnapshotDao.remove(vmSnapshot.getId()); } + protected boolean isRunningVMVolumeOnCLVMStorage(UserVmVO vm, String cantHandleLog) { + Long vmId = vm.getId(); + if (State.Running.equals(vm.getState())) { + List volumes = volumeDao.findByInstance(vmId); + for (VolumeVO volume : volumes) { + StoragePool pool = primaryDataStoreDao.findById(volume.getPoolId()); + if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + logger.warn("Rejecting VM snapshot request: {} - VM is running on CLVM storage (pool: {}, poolType: CLVM)", + cantHandleLog, pool.getName()); + return true; + } + } + } + return false; + } + @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { UserVmVO vm = userVmDao.findById(vmId); String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm); + + if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + if (State.Running.equals(vm.getState()) && !snapshotMemory) { logger.debug("{} as it is running and its memory will not be affected.", cantHandleLog, vm); return StrategyPriority.CANT_HANDLE; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java index 31b13fc279e..4ae6e26fbd9 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java @@ -345,6 +345,13 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy { } } + Long vmId = vmSnapshot.getVmId(); + UserVmVO vm = userVmDao.findById(vmId); + String cantHandleLog = String.format("Storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm); + if (vm != null && isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + if ( SnapshotManager.VmStorageSnapshotKvm.value() && userVm.getHypervisorType() == Hypervisor.HypervisorType.KVM && vmSnapshot.getType() == VMSnapshot.Type.Disk) { return StrategyPriority.HYPERVISOR; diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java index 41bfaa6f0c7..c27d4e13fa4 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.StoragePool; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; @@ -322,4 +323,236 @@ public class DefaultSnapshotStrategyTest { prepareMocksForIsSnapshotStoredOnSameZoneStoreForQCOW2VolumeTest(100L); Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSameZoneStoreForQCOW2Volume(snapshot, volumeVO)); } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NullVolume() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, null)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NullPoolId() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NullPool() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NonCLVMPool() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_RBDPool() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolNoSnapshotStores() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)).thenReturn(new ArrayList<>()); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolSnapshotInDifferentZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore2.getDataStoreId()).thenReturn(202L); + Mockito.when(snapshotStore2.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(111L); + Mockito.when(dataStoreManager.getStoreZoneId(202L, DataStoreRole.Image)).thenReturn(112L); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolSnapshotInSameZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolMultipleSnapshotsOneMatches() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore2.getDataStoreId()).thenReturn(202L); + Mockito.when(snapshotStore2.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore3 = Mockito.mock(SnapshotDataStoreVO.class); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2, snapshotStore3)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(111L); + Mockito.when(dataStoreManager.getStoreZoneId(202L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolNullZoneIds() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolVolumeNullDataCenter() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(1L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolMultipleSnapshotsAllInSameZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } } diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java index da377f96ec3..365ba3d4eb3 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -39,6 +40,10 @@ import com.cloud.storage.Storage; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshot; @RunWith(MockitoJUnitRunner.class) public class DefaultVMSnapshotStrategyTest { @@ -46,6 +51,8 @@ public class DefaultVMSnapshotStrategyTest { VolumeDao volumeDao; @Mock PrimaryDataStoreDao primaryDataStoreDao; + @Mock + UserVmDao userVmDao; @Spy @InjectMocks @@ -85,7 +92,7 @@ public class DefaultVMSnapshotStrategyTest { Mockito.when(vol2.getChainInfo()).thenReturn(newVolChain); Mockito.when(vol2.getSize()).thenReturn(vmSnapshotChainSize); Mockito.when(vol2.getId()).thenReturn(volumeId); - VolumeVO volumeVO = new VolumeVO("name", 0l, 0l, 0l, 0l, 0l, "folder", "path", Storage.ProvisioningType.THIN, 0l, Volume.Type.ROOT); + VolumeVO volumeVO = new VolumeVO("name", 0L, 0L, 0L, 0L, 0L, "folder", "path", Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); volumeVO.setPoolId(oldPoolId); volumeVO.setChainInfo(oldVolChain); volumeVO.setPath(oldVolPath); @@ -103,4 +110,110 @@ public class DefaultVMSnapshotStrategyTest { Assert.assertEquals(vmSnapshotChainSize, persistedVolume.getVmSnapshotChainSize()); Assert.assertEquals(newVolChain, persistedVolume.getChainInfo()); } + + @Test + public void testCanHandleRunningVMOnClvmStorageCantHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnClvm = createVolume(vmId, 1L); + List volumes = List.of(volumeOnClvm); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO clvmPool = createStoragePool("clvm-pool", Storage.StoragePoolType.CLVM); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(clvmPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return CANT_HANDLE for running VM on CLVM storage", + StrategyPriority.CANT_HANDLE, result); + } + + @Test + public void testCanHandleStoppedVMOnClvmStorageCanHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Stopped); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + Assert.assertEquals("Should return DEFAULT for stopped VM on CLVM storage", + StrategyPriority.DEFAULT, result); + } + + @Test + public void testCanHandleRunningVMOnNfsStorageCanHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnNfs = createVolume(vmId, 1L); + List volumes = List.of(volumeOnNfs); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO nfsPool = createStoragePool("nfs-pool", Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(nfsPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return DEFAULT for running VM on NFS storage", + StrategyPriority.DEFAULT, result); + } + + @Test + public void testCanHandleRunningVMWithMixedStorageClvmAndNfsCantHandle() { + // Arrange - VM has volumes on both CLVM and NFS + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnClvm = createVolume(vmId, 1L); + VolumeVO volumeOnNfs = createVolume(vmId, 2L); + List volumes = List.of(volumeOnClvm, volumeOnNfs); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO clvmPool = createStoragePool("clvm-pool", Storage.StoragePoolType.CLVM); + StoragePoolVO nfsPool = createStoragePool("nfs-pool", Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(clvmPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return CANT_HANDLE if any volume is on CLVM storage for running VM", + StrategyPriority.CANT_HANDLE, result); + } + + private VolumeVO createVolume(Long vmId, Long poolId) { + VolumeVO volume = new VolumeVO("volume", 0L, 0L, 0L, 0L, 0L, + "folder", "path", Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); + volume.setInstanceId(vmId); + volume.setPoolId(poolId); + return volume; + } + + private StoragePoolVO createStoragePool(String name, Storage.StoragePoolType poolType) { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Mockito.when(pool.getName()).thenReturn(name); + Mockito.when(pool.getPoolType()).thenReturn(poolType); + return pool; + } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java index 061d18dc376..7eee32b9f1b 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java @@ -32,8 +32,12 @@ import javax.inject.Inject; import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; +import com.cloud.storage.Volume; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.user.Account; import com.cloud.utils.Pair; +import com.cloud.utils.db.QueryBuilder; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -46,6 +50,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.LocalHostEndpoint; import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import org.springframework.stereotype.Component; @@ -59,8 +64,8 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage.TemplateType; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import com.cloud.utils.db.DB; -import com.cloud.utils.db.QueryBuilder; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; @@ -75,6 +80,12 @@ public class DefaultEndPointSelector implements EndPointSelector { private HostDao hostDao; @Inject private DedicatedResourceDao dedicatedResourceDao; + @Inject + private PrimaryDataStoreDao _storagePoolDao; + @Inject + private VolumeDetailsDao _volDetailsDao; + @Inject + private ClvmPoolManager clvmPoolManager; private static final String VOL_ENCRYPT_COLUMN_NAME = "volume_encryption_support"; private final String findOneHostOnPrimaryStorage = "select t.id from " @@ -264,6 +275,27 @@ public class DefaultEndPointSelector implements EndPointSelector { @Override public EndPoint select(DataObject srcData, DataObject destData, boolean volumeEncryptionSupportRequired) { + if (destData instanceof VolumeInfo) { + EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) destData, "template-to-volume copy"); + if (clvmEndpoint != null) { + return clvmEndpoint; + } + } + + // 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()); + EndPoint clvmEp = tryRouteToClvmLockHolder(srcVolume, pool, "copy operation"); + if (clvmEp != null) { + return clvmEp; + } + } + } + + // Default behavior for non-CLVM or when no destination host is set DataStore srcStore = srcData.getDataStore(); DataStore destStore = destData.getDataStore(); if (moveBetweenPrimaryImage(srcStore, destStore)) { @@ -305,7 +337,6 @@ public class DefaultEndPointSelector implements EndPointSelector { @Override public EndPoint select(DataObject srcData, DataObject destData, StorageAction action, boolean encryptionRequired) { - logger.error("IR24 select BACKUPSNAPSHOT from primary to secondary {} dest={}", srcData, destData); if (action == StorageAction.BACKUPSNAPSHOT && srcData.getDataStore().getRole() == DataStoreRole.Primary) { SnapshotInfo srcSnapshot = (SnapshotInfo)srcData; VolumeInfo volumeInfo = srcSnapshot.getBaseVolume(); @@ -314,6 +345,17 @@ public class DefaultEndPointSelector implements EndPointSelector { if (vm != null && vm.getState() == VirtualMachine.State.Running) { return getEndPointFromHostId(vm.getHostId()); } + // For CLVM pools, the snapshot LVM device only exists on the lock-holder host. + // Route the backup CopyCommand to that same host regardless of VM state. + DataStore srcStore = volumeInfo.getDataStore(); + if (srcStore != null && srcStore.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(srcStore.getId()); + logger.debug("Checking if CLVM store and lock-holder routing applicable for snapshot {}", srcSnapshot.getUuid()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volumeInfo, pool, "snapshot backup"); + if (clvmEp != null) { + return clvmEp; + } + } } if (srcSnapshot.getHypervisorType() == Hypervisor.HypervisorType.VMware) { if (vm != null) { @@ -388,18 +430,103 @@ public class DefaultEndPointSelector implements EndPointSelector { return sc.list(); } + /** + * Selects endpoint for CLVM volumes with destination host hint. + * This ensures volumes are created on the correct host with exclusive locks. + * + * @param volume The volume to check for CLVM routing + * @param operation Description of the operation (for logging) + * @return EndPoint for the destination host if CLVM routing applies, null otherwise + */ + private EndPoint selectClvmEndpointIfApplicable(VolumeInfo volume, String operation) { + DataStore store = volume.getDataStore(); + + if (store.getRole() != DataStoreRole.Primary) { + return null; + } + + + // Check if this is a CLVM pool + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return null; + } + if (Volume.State.Allocated == volume.getState()) { + // Check if destination host hint is set + Long destHostId = volume.getDestinationHostId(); + if (destHostId == null) { + return null; + } + + logger.info("CLVM {}: routing volume {} to destination host {} for optimal exclusive lock placement", + operation, volume.getUuid(), destHostId); + + EndPoint ep = getEndPointFromHostId(destHostId); + if (ep != null) { + return ep; + } + } + + Long lockHostId = getClvmLockHostId(volume); + if (lockHostId == null) { + return null; + } + logger.info("CLVM {}: routing existing volume {} to live lock-holder host {}", + operation, volume.getUuid(), lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for lock host {}, falling back to default selection", lockHostId); + return null; + } + @Override public EndPoint select(DataObject object, boolean encryptionSupportRequired) { DataStore store = object.getDataStore(); + + // This ensures volumes are created on the correct host with exclusive locks + String operation = ""; + if (DataStoreRole.Primary == store.getRole()) { + VolumeInfo volume = null; + if (object instanceof VolumeInfo) { + volume = (VolumeInfo) object; + operation = "volume creation"; + } else if (object instanceof SnapshotInfo) { + volume = ((SnapshotInfo) object).getBaseVolume(); + operation = "snapshot creation"; + } + + if (volume != null) { + EndPoint clvmEndpoint = selectClvmEndpointIfApplicable(volume, operation); + if (clvmEndpoint != null) { + return clvmEndpoint; + } + } + } + + // Default behavior for non-CLVM or when no destination host is set if (store.getRole() == DataStoreRole.Primary) { return findEndPointInScope(store.getScope(), findOneHostOnPrimaryStorage, store.getId(), encryptionSupportRequired); } throw new CloudRuntimeException(String.format("Storage role %s doesn't support encryption", store.getRole())); } + @Override public EndPoint select(DataObject object) { DataStore store = object.getDataStore(); + + // For CLVM volumes, check if there's a lock host ID to route to + if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) { + VolumeInfo volume = (VolumeInfo) object; + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volume, pool, "operation"); + if (clvmEp != null) { + return clvmEp; + } + } + EndPoint ep = select(store); if (ep != null) { return ep; @@ -493,6 +620,19 @@ public class DefaultEndPointSelector implements EndPointSelector { } case DELETEVOLUME: { VolumeInfo volume = (VolumeInfo) object; + + // For CLVM volumes, route to the host holding the exclusive lock + if (volume.getHypervisorType() == Hypervisor.HypervisorType.KVM) { + DataStore store = volume.getDataStore(); + if (store.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volume, pool, "deletion"); + if (clvmEp != null) { + return clvmEp; + } + } + } + if (volume.getHypervisorType() == Hypervisor.HypervisorType.VMware) { VirtualMachine vm = volume.getAttachedVM(); if (vm != null) { @@ -540,6 +680,14 @@ public class DefaultEndPointSelector implements EndPointSelector { if (vm.getState() == VirtualMachine.State.Running) { return getEndPointFromHostId(vm.getHostId()); + } else if (vm.getState() == VirtualMachine.State.Stopped) { + StoragePoolVO pool = _storagePoolDao.findById(volumeInfo.getPoolId()); + if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + EndPoint ep = getApplicableEndpointForClvm(snapshotInfo, volumeInfo); + if (ep != null) { + return ep; + } + } } Long hostId = vm.getLastHostId(); @@ -552,6 +700,20 @@ public class DefaultEndPointSelector implements EndPointSelector { return select(snapshotInfo, encryptionRequired); } + private EndPoint getApplicableEndpointForClvm(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo) { + Long lockHostId = getClvmLockHostId(volumeInfo); + if (lockHostId != null) { + logger.debug("CLVM snapshot operation: routing snapshot [{}] to lock-holder host [{}]", + snapshotInfo.getUuid(), lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for CLVM lock host {}, falling back", lockHostId); + } + return null; + } + @Override public EndPoint select(Scope scope, Long storeId) { return findEndPointInScope(scope, findOneHostOnPrimaryStorage, storeId); @@ -589,4 +751,46 @@ public class DefaultEndPointSelector implements EndPointSelector { } return endPoints; } + + protected EndPoint tryRouteToClvmLockHolder(VolumeInfo volume, StoragePoolVO pool, String operation) { + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return null; + } + Long lockHostId = getClvmLockHostId(volume); + if (lockHostId == null) { + logger.debug("No CLVM lock host tracked for volume {}, using default endpoint selection", volume.getUuid()); + return null; + } + logger.info("Routing CLVM volume {} {} to lock holder host {}", volume.getUuid(), operation, 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); + return null; + } + + /** + * 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 found + */ + protected Long getClvmLockHostId(VolumeInfo volume) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + + Long lockHostId = clvmPoolManager.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 lockHostId; + } } diff --git a/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java b/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java index 9f01ab162ba..ca124ec30f0 100644 --- a/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java +++ b/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java @@ -17,20 +17,41 @@ 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.Volume; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.VolumeDetailVO; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +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.Spy; 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 { @@ -46,12 +67,55 @@ public class DefaultEndPointSelectorTest { @Mock private DataStore datastoreMock; - @Spy - private DefaultEndPointSelector defaultEndPointSelectorSpy; + @Mock + private StoragePoolVO storagePoolVOMock; + + @Mock + private PrimaryDataStoreDao _storagePoolDao; + + @Mock + private VolumeDetailsDao _volDetailsDao; + + @Mock + private VolumeDetailVO volumeDetailVOMock; + + @Mock + private EndPoint endPointMock; + + @Mock + ClvmPoolManager clvmPoolManager; + + @Mock + HostDao hostDao; + + static MockedStatic remoteHostEndPointMock; + + @InjectMocks + private DefaultEndPointSelector defaultEndPointSelectorSpy = Mockito.spy(new DefaultEndPointSelector()); + + private static final Long VOLUME_ID = 1L; + private static final Long HOST_ID = 10L; + private static final Long DEST_HOST_ID = 20L; + 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(); + + // Common volume mock setup + Mockito.when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + Mockito.when(volumeInfoMock.getUuid()).thenReturn(VOLUME_UUID); } @Test @@ -197,4 +261,293 @@ public class DefaultEndPointSelectorTest { Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(snapshotInfoMock, false); } + + @Test + public void testSelectClvmEndpoint_VolumeWithDestinationHost_CLVM() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.when(volumeInfoMock.getDestinationHostId()).thenReturn(DEST_HOST_ID); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_VolumeWithDestinationHost_CLVM_NG() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_VolumeWithoutDestinationHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.when(volumeInfoMock.getDestinationHostId()).thenReturn(null); + Mockito.when(datastoreMock.getScope()).thenReturn(Mockito.mock(Scope.class)); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointInScope( + Mockito.any(), Mockito.anyString(), Mockito.eq(STORE_ID), Mockito.eq(false)); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.never()).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_NonCLVMPool() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem); + Mockito.when(datastoreMock.getScope()).thenReturn(Mockito.mock(Scope.class)); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointInScope( + Mockito.any(), Mockito.anyString(), Mockito.eq(STORE_ID), Mockito.eq(false)); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.never()).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_SnapshotWithBaseVolumeDestHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(snapshotInfoMock.getBaseVolume()).thenReturn(volumeInfoMock); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Creating); + + EndPoint result = defaultEndPointSelectorSpy.select(snapshotInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVMWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVM_NG_WithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + 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.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVMWithoutLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(volumeInfoMock, false); + } + + @Test + public void testSelectWithAction_DeleteVolume_NonCLVM() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(_volDetailsDao, Mockito.never()).findDetail(Mockito.anyLong(), Mockito.anyString()); + } + + @Test + public void testSelectObject_CLVMVolumeWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectObject_CLVM_NG_VolumeWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectObject_CLVMVolumeWithoutLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + 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); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectObject_CLVMVolumeWithInvalidLockHostId() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectObject_CLVMVolumeWithEmptyLockHostId() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectTwoObjects_TemplateToVolume_CLVMWithDestHost() { + DataObject srcDataMock = Mockito.mock(DataObject.class); + + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Creating); + + EndPoint result = defaultEndPointSelectorSpy.select(srcDataMock, volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectTwoObjects_TemplateToVolume_CLVMWithoutDestHost() { + DataObject srcDataMock = Mockito.mock(DataObject.class); + DataStore srcStoreMock = Mockito.mock(DataStore.class); + + Mockito.when(srcDataMock.getDataStore()).thenReturn(srcStoreMock); + Mockito.when(srcStoreMock.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointForImageMove( + srcStoreMock, datastoreMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(srcDataMock, volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).findEndPointForImageMove(srcStoreMock, datastoreMock, false); + } + } diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java index 7de9000782e..7644d4688f7 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java @@ -37,6 +37,7 @@ import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; @@ -139,6 +140,18 @@ public class DefaultHostListener implements HypervisorHostListener { Map 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 (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + Boolean clvmSecureZeroFill = ClvmPoolManager.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); + } + } + ModifyStoragePoolCommand cmd = new ModifyStoragePoolCommand(true, pool, detailsMap); cmd.setWait(modifyStoragePoolCommandWait); HostVO host = hostDao.findById(hostId); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java index 43218b3f6a0..20d677f9013 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java @@ -24,6 +24,7 @@ import com.cloud.configuration.Resource.ResourceType; import com.cloud.dc.VsphereStoragePolicyVO; import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.storage.StorageManager; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.utils.Pair; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallbackNoReturn; @@ -126,6 +127,7 @@ public class VolumeObject implements VolumeInfo { private boolean directDownload; private String vSphereStoragePolicyId; private boolean followRedirects; + private Long destinationHostId; // For CLVM: hints where volume should be created private List checkpointPaths; private Set checkpointImageStoreUrls; @@ -361,6 +363,30 @@ public class VolumeObject implements VolumeInfo { this.directDownload = directDownload; } + @Override + public Long getDestinationHostId() { + // If not in memory, try to load from the database (volume_details table) + // For CLVM volumes, this uses the CLVM_LOCK_HOST_ID, which serves a dual purpose: + // 1. During creation: hints where to create the volume + // 2. After creation: tracks which host holds the exclusive lock + if (destinationHostId == null && volumeVO != null) { + VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeVO.getId(), ClvmPoolManager.CLVM_LOCK_HOST_ID); + if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) { + try { + destinationHostId = Long.parseLong(detail.getValue()); + } catch (NumberFormatException e) { + logger.warn("Invalid CLVM lock host ID value in volume_details for volume {}: {}", volumeVO.getUuid(), detail.getValue()); + } + } + } + return destinationHostId; + } + + @Override + public void setDestinationHostId(Long hostId) { + this.destinationHostId = hostId; + } + public void update() { volumeDao.update(volumeVO.getId(), volumeVO); volumeVO = volumeDao.findById(volumeVO.getId()); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 8731e8791dd..6fe4c2708c5 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -32,6 +32,8 @@ import java.util.concurrent.ExecutionException; import javax.inject.Inject; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -221,6 +223,8 @@ public class VolumeServiceImpl implements VolumeService { private PassphraseDao passphraseDao; @Inject protected DiskOfferingDao diskOfferingDao; + @Inject + ClvmPoolManager clvmPoolManager; public VolumeServiceImpl() { } @@ -2970,4 +2974,173 @@ public class VolumeServiceImpl implements VolumeService { protected String buildVolumePath(long accountId, long volumeId) { return String.format("%s/%s/%s", TemplateConstants.DEFAULT_VOLUME_ROOT_DIR, accountId, volumeId); } + + @Override + public boolean transferVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId) { + StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); + if (pool == null) { + logger.error("Cannot transfer volume lock for volume {}: storage pool not found", volume.getUuid()); + return false; + } + + logger.info("Transferring CLVM lock for volume {} (pool: {}) from host {} to host {}", + volume.getUuid(), pool.getName(), sourceHostId, destHostId); + + return clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), volume.getPath(), + pool, sourceHostId, destHostId); + } + + @Override + public Long findVolumeLockHost(VolumeInfo volume) { + if (volume == null) { + logger.warn("Cannot find volume lock host: volume is null"); + return null; + } + + StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); + + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (lockHostId != null) { + logger.debug("Found actual lock host {} for volume {}", lockHostId, volume.getUuid()); + return lockHostId; + } + + Long instanceId = volume.getInstanceId(); + if (instanceId != null) { + VMInstanceVO vmInstance = vmDao.findById(instanceId); + if (vmInstance != null && vmInstance.getHostId() != null) { + logger.debug("Volume {} is attached to VM {} on host {}", + volume.getUuid(), vmInstance.getUuid(), vmInstance.getHostId()); + return vmInstance.getHostId(); + } + } + + if (pool != null && pool.getClusterId() != null) { + List hosts = _hostDao.findByClusterId(pool.getClusterId()); + if (hosts != null && !hosts.isEmpty()) { + for (HostVO host : hosts) { + if (host.getStatus() == com.cloud.host.Status.Up) { + logger.debug("Using fallback: first UP host {} in cluster {} for volume {}", + host.getId(), pool.getClusterId(), volume.getUuid()); + return host.getId(); + } + } + } + } + + logger.warn("Could not determine lock host for volume {}", volume.getUuid()); + return null; + } + + @Override + public VolumeInfo performLockMigration(VolumeInfo volume, Long destHostId) { + if (volume == null) { + throw new CloudRuntimeException("Cannot perform CLVM lock migration: volume is null"); + } + + String volumeUuid = volume.getUuid(); + logger.info("Starting CLVM lock migration for volume {} (id: {}) to host {}", + volumeUuid, volume.getUuid(), destHostId); + + Long sourceHostId = findVolumeLockHost(volume); + if (sourceHostId == null) { + logger.warn("Could not determine source host for CLVM volume {} lock, assuming volume is not exclusively locked", + volumeUuid); + sourceHostId = destHostId; + } + + if (sourceHostId.equals(destHostId)) { + logger.info("CLVM volume {} already has lock on destination host {}, no migration needed", + volumeUuid, destHostId); + return volume; + } + + logger.info("Migrating CLVM volume {} lock from host {} to host {}", + volumeUuid, sourceHostId, destHostId); + + boolean success = transferVolumeLock(volume, sourceHostId, destHostId); + if (!success) { + throw new CloudRuntimeException( + String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s", + volumeUuid, sourceHostId, destHostId)); + } + + logger.info("Successfully migrated CLVM volume {} lock from host {} to host {}", + volumeUuid, sourceHostId, destHostId); + + return volFactory.getVolume(volume.getId()); + } + + @Override + public boolean areBothPoolsClvmType(StoragePoolType volumePoolType, StoragePoolType vmPoolType) { + if (volumePoolType == null || vmPoolType == null) { + logger.debug("Cannot check if both pools are CLVM type: one or both pool types are null"); + return false; + } + return ClvmPoolManager.isClvmPoolType(volumePoolType) && + ClvmPoolManager.isClvmPoolType(vmPoolType); + } + + @Override + public boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType volumePoolType, StoragePoolType vmPoolType, + Long volumePoolId, Long vmPoolId, Long vmHostId) { + if (volumePoolType != null && !ClvmPoolManager.isClvmPoolType(volumePoolType)) { + return false; + } + + if (volumePoolId == null || !volumePoolId.equals(vmPoolId)) { + return false; + } + + Long volumeLockHostId = findVolumeLockHost(volumeToAttach); + + if (volumeLockHostId == null) { + VolumeVO volumeVO = _volumeDao.findById(volumeToAttach.getId()); + if (volumeVO != null && volumeVO.getState() == Volume.State.Ready && volumeVO.getInstanceId() == null) { + logger.debug("CLVM volume {} is detached on same pool, lock transfer may be needed", + volumeToAttach.getUuid()); + return true; + } + } + + if (volumeLockHostId != null && vmHostId != null && !volumeLockHostId.equals(vmHostId)) { + logger.info("CLVM lock transfer required: Volume {} lock is on host {} but VM is on host {}", + volumeToAttach.getUuid(), volumeLockHostId, vmHostId); + return true; + } + + return false; + } + + @Override + public boolean isLightweightMigrationNeeded(StoragePoolType volumePoolType, StoragePoolType vmPoolType, + String volumePoolPath, String vmPoolPath) { + if (!areBothPoolsClvmType(volumePoolType, vmPoolType)) { + return false; + } + + String volumeVgName = extractVgNameFromPath(volumePoolPath); + String vmVgName = extractVgNameFromPath(vmPoolPath); + + if (volumeVgName != null && volumeVgName.equals(vmVgName)) { + logger.info("CLVM lightweight migration detected: Volume is in same VG ({}), only lock transfer needed (no data copy)", volumeVgName); + return true; + } + + return false; + } + + private String extractVgNameFromPath(String poolPath) { + if (poolPath == null) { + return null; + } + return poolPath.startsWith("/") ? poolPath.substring(1) : poolPath; + } } diff --git a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java new file mode 100644 index 00000000000..38af2a7550b --- /dev/null +++ b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java @@ -0,0 +1,520 @@ +// 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.volume; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +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; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; + +/** + * Tests for CLVM lock management methods in VolumeServiceImpl. + */ +@RunWith(MockitoJUnitRunner.class) +public class VolumeServiceImplClvmTest { + + @Spy + @InjectMocks + private VolumeServiceImpl volumeService; + + @Mock + private VolumeDao volumeDao; + + @Mock + private PrimaryDataStoreDao storagePoolDao; + + @Mock + private HostDao _hostDao; + + @Mock + private VMInstanceDao vmDao; + + @Mock + private VolumeDataFactory volFactory; + + @Mock + private VolumeInfo volumeInfoMock; + + @Mock + private VolumeVO volumeVOMock; + + @Mock + private StoragePoolVO storagePoolVOMock; + + @Mock + private HostVO hostVOMock; + + @Mock + private VMInstanceVO vmInstanceVOMock; + + @Mock + private ClvmPoolManager clvmPoolManager; + + private static final Long VOLUME_ID = 1L; + private static final Long POOL_ID_1 = 100L; + private static final Long POOL_ID_2 = 200L; + private static final Long HOST_ID_1 = 10L; + private static final Long HOST_ID_2 = 20L; + private static final String POOL_PATH_VG1 = "/vg1"; + + @Before + 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; + volumeService.vmDao = vmDao; + volumeService.volFactory = volFactory; + volumeService._volumeDao = volumeDao; + volumeService.clvmPoolManager = clvmPoolManager; + } + + @Test + public void testAreBothPoolsClvmType_BothCLVM() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_BothCLVM_NG() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG)); + } + + @Test + public void testAreBothPoolsClvmType_MixedCLVMAndCLVM_NG() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_OneCLVMOneNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.NetworkFilesystem)); + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_OneCLVM_NGOneNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.NetworkFilesystem)); + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM_NG)); + } + + @Test + public void testAreBothPoolsClvmType_BothNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem)); + } + + @Test + public void testAreBothPoolsClvmType_NullVolumePoolType() { + assertFalse(volumeService.areBothPoolsClvmType(null, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_NullVmPoolType() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, null)); + } + + @Test + public void testAreBothPoolsClvmType_BothNull() { + assertFalse(volumeService.areBothPoolsClvmType(null, null)); + } + + + @Test + public void testIsLockTransferRequired_NonCLVMPool() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullPoolIds() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + null, POOL_ID_1, HOST_ID_1)); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, null, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DetachedVolumeReady() { + when(volumeDao.findById(VOLUME_ID)).thenReturn(volumeVOMock); + when(volumeVOMock.getState()).thenReturn(Volume.State.Ready); + when(volumeVOMock.getInstanceId()).thenReturn(null); // Detached + + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DetachedVolumeNotReady() { + when(volumeDao.findById(VOLUME_ID)).thenReturn(volumeVOMock); + when(volumeVOMock.getState()).thenReturn(Volume.State.Allocated); + + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentHosts() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_2)); + } + + @Test + public void testIsLockTransferRequired_SameHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullVmHostId() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, null)); + } + + @Test + public void testIsLockTransferRequired_CLVM_NG_DifferentHosts() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + POOL_ID_1, POOL_ID_1, HOST_ID_2)); + } + + @Test + public void testIsLightweightMigrationNeeded_NonCLVMPools() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem, + POOL_PATH_VG1, POOL_PATH_VG1)); + } + + @Test + public void testIsLightweightMigrationNeeded_OneCLVMOneNFS() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.NetworkFilesystem, + POOL_PATH_VG1, POOL_PATH_VG1)); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG_NoSlash() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "vg1", "vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG_MixedSlash() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "vg1")); + + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_DifferentVG() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "/vg2")); + } + + @Test + public void testIsLightweightMigrationNeeded_CLVM_NG_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_CLVM_NG_DifferentVG() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + "/vg1", "/vg2")); + } + + @Test + public void testIsLightweightMigrationNeeded_MixedCLVM_CLVM_NG_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM_NG, + "/vg1", "/vg1")); + + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_NullVolumePath() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + null, "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_NullVmPath() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", null)); + } + + @Test + public void testIsLightweightMigrationNeeded_BothPathsNull() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + null, null)); + } + + @Test + public void testIsLightweightMigrationNeeded_ComplexVGNames() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/cloudstack-vg-01", "/cloudstack-vg-01")); + + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/cloudstack-vg-01", "/cloudstack-vg-02")); + } + + @Test + public void testTransferVolumeLock_Success() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(true); + + assertTrue(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testTransferVolumeLock_Failure() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(false); + + assertFalse(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testTransferVolumeLock_PoolNotFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(null); + + assertFalse(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testFindVolumeLockHost_NullVolume() { + Long result = volumeService.findVolumeLockHost(null); + assertNull(result); + } + + @Test + public void testFindVolumeLockHost_ExplicitLockFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_FromAttachedVM() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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); + when(vmInstanceVOMock.getUuid()).thenReturn("vm-uuid"); + when(vmInstanceVOMock.getHostId()).thenReturn(HOST_ID_1); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_FallbackToClusterHost() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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); + when(_hostDao.findByClusterId(10L)).thenReturn(java.util.Collections.singletonList(hostVOMock)); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_NoHostFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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()); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertNull(result); + } + + @Test + public void testPerformLockMigration_Success() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(true); + when(volFactory.getVolume(VOLUME_ID)).thenReturn(volumeInfoMock); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + assertNotNull(result); + } + + @Test + public void testPerformLockMigration_SameHost() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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); + } + + @Test + public void testPerformLockMigration_SourceHostNull() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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); + assertNotNull(result); + } + + @Test(expected = com.cloud.utils.exception.CloudRuntimeException.class) + public void testPerformLockMigration_NullVolume() { + volumeService.performLockMigration(null, HOST_ID_2); + } + + @Test(expected = com.cloud.utils.exception.CloudRuntimeException.class) + public void testPerformLockMigration_TransferFails() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.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(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(false); + + volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index d0a162e0f6f..4a93b1bce4a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -79,6 +79,7 @@ import org.apache.cloudstack.command.ReconcileCommandService; import org.apache.cloudstack.command.ReconcileCommandUtils; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsAnswer; import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsCommand; import org.apache.cloudstack.storage.configdrive.ConfigDrive; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; @@ -228,6 +229,7 @@ import com.cloud.storage.JavaStorageLayer; import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageLayer; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Volume; import com.cloud.storage.resource.StorageSubsystemCommandHandler; import com.cloud.storage.resource.StorageSubsystemCommandHandlerBase; @@ -2584,6 +2586,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv if (pool.getType() == StoragePoolType.CLVM && volFormat == PhysicalDiskFormat.RAW) { return "CLVM"; + } else if (poolType == StoragePoolType.CLVM_NG) { + return "CLVM_NG"; } else if ((poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.SharedMountPoint || poolType == StoragePoolType.Filesystem @@ -3822,13 +3826,18 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv final String glusterVolume = pool.getSourceDir().replace("/", ""); disk.defNetworkBasedDisk(glusterVolume + path.replace(mountpoint, ""), pool.getSourceHost(), pool.getSourcePort(), null, null, devId, diskBusType, DiskProtocol.GLUSTER, DiskDef.DiskFmtType.QCOW2); - } else if (pool.getType() == StoragePoolType.CLVM || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { + } else if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { if (volume.getType() == Volume.Type.DATADISK && !(isWindowsTemplate && isUefiEnabled)) { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusTypeData); - } - else { + } else { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusType); } + + // CLVM_NG uses QCOW2 format on block devices, override the default RAW format + if (pool.getType() == StoragePoolType.CLVM_NG) { + disk.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); + } + if (pool.getType() == StoragePoolType.Linstor && isQemuDiscardBugFree(diskBusType)) { disk.setDiscard(DiscardType.UNMAP); } @@ -5728,10 +5737,73 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public Answer listFilesAtPath(ListDataStoreObjectsCommand command) { DataStoreTO store = command.getStore(); - KVMStoragePool storagePool = storagePoolManager.getStoragePool(StoragePoolType.NetworkFilesystem, store.getUuid()); + StoragePoolType poolType = StoragePoolType.NetworkFilesystem; + if (store instanceof PrimaryDataStoreTO) { + poolType = ((PrimaryDataStoreTO) store).getPoolType(); + } + KVMStoragePool storagePool = storagePoolManager.getStoragePool(poolType, store.getUuid()); + if (ClvmPoolManager.isClvmPoolType(poolType)) { + return listLvmVolumes(storagePool.getLocalPath(), command.getStartIndex(), command.getPageSize()); + } return listFilesAtPath(storagePool.getLocalPath(), command.getPath(), command.getStartIndex(), command.getPageSize()); } + private Answer listLvmVolumes(String localPath, int startIndex, int pageSize) { + String vgName = localPath; + if (vgName.startsWith("/")) { + String[] parts = vgName.split("/"); + for (int i = parts.length - 1; i >= 0; i--) { + if (!parts[i].isEmpty()) { + vgName = parts[i]; + break; + } + } + } + + Script lvs = new Script("lvs", 30000, logger); + lvs.add("--noheadings"); + lvs.add("--nosuffix"); + lvs.add("-o", "lv_name,lv_size"); + lvs.add("--units", "b"); + lvs.add(vgName); + AllLinesParser parser = new AllLinesParser(); + String result = lvs.execute(parser); + + List names = new ArrayList<>(); + List paths = new ArrayList<>(); + List absPaths = new ArrayList<>(); + List isDirs = new ArrayList<>(); + List sizes = new ArrayList<>(); + List lastModified = new ArrayList<>(); + + if (result != null) { + logger.warn("lvs listing failed for VG {}: {}", vgName, result); + return new ListDataStoreObjectsAnswer(false, 0, names, paths, absPaths, isDirs, sizes, lastModified); + } + + List entries = new ArrayList<>(); + for (String line : parser.getLines().split("\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + String[] cols = trimmed.split("\\s+"); + if (cols.length >= 2) entries.add(cols); + } + + int count = entries.size(); + for (int i = startIndex; i < startIndex + pageSize && i < count; i++) { + String lvName = entries.get(i)[0]; + long size = 0; + try { size = Long.parseLong(entries.get(i)[1]); } catch (NumberFormatException ignored) {} + names.add(lvName); + paths.add("/" + lvName); + absPaths.add("/dev/" + vgName + "/" + lvName); + isDirs.add(false); + sizes.add(size); + lastModified.add(0L); + } + return new ListDataStoreObjectsAnswer(true, count, names, paths, absPaths, isDirs, sizes, lastModified); + } + public boolean addNetworkRules(final String vmName, final String vmId, final String guestIP, final String guestIP6, final String sig, final String seq, final String mac, final String rules, final String vif, final String brname, final String secIps) { if (!canBridgeFirewall) { @@ -6820,4 +6892,237 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public String getGuestCpuArch() { return guestCpuArch; } + + /** + * CLVM volume state for migration operations on source host + */ + public enum ClvmVolumeState { + /** Shared mode (-asy) - used before migration to allow both hosts to access volume */ + SHARED("-asy", "shared", "Before migration: activating in shared mode"), + + /** Deactivate (-an) - used after successful migration to release volume on source */ + DEACTIVATE("-an", "deactivated", "After successful migration: deactivating volume"), + + /** Exclusive mode (-aey) - used after failed migration to revert to original exclusive state */ + EXCLUSIVE("-aey", "exclusive", "After failed migration: reverting to exclusive mode"); + + private final String lvchangeFlag; + private final String description; + private final String logMessage; + + ClvmVolumeState(String lvchangeFlag, String description, String logMessage) { + this.lvchangeFlag = lvchangeFlag; + this.description = description; + this.logMessage = logMessage; + } + + public String getLvchangeFlag() { + return lvchangeFlag; + } + + public String getDescription() { + return description; + } + + public String getLogMessage() { + return logMessage; + } + } + + public static void modifyClvmVolumesStateForMigration(List disks, VirtualMachineTO vmSpec, ClvmVolumeState state) { + for (DiskDef disk : disks) { + if (isClvmVolume(disk, vmSpec)) { + String volumePath = disk.getDiskPath(); + try { + modifyClvmVolumeState(volumePath, state.getLvchangeFlag(), state.getDescription(), state.getLogMessage()); + } catch (Exception e) { + LOGGER.error("[CLVM Migration] Exception while setting volume [{}] to {} state: {}", + volumePath, state.getDescription(), e.getMessage(), e); + } + } + } + } + + private static void modifyClvmVolumeState(String volumePath, String lvchangeFlag, + String stateDescription, String logMessage) { + try { + LOGGER.info("{} for volume [{}]", logMessage, volumePath); + + Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER); + cmd.add(lvchangeFlag); + cmd.add(volumePath); + + String result = cmd.execute(); + if (result != null) { + String errorMsg = String.format( + "Failed to set volume [%s] to %s state. Command result: %s", + volumePath, stateDescription, result); + LOGGER.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } else { + LOGGER.info("Successfully set volume [{}] to {} state.", + volumePath, stateDescription); + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = String.format( + "Exception while setting volume [%s] to %s state: %s", + volumePath, stateDescription, e.getMessage()); + LOGGER.error(errorMsg, e); + throw new CloudRuntimeException(errorMsg, e); + } + } + + public static void activateClvmVolumeExclusive(String volumePath) { + modifyClvmVolumeState(volumePath, ClvmVolumeState.EXCLUSIVE.getLvchangeFlag(), + ClvmVolumeState.EXCLUSIVE.getDescription(), + "Activating CLVM volume in exclusive mode"); + } + + public static void deactivateClvmVolume(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.DEACTIVATE.getLvchangeFlag(), + ClvmVolumeState.DEACTIVATE.getDescription(), + "Deactivating CLVM volume"); + } catch (Exception e) { + LOGGER.warn("Failed to deactivate CLVM volume {}: {}", volumePath, e.getMessage()); + } + } + + public static void setClvmVolumeToSharedMode(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.SHARED.getLvchangeFlag(), + ClvmVolumeState.SHARED.getDescription(), + "Setting CLVM volume to shared mode"); + } catch (Exception e) { + LOGGER.warn("Failed to set CLVM volume {} to shared mode: {}", volumePath, e.getMessage()); + } + } + + /** + * Determines if a disk is on a CLVM storage pool by checking the actual pool type from VirtualMachineTO. + * This is the most reliable method as it uses CloudStack's own storage pool information. + * + * @param disk The disk definition to check + * @param resource The LibvirtComputingResource instance (unused but kept for compatibility) + * @param vmSpec The VirtualMachineTO specification containing disk and pool information + * @return true if the disk is on a CLVM storage pool, false otherwise + */ + private static boolean isClvmVolume(DiskDef disk, VirtualMachineTO vmSpec) { + String diskPath = disk.getDiskPath(); + if (diskPath == null || vmSpec == null) { + return false; + } + + try { + if (vmSpec.getDisks() != null) { + for (DiskTO diskTO : vmSpec.getDisks()) { + if (!(diskTO.getData() instanceof VolumeObjectTO)) { + continue; + } + VolumeObjectTO volumeTO = (VolumeObjectTO) diskTO.getData(); + if (!diskPath.equals(volumeTO.getPath()) && !diskPath.equals(diskTO.getPath())) { + continue; + } + DataStoreTO dataStore = volumeTO.getDataStore(); + if (!(dataStore instanceof PrimaryDataStoreTO)) { + continue; + } + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) dataStore; + boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType() || + StoragePoolType.CLVM_NG == primaryStore.getPoolType(); + LOGGER.debug("Disk {} identified as CLVM/CLVM_NG={} via VirtualMachineTO pool type: {}", + diskPath, isClvm, primaryStore.getPoolType()); + return isClvm; + } + } + + if (diskPath.startsWith("/dev/") && !diskPath.contains("/dev/mapper/")) { + String vgName = extractVolumeGroupFromPath(diskPath); + if (vgName != null) { + boolean isClustered = checkIfVolumeGroupIsClustered(vgName); + LOGGER.debug("Disk {} VG {} identified as clustered={} via vgs attribute check", + diskPath, vgName, isClustered); + return isClustered; + } + } + + } catch (Exception e) { + LOGGER.error("Error determining if volume {} is CLVM: {}", diskPath, e.getMessage(), e); + } + + return false; + } + + /** + * Extracts the volume group name from a device path. + * + * @param devicePath The device path (e.g., /dev/vgname/lvname) + * @return The volume group name, or null if cannot be determined + */ + static String extractVolumeGroupFromPath(String devicePath) { + if (devicePath == null || !devicePath.startsWith("/dev/")) { + return null; + } + + // Format: /dev// + String[] parts = devicePath.split("/"); + if (parts.length >= 3) { + return parts[2]; // ["", "dev", "vgname", ...] + } + + return null; + } + + /** + * Checks if a volume group is clustered (CLVM) by examining its attributes. + * Uses 'vgs' command to check for the clustered/shared flag in VG attributes. + * + * VG Attr format (6 characters): wz--nc or wz--ns + * Position 6: Clustered flag - 'c' = CLVM (clustered), 's' = shared (lvmlockd), '-' = not clustered + * + * @param vgName The volume group name + * @return true if the VG is clustered or shared, false otherwise + */ + static boolean checkIfVolumeGroupIsClustered(String vgName) { + if (vgName == null) { + return false; + } + + try { + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + Script vgsCmd = new Script("vgs", 30000, LOGGER); + vgsCmd.add("--noheadings"); + vgsCmd.add("--unbuffered"); + vgsCmd.add("-o"); + vgsCmd.add("vg_attr"); + vgsCmd.add(vgName); + + String result = vgsCmd.execute(parser); + + if (result == null && parser.getLines() != null) { + String output = parser.getLines(); + if (output != null && !output.isEmpty()) { + String vgAttr = output.trim(); + if (vgAttr.length() >= 6) { + char clusterFlag = vgAttr.charAt(5); // Position 6 (0-indexed 5) + boolean isClustered = (clusterFlag == 'c' || clusterFlag == 's'); + LOGGER.debug("VG {} has attributes '{}', cluster/shared flag '{}' = {}", + vgName, vgAttr, clusterFlag, isClustered); + return isClustered; + } else { + LOGGER.warn("VG {} attributes '{}' have unexpected format (expected 6+ chars)", vgName, vgAttr); + } + } + } else { + LOGGER.warn("Failed to get VG attributes for {}: {}", vgName, result); + } + + } catch (Exception e) { + LOGGER.debug("Error checking if VG {} is clustered: {}", vgName, e.getMessage()); + } + + return false; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java new file mode 100644 index 00000000000..18c4324addf --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java @@ -0,0 +1,173 @@ +// 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 com.cloud.agent.api.Answer; +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; +import org.apache.cloudstack.storage.clvm.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 + extends CommandWrapper { + + @Override + public Answer execute(ClvmLockTransferCommand cmd, LibvirtComputingResource serverResource) { + String lvPath = cmd.getLvPath(); + ClvmLockTransferCommand.Operation operation = cmd.getOperation(); + String volumeUuid = cmd.getVolumeUuid(); + + logger.info("Executing CLVM lock transfer: operation={}, lv={}, volume={}", + operation, lvPath, volumeUuid); + + try { + + if (operation == ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE) { + return handleQueryLockState(cmd, lvPath, volumeUuid); + } + + 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 ClvmLockTransferAnswer(cmd, false, "Unknown operation: " + operation); + } + + Script script = new Script("/usr/sbin/lvchange", 60000, 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 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 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 ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage()); + } + } + + /** + * Query whether this host currently has the CLVM LV activated locally. + * Executes: lvs -o lv_attr,lv_host,lv_active --noheadings + * + * lv_attr[4]=='a' (isActive) is LOCAL and is the authoritative signal — true only on + * the host where the LV is currently activated. The management server fans out this + * query to all cluster hosts; the one returning isActive=true is the lock holder. + * lv_attr[5]=='o' (isOpen) means a VM has the device open on this host (doing I/O). + * lv_host is retained for diagnostic logging only — do NOT use it to identify the + * lock holder. + */ + private Answer handleQueryLockState(ClvmLockTransferCommand cmd, String lvPath, String volumeUuid) { + try { + Script script = new Script("/usr/sbin/lvs", 30000, logger); + script.add("-o"); + script.add("lv_attr,lv_host"); + script.add("--noheadings"); + script.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + 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)); + } + + String[] lines = parser.getLines().split("\n"); + String dataLine = null; + + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && + trimmed.length() >= 10 && + "-wrsvmpco".indexOf(trimmed.charAt(0)) >= 0) { + dataLine = trimmed; + break; + } + } + + if (dataLine == null) { + String allOutput = parser.getLines(); + logger.warn("Could not find lv_attr data line in lvs output for volume {}: {}", + volumeUuid, allOutput); + return new ClvmLockTransferAnswer(cmd, false, + String.format("Could not parse lvs output. Full output: %s", allOutput)); + } + + logger.debug("Parsed lv_attr data line for volume {}: {}", volumeUuid, dataLine); + + String[] parts = dataLine.split("\\s+"); + if (parts.length < 1) { + return new ClvmLockTransferAnswer(cmd, false, "Invalid lvs output format"); + } + + String lvAttr = parts[0]; + // lv_host: for diagnostics only, unreliable for lock-holder identification + String hostname = parts.length > 1 ? parts[1] : null; + + // lv_attr[4]=='a' → LV is active on THIS host (local activation state) + boolean isActive = lvAttr.length() > 4 && lvAttr.charAt(4) == 'a'; + // lv_attr[5]=='o' → a process has the device file open on this host (VM doing I/O) + boolean isOpen = lvAttr.length() > 5 && lvAttr.charAt(5) == 'o'; + + logger.info("Queried lock state for volume {}: attr={}, hostname={}, active={}, open={}", + volumeUuid, lvAttr, hostname, isActive, isOpen); + + return new ClvmLockTransferAnswer(cmd, true, + String.format("Lock state: active=%s, open=%s, host=%s", + isActive, isOpen, hostname != null ? hostname : "none"), + hostname, isActive, isOpen, 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()); + } + } + +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index f54918bbc22..ed02ae6da38 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -1,5 +1,3 @@ -// -// 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 @@ -42,9 +40,16 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import com.cloud.agent.api.VgpuTypesInfo; +import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.GPUDeviceTO; import com.cloud.hypervisor.kvm.resource.LibvirtGpuDef; import com.cloud.hypervisor.kvm.resource.LibvirtXMLParser; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.Ternary; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.collections4.CollectionUtils; @@ -69,7 +74,6 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.MigrateAnswer; import com.cloud.agent.api.MigrateCommand; import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; -import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.VirtualMachineTO; @@ -82,11 +86,6 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; import com.cloud.hypervisor.kvm.resource.MigrateKVMAsync; import com.cloud.hypervisor.kvm.resource.VifDriver; -import com.cloud.resource.CommandWrapper; -import com.cloud.resource.ResourceWrapper; -import com.cloud.utils.Ternary; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VirtualMachine; @ResourceWrapper(handles = MigrateCommand.class) public final class LibvirtMigrateCommandWrapper extends CommandWrapper { @@ -117,7 +116,8 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper ifaces = null; - List disks; + List disks = new ArrayList<>(); + VirtualMachineTO to = null; Domain dm = null; Connect dconn = null; @@ -136,7 +136,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper migrateDiskInfoList, List disks) { for (DiskDef disk : disks) { - MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); if (migrateDiskInfo != null && migrateDiskInfo.isSourceDiskOnStorageFileSystem()) { deleteLocalVolume(disk.getDiskPath()); } else { @@ -800,7 +812,10 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper]*type=['\"]vnc['\"][^>]*passwd=['\"])([^'\"]*)(['\"])", "$1*****$3"); } + + /** + * Checks if any of the destination disks in the migration target a CLVM or CLVM_NG storage pool. + * This is used to determine if incremental migration should be disabled to avoid libvirt + * precreate errors with QCOW2-on-LVM setups. + * + * @param mapMigrateStorage the map containing migration disk information with destination pool types + * @return true if any destination disk targets CLVM or CLVM_NG, false otherwise + */ + protected boolean hasClvmDestinationDisks(Map mapMigrateStorage) { + if (MapUtils.isEmpty(mapMigrateStorage)) { + return false; + } + + try { + for (Map.Entry entry : mapMigrateStorage.entrySet()) { + MigrateCommand.MigrateDiskInfo diskInfo = entry.getValue(); + if (isClvmBlockDevice(diskInfo)) { + logger.debug("Found disk targeting CLVM/CLVM_NG destination pool"); + return true; + } + } + } catch (final Exception e) { + logger.debug("Failed to check for CLVM destination disks: {}. Assuming no CLVM disks.", e.getMessage()); + } + + return false; + } + + private boolean isClvmBlockDevice(MigrateCommand.MigrateDiskInfo diskInfo) { + if (diskInfo == null ||diskInfo.getDestPoolType() == null) { + return false; + } + return (Storage.StoragePoolType.CLVM.equals(diskInfo.getDestPoolType()) || Storage.StoragePoolType.CLVM_NG.equals(diskInfo.getDestPoolType())); + } + + /** + * Determines if the driver type should be updated during migration based on CLVM involvement. + * The driver type needs to be updated when: + * - Managed storage is being migrated, OR + * - Source pool is CLVM or CLVM_NG, OR + * - Destination pool is CLVM or CLVM_NG + * + * This ensures the libvirt XML driver type matches the destination format (raw/qcow2/etc). + * + * @param migrateStorageManaged true if migrating managed storage + * @param migrateDiskInfo the migration disk information containing source and destination pool types + * @return true if driver type should be updated, false otherwise + */ + private boolean shouldUpdateDriverTypeForMigration(boolean migrateStorageManaged, + MigrateCommand.MigrateDiskInfo migrateDiskInfo) { + boolean sourceIsClvm = Storage.StoragePoolType.CLVM == migrateDiskInfo.getSourcePoolType() || + Storage.StoragePoolType.CLVM_NG == migrateDiskInfo.getSourcePoolType(); + + boolean destIsClvm = Storage.StoragePoolType.CLVM == migrateDiskInfo.getDestPoolType() || + Storage.StoragePoolType.CLVM_NG == migrateDiskInfo.getDestPoolType(); + + boolean isClvmRelatedMigration = sourceIsClvm || destIsClvm; + return migrateStorageManaged || isClvmRelatedMigration; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java index 990cefda8f3..bc22d7bfd70 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java @@ -52,9 +52,19 @@ public final class LibvirtModifyStoragePoolCommandWrapper extends CommandWrapper final KVMStoragePool storagepool; try { + Map poolDetails = command.getDetails(); + if (poolDetails == null) { + poolDetails = new HashMap<>(); + } + + // Ensure CLVM secure zero-fill setting has a default value if not provided by MS + if (!poolDetails.containsKey(KVMStoragePool.CLVM_SECURE_ZERO_FILL)) { + poolDetails.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + } + storagepool = storagePoolMgr.createStoragePool(command.getPool().getUuid(), command.getPool().getHost(), command.getPool().getPort(), command.getPool().getPath(), command.getPool() - .getUserInfo(), command.getPool().getType(), command.getDetails()); + .getUserInfo(), command.getPool().getType(), poolDetails); if (storagepool == null) { return new Answer(command, false, " Failed to create storage pool"); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java new file mode 100644 index 00000000000..608770974dc --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java @@ -0,0 +1,82 @@ +// +// 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 java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.PostMigrationAnswer; +import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +/** + * Wrapper for PostMigrationCommand on KVM hypervisor. + * Handles post-migration tasks on the destination host after a VM has been successfully migrated. + * Primary responsibility: Convert CLVM volumes from shared mode to exclusive mode on destination. + */ +@ResourceWrapper(handles = PostMigrationCommand.class) +public final class LibvirtPostMigrationCommandWrapper extends CommandWrapper { + + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(final PostMigrationCommand command, final LibvirtComputingResource libvirtComputingResource) { + final VirtualMachineTO vm = command.getVirtualMachine(); + final String vmName = command.getVmName(); + + if (vm == null || vmName == null) { + return new PostMigrationAnswer(command, "VM or VM name is null"); + } + + logger.debug("Executing post-migration tasks for VM {} on destination host", vmName); + + try { + final Connect conn = LibvirtConnection.getConnectionByVmName(vmName); + + List disks = libvirtComputingResource.getDisks(conn, vmName); + logger.debug("[CLVM Post-Migration] Processing volumes for VM {} to claim exclusive locks on any CLVM volumes", vmName); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vm, + LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE + ); + + logger.debug("Successfully completed post-migration tasks for VM {}", vmName); + return new PostMigrationAnswer(command); + + } catch (final LibvirtException e) { + logger.error("Libvirt error during post-migration for VM {}: {}", vmName, e.getMessage(), e); + return new PostMigrationAnswer(command, e); + } catch (final Exception e) { + logger.error("Error during post-migration for VM {}: {}", vmName, e.getMessage(), e); + return new PostMigrationAnswer(command, e); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java new file mode 100644 index 00000000000..c47760040c8 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java @@ -0,0 +1,84 @@ +// +// 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 com.cloud.agent.api.Answer; +import com.cloud.agent.api.PreMigrationCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +import java.util.List; + +/** + * Handles PreMigrationCommand on the source host before live migration. + * Converts CLVM volume locks from exclusive to shared mode so the destination host can access them. + */ +@ResourceWrapper(handles = PreMigrationCommand.class) +public class LibvirtPreMigrationCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(PreMigrationCommand command, LibvirtComputingResource libvirtComputingResource) { + String vmName = command.getVmName(); + VirtualMachineTO vmSpec = command.getVirtualMachine(); + + logger.info("Preparing source host for migration of VM: {}", vmName); + + Connect conn = null; + Domain dm = null; + + try { + LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + dm = conn.domainLookupByName(vmName); + + List disks = libvirtComputingResource.getDisks(conn, vmName); + logger.info("Converting CLVM volumes to shared mode for VM: {}", vmName); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vmSpec, + LibvirtComputingResource.ClvmVolumeState.SHARED + ); + + logger.info("Successfully prepared source host for migration of VM: {}", vmName); + return new Answer(command, true, "Source host prepared for migration"); + + } catch (LibvirtException e) { + logger.error("Failed to prepare source host for migration of VM: {}", vmName, e); + return new Answer(command, false, "Failed to prepare source host: " + e.getMessage()); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException e) { + logger.warn("Failed to free domain {}: {}", vmName, e.getMessage()); + } + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java index d9323df4477..f7ca79127da 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java @@ -21,6 +21,7 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.cloudstack.storage.configdrive.ConfigDrive; @@ -124,6 +125,19 @@ public final class LibvirtPrepareForMigrationCommandWrapper extends CommandWrapp return new PrepareForMigrationAnswer(command, "failed to connect physical disks to host"); } + // Activate CLVM volumes in shared mode on destination host for live migration + try { + List disks = libvirtComputingResource.getDisks(conn, vm.getName()); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vm, + LibvirtComputingResource.ClvmVolumeState.SHARED + ); + } catch (Exception e) { + logger.warn("Failed to activate CLVM volumes in shared mode on destination for VM {}: {}", + vm.getName(), e.getMessage(), e); + } + logger.info("Successfully prepared destination host for migration of VM {}", vm.getName()); return createPrepareForMigrationAnswer(command, dpdkInterfaceMapping, libvirtComputingResource, vm); } catch (final LibvirtException | CloudRuntimeException | InternalErrorException | URISyntaxException e) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java index f2af46d4cc8..a43b584dd6d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java @@ -113,7 +113,8 @@ public final class LibvirtResizeVolumeCommandWrapper extends CommandWrapper details, boolean isPrimaryStorage) { + logger.info("Attempting to create CLVM/CLVM_NG storage pool {} in libvirt", name); + + Connect conn; + try { + conn = LibvirtConnection.getConnection(); + } catch (LibvirtException e) { + throw new CloudRuntimeException(e.toString()); + } + + StoragePool sp = createCLVMStoragePool(conn, name, host, path); + if (sp == null) { + logger.info("Falling back to virtual CLVM/CLVM_NG pool without libvirt for: {}", name); + return createVirtualClvmPool(name, host, path, type, details); + } + + try { + if (!isPrimaryStorage) { + incStoragePoolRefCount(name); + } + // CLVM/CLVM_NG pools are kept inactive in libvirt; we use direct LVM commands + return getStoragePool(name); + } catch (Exception e) { + decStoragePoolRefCount(name); + throw new CloudRuntimeException("Failed to create CLVM storage pool: " + name, e); + } + } + + @Override + public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { + logger.info("Fetching CLVM/CLVM_NG storage pool {} ", uuid); + try { + Connect conn = LibvirtConnection.getConnection(); + StoragePool storage = conn.storagePoolLookupByUUIDString(uuid); + + LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); + if (spd == null) { + throw new CloudRuntimeException("Unable to parse storage pool definition for pool " + uuid); + } + + // CLVM pools in libvirt are always LOGICAL type + StoragePoolType type = StoragePoolType.CLVM; + + // Do NOT activate the pool — CLVM/CLVM_NG pools stay inactive in libvirt + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); + pool.setLocalPath(spd.getTargetPath()); + + // Always read capacity from LVM directly + String vgName = storage.getName(); + try { + long[] vgStats = getVgStats(vgName); + setPoolCapacityFromVgStats(pool, vgStats, vgName); + } catch (CloudRuntimeException e) { + logger.warn("Failed to get VG stats for CLVM/CLVM_NG pool {}: {}. Using libvirt values (may be 0)", vgName, e.getMessage()); + pool.setCapacity(storage.getInfo().capacity); + pool.setUsed(storage.getInfo().allocation); + pool.setAvailable(storage.getInfo().available); + } + + return pool; + } catch (LibvirtException e) { + logger.debug("CLVM/CLVM_NG pool {} not found in libvirt, creating virtual pool", uuid); + throw new CloudRuntimeException(e.toString(), e); + } + } + + @Override + public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { + LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; + + // Pool has no libvirt backing - go directly to block device + if (libvirtPool.getPool() == null) { + logger.debug("CLVM/CLVM_NG pool has no libvirt backing, using direct block device access for volume: {}", volumeUuid); + return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); + } + + try { + StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol == null) { + logger.debug("Volume {} not found in libvirt, falling back to CLVM direct access", volumeUuid); + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); + } + + boolean isQcow2 = StoragePoolType.CLVM_NG.equals(pool.getType()); + KVMPhysicalDisk disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); + disk.setSize(vol.getInfo().allocation); + disk.setVirtualSize(isQcow2 ? getQcow2VirtualSize(vol.getPath()) : vol.getInfo().capacity); + disk.setFormat(isQcow2 ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW); + return disk; + } catch (LibvirtException e) { + logger.warn("LibvirtException looking up volume {}: {}", volumeUuid, e.getMessage()); + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); + } + } + + @Override + public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, + PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { + logger.info("Creating CLVM/CLVM_NG volume {} in pool {} with size {}", name, pool.getUuid(), toHumanReadableSize(size)); + + if (StoragePoolType.CLVM_NG.equals(pool.getType())) { + return createClvmNgDiskWithBacking(name, 0, size, null, pool, provisioningType); + } else { + return createClvmVolume(name, size, pool); + } + } + + @Override + public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { + if (isVMMigrate) { + logger.info("Activating CLVM/CLVM_NG volume {} in shared mode for VM migration", name); + Script activateVol = new Script("lvchange", 30000, logger); + activateVol.add("-asy"); + activateVol.add(pool.getLocalPath() + File.separator + name); + String result = activateVol.execute(); + if (result != null) { + logger.error("Failed to activate CLVM/CLVM_NG volume {} in shared mode. Output: {}", name, result); + return false; + } + } + + if (StoragePoolType.CLVM_NG.equals(pool.getType())) { + ensureClvmNgBackingFileAccessible(name, pool); + } + + return true; + } + + @Override + public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, + String name, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, + long size, KVMStoragePool destPool, int timeout, byte[] passphrase) { + + if (StoragePoolType.CLVM_NG.equals(destPool.getType()) && format == PhysicalDiskFormat.QCOW2) { + logger.info("Creating CLVM_NG volume {} with backing file from template {}", name, template.getName()); + String backingFile = getClvmBackingFile(template, destPool); + return createClvmNgDiskWithBacking(name, timeout, size, backingFile, destPool, provisioningType); + } + + return super.createDiskFromTemplate(template, name, format, provisioningType, size, destPool, timeout, passphrase); + } + + @Override + public void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String lvName = "template-" + templateUuid; + String lvPath = "/dev/" + vgName + "/" + lvName; + + if (lvExists(lvPath)) { + logger.info("Template LV {} already exists in VG {}. Skipping creation.", lvName, vgName); + return; + } + + logger.info("Creating new template LV {} in VG {} for template {}", lvName, vgName, templateUuid); + + long virtualSize = getQcow2VirtualSize(templatePath); + long physicalSize = getQcow2PhysicalSize(templatePath); + long lvSize = virtualSize; + + logger.info("Template source - Physical: {} bytes, Virtual: {} bytes, LV will be: {} bytes", physicalSize, virtualSize, lvSize); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", lvName); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG template: " + result); + } + + Script qemuImgConvert = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImgConvert.add("convert"); + qemuImgConvert.add(templatePath); + qemuImgConvert.add("-O", "qcow2"); + qemuImgConvert.add("-o", "cluster_size=64k,extended_l2=off,preallocation=off"); + qemuImgConvert.add(lvPath); + result = qemuImgConvert.execute(); + + if (result != null) { + removeLvOnFailure(lvPath, timeout); + throw new CloudRuntimeException("Failed to convert template to CLVM_NG volume: " + result); + } + + long actualVirtualSize = getQcow2VirtualSize(lvPath); + + try { + ensureTemplateLvInSharedMode(lvPath, true); + } catch (CloudRuntimeException e) { + logger.error("Failed to activate template LV {} in shared mode. Cleaning up.", lvPath); + removeLvOnFailure(lvPath, timeout); + throw e; + } + + KVMPhysicalDisk templateDisk = new KVMPhysicalDisk(lvPath, lvName, pool); + templateDisk.setFormat(PhysicalDiskFormat.QCOW2); + templateDisk.setVirtualSize(actualVirtualSize); + templateDisk.setSize(lvSize); + } + + private StoragePool createCLVMStoragePool(Connect conn, String uuid, String host, String path) { + String volgroupPath = "/dev/" + path; + String volgroupName = path; + volgroupName = volgroupName.replaceFirst("^/", ""); + + Script checkVgExists = new Script("vgs", 30000, logger); + checkVgExists.add("--noheadings"); + checkVgExists.add("-o", "vg_name"); + checkVgExists.add(volgroupName); + String vgCheckResult = checkVgExists.execute(); + + if (vgCheckResult != null) { + logger.error("Volume group {} does not exist or is not accessible", volgroupName); + return null; + } + + logger.info("Volume group {} verified, creating libvirt pool definition for CLVM/CLVM_NG", volgroupName); + LibvirtStoragePoolDef poolDef = new LibvirtStoragePoolDef( + LibvirtStoragePoolDef.PoolType.LOGICAL, + volgroupName, + uuid, + null, + volgroupName, + volgroupPath + ); + + try { + StoragePool pool = conn.storagePoolDefineXML(poolDef.toString(), 0); + logger.info("Created libvirt pool definition for CLVM/CLVM_NG VG: {} (pool will remain inactive)", volgroupName); + pool.setAutostart(1); + return pool; + } catch (LibvirtException e) { + logger.warn("Failed to define CLVM/CLVM_NG pool in libvirt: {}", e.getMessage()); + return null; + } + } + + private void setPoolCapacityFromVgStats(LibvirtStoragePool pool, long[] vgStats, String vgName) { + long capacity = vgStats[0]; + long available = vgStats[1]; + long used = capacity - available; + + pool.setCapacity(capacity); + pool.setAvailable(available); + pool.setUsed(used); + + logger.debug("CLVM/CLVM_NG pool {} - Capacity: {}, Used: {}, Available: {}", + vgName, toHumanReadableSize(capacity), toHumanReadableSize(used), toHumanReadableSize(available)); + } + + private long[] getVgStats(String vgName) { + Script getVgStats = new Script("vgs", 30000, logger); + getVgStats.add("--noheadings"); + getVgStats.add("--units", "b"); + getVgStats.add("--nosuffix"); + getVgStats.add("-o", "vg_size,vg_free"); + getVgStats.add(vgName); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = getVgStats.execute(parser); + + if (result != null) { + String errorMsg = "Failed to get statistics for volume group " + vgName + ": " + result; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + String output = parser.getLines().trim(); + String[] lines = output.split("\\n"); + String dataLine = null; + + for (String line : lines) { + line = line.trim(); + if (!line.isEmpty() && Character.isDigit(line.charAt(0))) { + dataLine = line; + break; + } + } + + if (dataLine == null) { + String errorMsg = "No valid data line found in vgs output for " + vgName + ": " + output; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + String[] stats = dataLine.split("\\s+"); + + if (stats.length < 2) { + String errorMsg = "Unexpected output from vgs command for " + vgName + ": " + dataLine; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + try { + long capacity = Long.parseLong(stats[0].trim()); + long available = Long.parseLong(stats[1].trim()); + return new long[]{capacity, available}; + } catch (NumberFormatException e) { + String errorMsg = "Failed to parse VG statistics for " + vgName + ": " + e.getMessage(); + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg, e); + } + } + + private KVMStoragePool createVirtualClvmPool(String uuid, String host, String path, StoragePoolType type, Map details) { + String volgroupName = path.replaceFirst("^/", ""); + String volgroupPath = "/dev/" + volgroupName; + + logger.info("Creating virtual CLVM/CLVM_NG pool {} without libvirt using direct LVM access", volgroupName); + + long[] vgStats = getVgStats(volgroupName); + + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, volgroupName, type, this, null); + pool.setLocalPath(volgroupPath); + setPoolCapacityFromVgStats(pool, vgStats, volgroupName); + + if (details != null) { + pool.setDetails(details); + } + + return pool; + } + + /** + * 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); + boolean isQcow2 = StoragePoolType.CLVM_NG.equals(pool.getType()); + KVMPhysicalDisk disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); + disk.setSize(vol.getInfo().allocation); + disk.setVirtualSize(isQcow2 ? getQcow2VirtualSize(vol.getPath()) : vol.getInfo().capacity); + disk.setFormat(isQcow2 ? 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 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; + } + + 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; + } + + /** + * 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 void verifyLvExistsInVg(String volumeUuid, String vgName) { + logger.debug("Checking if volume {} exists in VG {}", volumeUuid, vgName); + Script checkLvCmd = new Script("/usr/sbin/lvs", 30000, logger); + checkLvCmd.add("--noheadings"); + checkLvCmd.add("--unbuffered"); + checkLvCmd.add(vgName + "/" + volumeUuid); + String checkResult = checkLvCmd.execute(); + if (checkResult != null) { + 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); + if (!new File(lvPath).exists()) { + lvPath = handleMissingDeviceNode(volumeUuid, vgName, pool); + } + } + + return lvPath; + } + + private String tryDeviceMapperPath(String volumeUuid, String vgName) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + return "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + } + + private String handleMissingDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + if (StoragePoolType.CLVM_NG.equals(pool.getType()) && volumeUuid.startsWith("template-")) { + return activateTemplateAndGetPath(volumeUuid, vgName); + } + throw new CloudRuntimeException(String.format("Could not find volume %s in VG %s - volume exists in LVM but device node not accessible", volumeUuid, vgName)); + } + + private String activateTemplateAndGetPath(String volumeUuid, String vgName) { + logger.info("Template volume {} device node not found. Attempting to activate in shared mode.", volumeUuid); + String templateLvPath = "/dev/" + vgName + "/" + volumeUuid; + + try { + ensureTemplateLvInSharedMode(templateLvPath, false); + + String lvPath = findDeviceNodeAfterActivation(templateLvPath, volumeUuid, vgName); + + logger.info("Successfully activated template volume {} at {}", volumeUuid, lvPath); + return lvPath; + } catch (CloudRuntimeException e) { + throw new CloudRuntimeException(String.format("Failed to activate template volume %s in VG %s: %s", volumeUuid, vgName, e.getMessage()), e); + } + } + + private String findDeviceNodeAfterActivation(String templateLvPath, String volumeUuid, String vgName) { + File lvDevice = new File(templateLvPath); + String lvPath = templateLvPath; + + if (!lvDevice.exists()) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + lvPath = "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + lvDevice = new File(lvPath); + } + + if (!lvDevice.exists()) { + logger.error("Template volume {} still not accessible after activation attempt", volumeUuid); + throw new CloudRuntimeException(String.format("Could not activate template volume %s in VG %s - device node not accessible after activation", volumeUuid, vgName)); + } + + return lvPath; + } + + private void ensureTemplateAccessibility(String volumeUuid, String lvPath, KVMStoragePool pool) { + if (StoragePoolType.CLVM_NG.equals(pool.getType()) && volumeUuid.startsWith("template-")) { + logger.info("Detected template volume {}. Ensuring it's activated in shared mode.", volumeUuid); + ensureTemplateLvInSharedMode(lvPath, false); + } + } + + private long getClvmVolumeSize(String lvPath) { + try { + Script lvsCmd = new Script("/usr/sbin/lvs", 30000, logger); + lvsCmd.add("--noheadings"); + lvsCmd.add("--units"); + lvsCmd.add("b"); + lvsCmd.add("-o"); + lvsCmd.add("lv_size"); + lvsCmd.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = lvsCmd.execute(parser); + + String output = (result == null) ? parser.getLines() : result; + + if (output != null && !output.isEmpty()) { + String sizeStr = output.trim().replaceAll("[^0-9]", ""); + if (!sizeStr.isEmpty()) { + return Long.parseLong(sizeStr); + } + } + } catch (Exception sizeEx) { + logger.warn("Failed to get size for CLVM volume via lvs: {}", sizeEx.getMessage()); + File lvDevice = new File(lvPath); + if (lvDevice.isFile()) { + return lvDevice.length(); + } + } + return 0; + } + + private KVMPhysicalDisk createPhysicalDiskFromClvmLv(String lvPath, String volumeUuid, KVMStoragePool pool, long size) { + PhysicalDiskFormat diskFormat = StoragePoolType.CLVM_NG.equals(pool.getType()) + ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW; + + logger.debug("{} pool detected, setting disk format to {} for volume {}", pool.getType(), diskFormat, volumeUuid); + + KVMPhysicalDisk disk = new KVMPhysicalDisk(lvPath, volumeUuid, pool); + disk.setFormat(diskFormat); + disk.setSize(size); + disk.setVirtualSize(diskFormat == PhysicalDiskFormat.QCOW2 ? getQcow2VirtualSize(lvPath) : size); + + logger.info("Successfully accessed CLVM/CLVM_NG volume via direct block device: {} with format: {} and size: {} bytes", + lvPath, diskFormat, size); + return disk; + } + + /** + * Checks if a CLVM_NG QCOW2 volume has a backing file (template) and ensures it's activated in shared mode. + */ + private void ensureClvmNgBackingFileAccessible(String volumeName, KVMStoragePool pool) { + try { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + + logger.debug("Checking if CLVM_NG volume {} has a backing file that needs activation", volumePath); + + Script qemuImgInfo = new Script("qemu-img", Duration.millis(10000), logger); + qemuImgInfo.add("info"); + qemuImgInfo.add("--output=json"); + qemuImgInfo.add(volumePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImgInfo.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String jsonOutput = parser.getLines(); + + if (jsonOutput.contains("\"backing-filename\"")) { + int backingStart = jsonOutput.indexOf("\"backing-filename\""); + if (backingStart > 0) { + int valueStart = jsonOutput.indexOf(":", backingStart); + if (valueStart > 0) { + valueStart = jsonOutput.indexOf("\"", valueStart) + 1; + int valueEnd = jsonOutput.indexOf("\"", valueStart); + + if (valueEnd > valueStart) { + String backingFile = jsonOutput.substring(valueStart, valueEnd).trim(); + if (!backingFile.isEmpty() && backingFile.startsWith("/dev/")) { + logger.info("Volume {} has backing file: {}. Ensuring backing file is in shared mode.", volumePath, backingFile); + ensureTemplateLvInSharedMode(backingFile, false); + } + } + } + } + } else { + logger.debug("Volume {} does not have a backing file (full clone)", volumePath); + } + } + } catch (Exception e) { + logger.warn("Failed to check/activate backing file for volume {}: {}. VM deployment may fail if template is not accessible.", + volumeName, e.getMessage()); + } + } + + private String getClvmBackingFile(KVMPhysicalDisk template, KVMStoragePool destPool) { + String templateLvName = template.getName(); + KVMPhysicalDisk templateOnPrimary = null; + + try { + templateOnPrimary = destPool.getPhysicalDisk(templateLvName); + } catch (CloudRuntimeException e) { + logger.warn("Template {} not found on CLVM_NG pool {}.", templateLvName, destPool.getUuid()); + } + + if (templateOnPrimary != null) { + String backingFile = templateOnPrimary.getPath(); + logger.info("Using template on primary storage as backing file: {}", backingFile); + ensureTemplateLvInSharedMode(backingFile); + return backingFile; + } + + logger.error("Template {} should be on primary storage before creating volumes from it", templateLvName); + throw new CloudRuntimeException(String.format("Template not found on CLVM_NG primary storage: %s. Template must be copied to primary storage first.", templateLvName)); + } + + /** + * Ensures a template LV is activated in shared mode so multiple VMs can use it as a backing file. + */ + private void ensureTemplateLvInSharedMode(String templatePath, boolean throwOnFailure) { + try { + Script checkLvs = new Script("lvs", Duration.millis(30000), logger); + checkLvs.add("--noheadings"); + checkLvs.add("-o", "lv_attr"); + checkLvs.add(templatePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = checkLvs.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String lvAttr = parser.getLines().trim(); + if (lvAttr.length() >= 6) { + boolean isActive = (lvAttr.indexOf('a') >= 0); + boolean isShared = (lvAttr.indexOf('s') >= 0); + + if (!isShared || !isActive) { + logger.info("Template LV {} is not in shared mode (attr: {}). Activating in shared mode.", templatePath, lvAttr); + LibvirtComputingResource.setClvmVolumeToSharedMode(templatePath); + } else { + logger.debug("Template LV {} is already in shared mode (attr: {})", templatePath, lvAttr); + } + } + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = "Failed to check/ensure template LV shared mode for " + templatePath + ": " + e.getMessage(); + if (throwOnFailure) { + throw new CloudRuntimeException(errorMsg, e); + } else { + logger.warn(errorMsg, e); + } + } + } + + private void ensureTemplateLvInSharedMode(String templatePath) { + ensureTemplateLvInSharedMode(templatePath, false); + } + + private long getVgPhysicalExtentSize(String vgName) { + final long DEFAULT_PE_SIZE = 4 * 1024 * 1024L; + String warningMessage = String.format("Failed to get PE size for VG %s, defaulting to 4MiB", vgName); + + try { + Script vgDisplay = new Script("vgdisplay", 300000, logger); + vgDisplay.add("--units", "b"); + vgDisplay.add("-C"); + vgDisplay.add("--noheadings"); + vgDisplay.add("-o", "vg_extent_size"); + vgDisplay.add(vgName); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = vgDisplay.execute(parser); + + if (result != null) { + logger.warn("{}: {}", warningMessage, result); + return DEFAULT_PE_SIZE; + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + logger.warn("{}: empty output", warningMessage); + return DEFAULT_PE_SIZE; + } + + output = output.trim(); + if (output.endsWith("B")) { + output = output.substring(0, output.length() - 1).trim(); + } + + long peSize = Long.parseLong(output); + logger.debug("Physical Extent size for VG {} is {} bytes", vgName, peSize); + return peSize; + } catch (NumberFormatException e) { + logger.warn("{}: failed to parse PE size", warningMessage, e); + } catch (Exception e) { + logger.warn("{}: {}", warningMessage, e.getMessage()); + } + + logger.info("Using default PE size for VG {}: {} bytes (4 MiB)", vgName, DEFAULT_PE_SIZE); + return DEFAULT_PE_SIZE; + } + + /** + * Calculate LVM LV size for CLVM_NG volume allocation. + * {@code peSize} must be the Physical Extent size of the VG (from {@link #getVgPhysicalExtentSize}). + */ + private long calculateClvmNgLvSize(long virtualSize, long peSize) { + long clusterSize = 64 * 1024L; + long l2Multiplier = 4096L; + + long numDataClusters = (virtualSize + clusterSize - 1) / clusterSize; + long numL2Clusters = (numDataClusters + l2Multiplier - 1) / l2Multiplier; + long l2TableSize = numL2Clusters * clusterSize; + long refcountTableSize = l2TableSize; + + long headerOverhead = 2 * 1024 * 1024L; + long metadataOverhead = l2TableSize + refcountTableSize + headerOverhead; + long targetSize = virtualSize + metadataOverhead; + long roundedSize = ((targetSize + peSize - 1) / peSize) * peSize; + long virtualSizeGiB = virtualSize / (1024 * 1024 * 1024L); + long overheadMiB = metadataOverhead / (1024 * 1024L); + + logger.info("Calculated volume LV size: {} bytes (virtual: {} GiB, QCOW2 metadata overhead: {} MiB, rounded to {} PEs, PE size = {} bytes)", + roundedSize, virtualSizeGiB, overheadMiB, roundedSize / peSize, peSize); + + return roundedSize; + } + + private long getQcow2VirtualSize(String imagePath) { + Script qemuImg = new Script("qemu-img", 300000, logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add("-U"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 virtual size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("virtual-size").getAsLong(); + } + + private long getQcow2PhysicalSize(String imagePath) { + Script qemuImg = new Script("qemu-img", Duration.millis(300000), logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 physical size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("actual-size").getAsLong(); + } + + private KVMPhysicalDisk createClvmNgDiskWithBacking(String volumeUuid, int timeout, long virtualSize, String backingFile, + KVMStoragePool pool, Storage.ProvisioningType provisioningType) { + String vgName = getVgName(pool.getLocalPath()); + // Query PE size once and reuse for both the QCOW2 virtual-size alignment and the + long peSize = getVgPhysicalExtentSize(vgName); + long peAlignedVirtualSize = ((virtualSize + peSize - 1) / peSize) * peSize; + long lvSize = calculateClvmNgLvSize(peAlignedVirtualSize, peSize); + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + logger.debug("Creating CLVM_NG volume {} with LV size {} bytes (requested virtual: {} bytes, PE-aligned virtual: {} bytes, provisioning: {})", + volumeUuid, lvSize, virtualSize, peAlignedVirtualSize, provisioningType); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeUuid); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG volume: " + result); + } + + Script qemuImg = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImg.add("create"); + qemuImg.add("-f", "qcow2"); + + StringBuilder qcow2Options = new StringBuilder(); + String preallocation = (provisioningType == Storage.ProvisioningType.THIN) ? "off" : "metadata"; + qcow2Options.append("preallocation=").append(preallocation); + qcow2Options.append(",extended_l2=on"); + qcow2Options.append(",cluster_size=64k"); + + if (backingFile != null && !backingFile.isEmpty()) { + qcow2Options.append(",backing_file=").append(backingFile); + qcow2Options.append(",backing_fmt=qcow2"); + logger.debug("Creating CLVM_NG volume with backing file: {}", backingFile); + } + + qemuImg.add("-o", qcow2Options.toString()); + qemuImg.add(volumePath); + qemuImg.add(peAlignedVirtualSize + ""); + + result = qemuImg.execute(); + if (result != null) { + removeLvOnFailure(volumePath, timeout); + throw new CloudRuntimeException("Failed to create QCOW2 on CLVM_NG volume: " + result); + } + + long actualSize = getClvmVolumeSize(volumePath); + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeUuid, pool); + disk.setFormat(PhysicalDiskFormat.QCOW2); + disk.setSize(actualSize); + disk.setVirtualSize(peAlignedVirtualSize); + + logger.info("Successfully created CLVM_NG volume {} (LV size: {}, PE-aligned virtual size: {}, provisioning: {}, preallocation: {})", + volumeUuid, lvSize, peAlignedVirtualSize, provisioningType, preallocation); + + return disk; + } + + private boolean lvExists(String lvPath) { + Script checkLv = new Script("lvs", Duration.millis(30000), logger); + checkLv.add("--noheadings"); + checkLv.add("--unbuffered"); + checkLv.add(lvPath); + return checkLv.execute() == null; + } + + private void removeLvOnFailure(String lvPath, int timeout) { + Script lvremove = new Script("lvremove", Duration.millis(timeout), logger); + lvremove.add("-f"); + lvremove.add(lvPath); + lvremove.execute(); + } + + private KVMPhysicalDisk createClvmVolume(String volumeName, long size, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + int timeout = 30000; + + logger.info("Creating CLVM volume {} in VG {} with size {} bytes", volumeName, vgName, size); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeName); + lvcreate.add("-L", size + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create CLVM volume: " + result); + } + + logger.info("Successfully created CLVM volume {} at {} with size {}", volumeName, volumePath, toHumanReadableSize(size)); + + long actualSize = getClvmVolumeSize(volumePath); + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeName, pool); + disk.setFormat(PhysicalDiskFormat.RAW); + disk.setSize(actualSize); + disk.setVirtualSize(actualSize); + + return disk; + } + + @Override + public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.ImageFormat format) { + logger.info("CLVM/CLVM_NG pool detected - using direct LVM cleanup with secure zero-fill for volume {}", uuid); + return cleanupCLVMVolume(uuid, pool); + } + + /** + * Clean up CLVM volume and its snapshots directly using LVM commands. + */ + private boolean cleanupCLVMVolume(String uuid, KVMStoragePool pool) { + logger.info("Starting direct LVM cleanup for CLVM volume: {} in pool: {}", uuid, pool.getUuid()); + + try { + String sourceDir = pool.getLocalPath(); + if (sourceDir == null || sourceDir.isEmpty()) { + logger.debug("Source directory is null or empty, cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); + return true; + } + String vgName = getVgName(sourceDir); + logger.info("Determined VG name: {} for pool: {}", vgName, pool.getUuid()); + + if (vgName == null || vgName.isEmpty()) { + logger.warn("Cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); + return true; + } + + String lvPath = "/dev/" + vgName + "/" + uuid; + logger.debug("Volume path: {}", lvPath); + + Script checkLvs = new Script("lvs", 30000, logger); + checkLvs.add("--noheadings"); + checkLvs.add("--unbuffered"); + checkLvs.add(lvPath); + + logger.info("Checking if volume exists: lvs --noheadings --unbuffered {}", lvPath); + String checkResult = checkLvs.execute(); + + if (checkResult != null) { + logger.info("CLVM volume {} does not exist in LVM (check returned: {}), considering it as already deleted", uuid, checkResult); + return true; + } + + logger.info("Volume {} exists, proceeding with cleanup", uuid); + + boolean secureZeroFillEnabled = shouldSecureZeroFill(pool); + + if (secureZeroFillEnabled) { + logger.info("Step 1: Zero-filling volume {} for security", uuid); + secureZeroFillVolume(lvPath, uuid); + } else { + logger.info("Secure zero-fill is disabled, skipping zero-filling for volume {}", uuid); + } + + logger.info("Step 2: Removing volume {}", uuid); + Script removeLv = new Script("lvremove", 30000, logger); + removeLv.add("-f"); + removeLv.add(lvPath); + + logger.info("Executing command: lvremove -f {}", lvPath); + String removeResult = removeLv.execute(); + + if (removeResult == null) { + logger.info("Successfully removed CLVM volume {} using direct LVM cleanup", uuid); + return true; + } else { + logger.warn("Command 'lvremove -f {}' returned error: {}", lvPath, removeResult); + if (removeResult.contains("not found") || removeResult.contains("Failed to find")) { + logger.info("CLVM volume {} not found during cleanup, considering it as already deleted", uuid); + return true; + } + return false; + } + } catch (Exception ex) { + logger.error("Exception during CLVM volume cleanup for {}: {}", uuid, ex.getMessage(), ex); + return true; + } + } + + private boolean shouldSecureZeroFill(KVMStoragePool pool) { + Map details = pool.getDetails(); + String secureZeroFillStr = (details != null) ? details.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL) : null; + return Boolean.parseBoolean(secureZeroFillStr); + } + + /** + * Securely zero-fill a volume before deletion to prevent data leakage. + * Uses blkdiscard (fast TRIM) as primary method, with dd zero-fill as fallback. + */ + private void secureZeroFillVolume(String lvPath, String volumeUuid) { + logger.info("Starting secure zero-fill for CLVM volume: {} at path: {}", volumeUuid, lvPath); + + boolean blkdiscardSuccess = false; + + try { + Script blkdiscard = new Script("blkdiscard", 300000, logger); + blkdiscard.add("-f"); + blkdiscard.add(lvPath); + + String result = blkdiscard.execute(); + if (result == null) { + logger.info("Successfully zero-filled CLVM volume {} using blkdiscard (TRIM)", volumeUuid); + blkdiscardSuccess = true; + } else { + if (result.contains("Operation not supported") || result.contains("BLKDISCARD ioctl failed")) { + logger.info("blkdiscard not supported for volume {} (device doesn't support TRIM/DISCARD), using dd fallback", volumeUuid); + } else { + logger.warn("blkdiscard failed for volume {}: {}, will try dd fallback", volumeUuid, result); + } + } + } catch (Exception e) { + logger.warn("Exception during blkdiscard for volume {}: {}, will try dd fallback", volumeUuid, e.getMessage()); + } + + if (!blkdiscardSuccess) { + logger.info("Attempting zero-fill using dd for CLVM volume: {}", volumeUuid); + try { + String command = String.format( + "nice -n 19 ionice -c 2 -n 7 dd if=/dev/zero of=%s bs=1M oflag=direct 2>&1 || true", + lvPath + ); + + Script ddZeroFill = new Script("/bin/bash", 3600000, logger); + ddZeroFill.add("-c"); + ddZeroFill.add(command); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String ddResult = ddZeroFill.execute(parser); + String output = parser.getLines(); + + if (output != null && (output.contains("copied") || output.contains("records in") || + output.contains("No space left on device"))) { + logger.info("Successfully zero-filled CLVM volume {} using dd", volumeUuid); + } else if (ddResult == null) { + logger.info("Zero-fill completed for CLVM volume {}", volumeUuid); + } else { + logger.warn("dd zero-fill for volume {} completed with output: {}", volumeUuid, + output != null ? output : ddResult); + } + } catch (Exception e) { + logger.warn("Failed to zero-fill CLVM volume {} before deletion: {}. Proceeding with deletion anyway.", + volumeUuid, e.getMessage()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java index 3e35ed9476b..a8207cec3fa 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java @@ -33,6 +33,7 @@ import com.cloud.storage.Storage.StoragePoolType; public interface KVMStoragePool { + public static final String CLVM_SECURE_ZERO_FILL = "clvmsecurezerofill"; long HeartBeatUpdateTimeoutInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HEARTBEAT_UPDATE_TIMEOUT); long HeartBeatUpdateFreqInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_FREQUENCY); long HeartBeatCheckerTimeoutInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_CHECKER_TIMEOUT); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java index 35cc864268c..996398a286f 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java @@ -72,7 +72,9 @@ public class KVMStoragePoolManager { private void addStoragePool(String uuid, StoragePoolInformation pool) { synchronized (_storagePools) { - if (!_storagePools.containsKey(uuid)) { + // Insert on first registration; on subsequent calls (e.g. ModifyStoragePoolCommand) + // overwrite when new details are present so config changes are reflected + if (!_storagePools.containsKey(uuid) || MapUtils.isNotEmpty(pool.getDetails())) { _storagePools.put(uuid, pool); } } @@ -81,6 +83,10 @@ public class KVMStoragePoolManager { public KVMStoragePoolManager(StorageLayer storagelayer, KVMHAMonitor monitor) { this._haMonitor = monitor; this._storageMapper.put("libvirt", new LibvirtStorageAdaptor(storagelayer)); + // Register CLVM/CLVM_NG adaptor explicitly for both types (one shared instance) + ClvmStorageAdaptor clvmAdaptor = new ClvmStorageAdaptor(storagelayer); + this._storageMapper.put(StoragePoolType.CLVM.toString(), clvmAdaptor); + this._storageMapper.put(StoragePoolType.CLVM_NG.toString(), clvmAdaptor); // add other storage adaptors manually here // add any adaptors that wish to register themselves via call to adaptor.getStoragePoolType() @@ -92,8 +98,8 @@ public class KVMStoragePoolManager { logger.debug("Skipping registration of abstract class / interface " + storageAdaptorClass.getName()); continue; } - if (storageAdaptorClass.isAssignableFrom(LibvirtStorageAdaptor.class)) { - logger.debug("Skipping re-registration of LibvirtStorageAdaptor"); + if (storageAdaptorClass == LibvirtStorageAdaptor.class || storageAdaptorClass == ClvmStorageAdaptor.class) { + logger.debug("Skipping re-registration of explicitly registered adaptor: {}", storageAdaptorClass.getSimpleName()); continue; } try { @@ -288,19 +294,45 @@ public class KVMStoragePoolManager { } if (pool instanceof LibvirtStoragePool) { - addPoolDetails(uuid, (LibvirtStoragePool) pool); + LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; + addPoolDetails(uuid, libvirtPool); + ((LibvirtStoragePool) pool).setType(type); + updatePoolTypeIfApplicable(libvirtPool, pool, type, uuid); } return pool; } + private void updatePoolTypeIfApplicable(LibvirtStoragePool libvirtPool, KVMStoragePool pool, + StoragePoolType type, String uuid) { + StoragePoolType correctType = type; + if (correctType == null || correctType == StoragePoolType.CLVM) { + StoragePoolInformation info = _storagePools.get(uuid); + if (info != null && info.getPoolType() != null) { + correctType = info.getPoolType(); + } + } + + if (correctType != null && correctType != pool.getType() && + (correctType == StoragePoolType.CLVM || correctType == StoragePoolType.CLVM_NG) && + (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG)) { + logger.debug("Correcting pool type from {} to {} for pool {} based on caller/cached information", + pool.getType(), correctType, uuid); + libvirtPool.setType(correctType); + } + } + /** * As the class {@link LibvirtStoragePool} is constrained to the {@link org.libvirt.StoragePool} class, there is no way of saving a generic parameter such as the details, hence, * this method was created to always make available the details of libvirt primary storages for when they are needed. */ private void addPoolDetails(String uuid, LibvirtStoragePool pool) { StoragePoolInformation storagePoolInformation = _storagePools.get(uuid); + if (storagePoolInformation == null) { + logger.warn("No cached StoragePoolInformation found for pool UUID {}, pool details will not be set.", uuid); + return; + } Map details = storagePoolInformation.getDetails(); if (MapUtils.isNotEmpty(details)) { @@ -454,6 +486,10 @@ public class KVMStoragePoolManager { return adaptor.createDiskFromTemplate(template, name, PhysicalDiskFormat.RAW, provisioningType, size, destPool, timeout, passphrase); + } else if (destPool.getType() == StoragePoolType.CLVM_NG) { + return adaptor.createDiskFromTemplate(template, name, + PhysicalDiskFormat.QCOW2, provisioningType, + size, destPool, timeout, passphrase); } else if (template.getFormat() == PhysicalDiskFormat.DIR) { return adaptor.createDiskFromTemplate(template, name, PhysicalDiskFormat.DIR, provisioningType, @@ -495,6 +531,11 @@ public class KVMStoragePoolManager { return adaptor.createTemplateFromDirectDownloadFile(templateFilePath, destTemplatePath, destPool, format, timeout); } + public void createTemplateOnClvmNg(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + StorageAdaptor adaptor = getStorageAdaptor(pool.getType()); + adaptor.createTemplate(templatePath, templateUuid, timeout, pool); + } + public Ternary, String> prepareStorageClient(StoragePoolType type, String uuid, Map details) { StorageAdaptor adaptor = getStorageAdaptor(type); return adaptor.prepareStorageClient(uuid, details); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index f95ebff5326..4a77f7e9e19 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -55,6 +55,7 @@ import javax.xml.xpath.XPathFactory; import com.cloud.agent.api.Command; import com.cloud.hypervisor.kvm.resource.LibvirtXMLParser; +import com.cloud.storage.clvm.ClvmPoolManager; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -225,6 +226,26 @@ public class KVMStorageProcessor implements StorageProcessor { " \n" + ""; + private static final String DUMMY_VM_XML_BLOCK = "\n" + + " %s\n" + + " 256\n" + + " 256\n" + + " 1\n" + + " \n" + + " hvm\n" + + " \n" + + " \n" + + " \n" + + " %s\n" + + " \n" + + " \n"+ + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + public KVMStorageProcessor(final KVMStoragePoolManager storagePoolMgr, final LibvirtComputingResource resource) { this.storagePoolMgr = storagePoolMgr; @@ -347,15 +368,28 @@ public class KVMStorageProcessor implements StorageProcessor { path = destTempl.getUuid(); } - if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { - logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); - return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); - } + if (primaryPool.getType() == StoragePoolType.CLVM_NG) { + logger.info("Copying template {} to CLVM_NG pool {}", + destTempl.getUuid(), primaryPool.getUuid()); - primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + try { + storagePoolMgr.createTemplateOnClvmNg(tmplVol.getPath(), path, cmd.getWaitInMillSeconds(), primaryPool); + primaryVol = primaryPool.getPhysicalDisk("template-" + path); + } catch (Exception e) { + logger.error("Failed to create CLVM_NG template: {}", e.getMessage(), e); + return new PrimaryStorageDownloadAnswer("Failed to create CLVM_NG template: " + e.getMessage()); + } + } else { + if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { + logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); + } - if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { - logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + + if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { + logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + } } } else { primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, UUID.randomUUID().toString(), primaryPool, cmd.getWaitInMillSeconds()); @@ -376,7 +410,8 @@ public class KVMStorageProcessor implements StorageProcessor { StoragePoolType.RBD, StoragePoolType.PowerFlex, StoragePoolType.Linstor, - StoragePoolType.FiberChannel).contains(primaryPool.getType())) { + StoragePoolType.FiberChannel, + StoragePoolType.CLVM).contains(primaryPool.getType())) { newTemplate.setFormat(ImageFormat.RAW); } else { newTemplate.setFormat(ImageFormat.QCOW2); @@ -589,7 +624,9 @@ public class KVMStorageProcessor implements StorageProcessor { String path = details != null ? details.get(DiskTO.IQN) : null; - storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details); + if (!ClvmPoolManager.isClvmPoolType(primaryStore.getPoolType())) { + storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details); + } final String volumeName = UUID.randomUUID().toString(); @@ -618,7 +655,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 (!ClvmPoolManager.isClvmPoolType(primaryStore.getPoolType())) { + storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path); + } final VolumeObjectTO newVol = new VolumeObjectTO(); @@ -1118,7 +1157,14 @@ public class KVMStorageProcessor implements StorageProcessor { } } else { final Script command = new Script(_manageSnapshotPath, cmd.getWaitInMillSeconds(), logger); - command.add("-b", isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath()); + String backupPath; + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + backupPath = snapshotDisk.getPath(); + logger.debug("Using snapshotDisk path for CLVM/CLVM_NG backup: " + backupPath); + } else { + backupPath = isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath(); + } + command.add("-b", backupPath); command.add(NAME_OPTION, snapshotName); command.add("-p", snapshotDestPath); @@ -1163,6 +1209,90 @@ public class KVMStorageProcessor implements StorageProcessor { } } + /** + * Parse CLVM/CLVM_NG snapshot path and compute MD5 hash. + * Snapshot path format: /dev/vgname/volumeuuid/snapshotuuid + * + * @param snapshotPath The snapshot path from database + * @param poolType Storage pool type (for logging) + * @return Array of [vgName, volumeUuid, snapshotUuid, md5Hash] or null if invalid + */ + private String[] parseClvmSnapshotPath(String snapshotPath, StoragePoolType poolType) { + String[] pathParts = snapshotPath.split("/"); + if (pathParts.length < 5) { + logger.warn("Invalid {} snapshot path format: {}, expected format: /dev/vgname/volume-uuid/snapshot-uuid", + poolType, snapshotPath); + return null; + } + + String vgName = pathParts[2]; + String volumeUuid = pathParts[3]; + String snapshotUuid = pathParts[4]; + + logger.info("Parsed {} snapshot path - VG: {}, Volume: {}, Snapshot: {}", + poolType, vgName, volumeUuid, snapshotUuid); + + String md5Hash = computeMd5Hash(snapshotUuid); + logger.debug("Computed MD5 hash for snapshot UUID {}: {}", snapshotUuid, md5Hash); + + return new String[]{vgName, volumeUuid, snapshotUuid, md5Hash}; + } + + /** + * Delete a CLVM or CLVM_NG snapshot using managesnapshot.sh script. + * For both CLVM and CLVM_NG, the snapshot path stored in DB is: /dev/vgname/volumeuuid/snapshotuuid + * The script handles MD5 transformation and pool-specific deletion commands internally: + * - CLVM: Uses lvremove to delete LVM snapshot + * - CLVM_NG: Uses qemu-img snapshot -d to delete QCOW2 internal snapshot + * This approach is consistent with snapshot creation and backup which also use the script. + * + * @param snapshotPath The snapshot path from database + * @param poolType Storage pool type (CLVM or CLVM_NG) + * @param checkExistence If true, checks if snapshot exists before cleanup (for explicit deletion) + * If false, always performs cleanup (for post-backup cleanup) + * @return true if cleanup was performed, false if snapshot didn't exist (when checkExistence=true) + */ + private boolean deleteClvmSnapshot(String snapshotPath, StoragePoolType poolType, boolean checkExistence) { + logger.info("Starting {} snapshot deletion for path: {}, checkExistence: {}", poolType, snapshotPath, checkExistence); + + try { + String[] parsed = parseClvmSnapshotPath(snapshotPath, poolType); + if (parsed == null) { + return false; + } + + String vgName = parsed[0]; + String volumeUuid = parsed[1]; + String snapshotUuid = parsed[2]; + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + // Use managesnapshot.sh script for deletion (consistent with create/backup) + // Script handles MD5 transformation and pool-specific commands internally + Script deleteCommand = new Script(_manageSnapshotPath, 30000, logger); + deleteCommand.add("-d", volumePath); + deleteCommand.add("-n", snapshotUuid); + + logger.info("Executing: managesnapshot.sh -d {} -n {}", volumePath, snapshotUuid); + String result = deleteCommand.execute(); + + if (result == null) { + logger.info("Successfully deleted {} snapshot: {}", poolType, snapshotPath); + return true; + } else { + if (checkExistence && result.contains("does not exist")) { + logger.info("{} snapshot {} already deleted, no cleanup needed", poolType, snapshotPath); + return true; + } + logger.warn("Failed to delete {} snapshot {}: {}", poolType, snapshotPath, result); + return false; + } + + } catch (Exception ex) { + logger.error("Exception while deleting {} snapshot {}", poolType, snapshotPath, ex); + return false; + } + } + private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObjectTO snapshot, KVMStoragePool primaryPool) { String snapshotPath = snapshot.getPath(); @@ -1175,7 +1305,19 @@ public class KVMStorageProcessor implements StorageProcessor { if ((backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) && deleteSnapshotOnPrimary) { try { - Files.deleteIfExists(Paths.get(snapshotPath)); + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + // Both CLVM and CLVM_NG use the same deletion method via managesnapshot.sh script + boolean cleanedUp = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), false); + if (!cleanedUp) { + String[] parsedPath = parseClvmSnapshotPath(snapshotPath, primaryPool.getType()); + String snapMd5 = (parsedPath != null) ? computeMd5Hash(parsedPath[2]) : computeMd5Hash(snapshotPath); + logger.warn("Deletion of Snapshot: {} on primary store may have failed as it doesn't exist: {} " + + "(MD5 of snapshot UUID: {} - admins can use this to manually locate and delete the LV via managesnapshot.sh or lvremove)", + primaryPool.getType(), snapshotPath, snapMd5); + } + } else { + Files.deleteIfExists(Paths.get(snapshotPath)); + } } catch (IOException ex) { logger.error("Failed to delete snapshot [{}] on primary storage [{}].", snapshot.getId(), snapshot.getName(), ex); } @@ -1184,6 +1326,26 @@ public class KVMStorageProcessor implements StorageProcessor { } } + + /** + * Compute MD5 hash of a string, matching what managesnapshot.sh does: + * echo "${snapshot}" | md5sum -t | awk '{ print $1 }' + */ + private String computeMd5Hash(String input) { + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] array = md.digest((input + "\n").getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte b : array) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + logger.error("Failed to compute MD5 hash for: {}", input, e); + return input; + } + } + protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params, DataStoreTO store) throws LibvirtException, InternalErrorException { DiskDef iso = new DiskDef(); @@ -1523,6 +1685,10 @@ public class KVMStorageProcessor implements StorageProcessor { if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) { diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); } + } else if (attachingPool.getType() == StoragePoolType.CLVM_NG) { + // CLVM_NG uses QCOW2 format on block devices + diskdef.defBlockBasedDisk(attachingDisk.getPath(), devId, busT); + diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); } else if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) { diskdef.defFileBasedDisk(attachingDisk.getPath(), devId, busT, DiskDef.DiskFmtType.QCOW2); } else if (attachingDisk.getFormat() == PhysicalDiskFormat.RAW) { @@ -1738,13 +1904,22 @@ public class KVMStorageProcessor implements StorageProcessor { primaryPool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); disksize = volume.getSize(); PhysicalDiskFormat format; - if (volume.getFormat() == null || StoragePoolType.RBD.equals(primaryStore.getPoolType())) { + + MigrationOptions migrationOptions = volume.getMigrationOptions(); + boolean useDstPoolFormat = useDestPoolFormat(migrationOptions, primaryStore); + + if (volume.getFormat() == null || StoragePoolType.RBD.equals(primaryStore.getPoolType()) || useDstPoolFormat) { format = primaryPool.getDefaultFormat(); + if (useDstPoolFormat) { + logger.debug("Using destination pool default format {} for volume {} due to CLVM migration (src: {}, dst: {})", + format, volume.getUuid(), + migrationOptions != null ? migrationOptions.getSrcPoolType() : "unknown", + primaryStore.getPoolType()); + } } else { format = PhysicalDiskFormat.valueOf(volume.getFormat().toString().toUpperCase()); } - MigrationOptions migrationOptions = volume.getMigrationOptions(); if (migrationOptions != null) { int timeout = migrationOptions.getTimeout(); @@ -1769,7 +1944,11 @@ public class KVMStorageProcessor implements StorageProcessor { format = vol.getFormat(); } } - newVol.setSize(volume.getSize()); + if (StoragePoolType.CLVM_NG.equals(primaryStore.getPoolType()) && vol != null && vol.getVirtualSize() > 0) { + newVol.setSize(vol.getVirtualSize()); + } else { + newVol.setSize(volume.getSize()); + } newVol.setFormat(ImageFormat.valueOf(format.toString().toUpperCase())); return new CreateObjectAnswer(newVol); @@ -1781,6 +1960,29 @@ public class KVMStorageProcessor implements StorageProcessor { } } + /** + * For migration involving CLVM (RAW format), use destination pool's default format + * CLVM uses RAW format which may not match destination pool's format (e.g., NFS uses QCOW2) + * This specifically handles: + * - CLVM (RAW) -> NFS/Local/CLVM_NG (QCOW2) + * - NFS/Local/CLVM_NG (QCOW2) -> CLVM (RAW) + * @param migrationOptions + * @param primaryStore + * @return + */ + private boolean useDestPoolFormat(MigrationOptions migrationOptions, PrimaryDataStoreTO primaryStore) { + boolean useDstPoolFormat = false; + if (migrationOptions != null && migrationOptions.getSrcPoolType() != null) { + StoragePoolType srcPoolType = migrationOptions.getSrcPoolType(); + StoragePoolType dstPoolType = primaryStore.getPoolType(); + + if (srcPoolType != dstPoolType) { + useDstPoolFormat = (srcPoolType == StoragePoolType.CLVM || dstPoolType == StoragePoolType.CLVM); + } + } + return useDstPoolFormat; + } + /** * XML to take disk-only snapshot of the VM.

* 1st parameter: snapshot's name;
@@ -1870,10 +2072,22 @@ public class KVMStorageProcessor implements StorageProcessor { if (snapshotSize != null) { newSnapshot.setPhysicalSize(snapshotSize); } - } else if (primaryPool.getType() == StoragePoolType.CLVM) { - CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName); - if (result != null) return result; - newSnapshot.setPath(snapshotPath); + } else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + if (primaryPool.getType() == StoragePoolType.CLVM_NG && snapshotTO.isKvmIncrementalSnapshot()) { + if (secondaryPool == null) { + String errorMsg = String.format("Incremental snapshots for CLVM_NG require secondary storage. " + + "Please configure secondary storage or disable incremental snapshots for volume [%s].", volume.getName()); + logger.error(errorMsg); + return new CreateObjectAnswer(errorMsg); + } + logger.info("Taking incremental snapshot of CLVM_NG volume [{}] using QCOW2 backup to secondary storage.", volume.getName()); + newSnapshot = takeIncrementalVolumeSnapshotOfStoppedVm(snapshotTO, primaryPool, secondaryPool, + imageStoreTo.getUrl(), snapshotName, volume, conn, cmd.getWait()); + } else { + CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName); + if (result != null) return result; + newSnapshot.setPath(snapshotPath); + } } else { if (snapshotTO.isKvmIncrementalSnapshot()) { newSnapshot = takeIncrementalVolumeSnapshotOfStoppedVm(snapshotTO, primaryPool, secondaryPool, imageStoreTo != null ? imageStoreTo.getUrl() : null, snapshotName, volume, conn, cmd.getWait()); @@ -1946,7 +2160,11 @@ public class KVMStorageProcessor implements StorageProcessor { String machine = resource.isGuestAarch64() ? LibvirtComputingResource.VIRT : LibvirtComputingResource.PC; String cpuArch = resource.getGuestCpuArch() != null ? resource.getGuestCpuArch() : "x86_64"; - return String.format(DUMMY_VM_XML, vmName, cpuArch, machine, resource.getHypervisorPath(), primaryPool.getLocalPathFor(volumeObjectTo.getPath())); + String volumePath = primaryPool.getLocalPathFor(volumeObjectTo.getPath()); + boolean isClvmNg = StoragePoolType.CLVM_NG == primaryPool.getType(); + + String xmlTemplate = isClvmNg ? DUMMY_VM_XML_BLOCK : DUMMY_VM_XML; + return String.format(xmlTemplate, vmName, cpuArch, machine, resource.getHypervisorPath(), volumePath); } private SnapshotObjectTO takeIncrementalVolumeSnapshotOfRunningVm(SnapshotObjectTO snapshotObjectTO, KVMStoragePool primaryPool, KVMStoragePool secondaryPool, @@ -2667,11 +2885,13 @@ public class KVMStorageProcessor implements StorageProcessor { final PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO)vol.getDataStore(); try { final KVMStoragePool pool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); - try { - pool.getPhysicalDisk(vol.getPath()); - } catch (final Exception e) { - logger.debug(String.format("can't find volume: %s, return true", vol)); - return new Answer(null); + if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG) { + try { + pool.getPhysicalDisk(vol.getPath()); + } catch (final Exception e) { + logger.debug(String.format("can't find volume: %s, return true", vol)); + return new Answer(null); + } } pool.deletePhysicalDisk(vol.getPath(), vol.getFormat()); return new Answer(null); @@ -2900,6 +3120,25 @@ public class KVMStorageProcessor implements StorageProcessor { if (snapshotTO.isKvmIncrementalSnapshot()) { deleteCheckpoint(snapshotTO); } + } else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + // For CLVM/CLVM_NG, snapshots are typically already deleted from primary storage during backup + // via deleteSnapshotOnPrimary in the backupSnapshot finally block. + // This is called when the user explicitly deletes the snapshot via UI/API. + // We check if the snapshot still exists and clean it up if needed. + logger.info("Processing CLVM/CLVM_NG snapshot deletion (id={}, name={}, path={}) on primary storage", + snapshotTO.getId(), snapshotTO.getName(), snapshotTO.getPath()); + + String snapshotPath = snapshotTO.getPath(); + if (snapshotPath != null && !snapshotPath.isEmpty()) { + boolean wasDeleted = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), true); + if (wasDeleted) { + logger.info("Successfully cleaned up {} snapshot {} from primary storage", primaryPool.getType(), snapshotName); + } else { + logger.info("{} snapshot {} was already deleted from primary storage during backup, no cleanup needed", primaryPool.getType(), snapshotName); + } + } else { + logger.debug("{} snapshot path is null or empty, assuming already cleaned up", primaryPool.getType()); + } } else { logger.warn("Operation not implemented for storage pool type of " + primaryPool.getType().toString()); throw new InternalErrorException("Operation not implemented for storage pool type of " + primaryPool.getType().toString()); @@ -3175,7 +3414,8 @@ public class KVMStorageProcessor implements StorageProcessor { StoragePoolType.RBD, StoragePoolType.PowerFlex, StoragePoolType.Linstor, - StoragePoolType.FiberChannel).contains(poolType)) { + StoragePoolType.FiberChannel, + StoragePoolType.CLVM).contains(poolType)) { return ImageFormat.RAW; } else { return ImageFormat.QCOW2; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index a03daeb197b..ed159c92790 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -231,6 +231,11 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } public StorageVol getVolume(StoragePool pool, String volName) { + if (pool == null) { + logger.debug("LibVirt StoragePool is null (likely CLVM/CLVM_NG virtual pool), cannot lookup volume {} via libvirt", volName); + return null; + } + StorageVol vol = null; try { @@ -254,9 +259,12 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { vol = pool.storageVolLookupByName(volName); - logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool"); + if (vol != null) { + logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool"); + } } catch (LibvirtException e) { - throw new CloudRuntimeException("Could not find volume " + volName + ": " + e.getMessage()); + logger.debug("Volume " + volName + " still not found after pool refresh: " + e.getMessage()); + return null; } } @@ -349,38 +357,6 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } } - private StoragePool createCLVMStoragePool(Connect conn, String uuid, String host, String path) { - - String volgroupPath = "/dev/" + path; - String volgroupName = path; - volgroupName = volgroupName.replaceFirst("/", ""); - - LibvirtStoragePoolDef spd = new LibvirtStoragePoolDef(PoolType.LOGICAL, volgroupName, uuid, host, volgroupPath, volgroupPath); - StoragePool sp = null; - try { - logger.debug(spd.toString()); - sp = conn.storagePoolCreateXML(spd.toString(), 0); - return sp; - } catch (LibvirtException e) { - logger.error(e.toString()); - if (sp != null) { - try { - if (sp.isPersistent() == 1) { - sp.destroy(); - sp.undefine(); - } else { - sp.destroy(); - } - sp.free(); - } catch (LibvirtException l) { - logger.debug("Failed to define clvm storage pool with: " + l.toString()); - } - } - return null; - } - - } - private List getNFSMountOptsFromDetails(StoragePoolType type, Map details) { List nfsMountOpts = null; if (!type.equals(StoragePoolType.NetworkFilesystem) || details == null) { @@ -580,14 +556,11 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { Connect conn = LibvirtConnection.getConnection(); storage = conn.storagePoolLookupByUUIDString(uuid); - if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { - logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); - storage.create(0); - } LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); if (spd == null) { throw new CloudRuntimeException("Unable to parse the storage pool definition for storage pool " + uuid); } + StoragePoolType type = null; if (spd.getPoolType() == LibvirtStoragePoolDef.PoolType.NETFS) { type = StoragePoolType.NetworkFilesystem; @@ -603,6 +576,12 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { type = StoragePoolType.PowerFlex; } + // Activate pool if not running + if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { + logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); + storage.create(0); + } + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); if (pool.getType() != StoragePoolType.RBD && pool.getType() != StoragePoolType.PowerFlex) @@ -640,15 +619,17 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { logger.info("Asking libvirt to refresh storage pool " + uuid); pool.refresh(); } + pool.setCapacity(storage.getInfo().capacity); pool.setUsed(storage.getInfo().allocation); - updateLocalPoolIops(pool); pool.setAvailable(storage.getInfo().available); - logger.debug("Successfully refreshed pool " + uuid + - " Capacity: " + toHumanReadableSize(storage.getInfo().capacity) + - " Used: " + toHumanReadableSize(storage.getInfo().allocation) + - " Available: " + toHumanReadableSize(storage.getInfo().available)); + logger.debug("Successfully refreshed pool {} Capacity: {} Used: {} Available: {}", + uuid, toHumanReadableSize(storage.getInfo().capacity), + toHumanReadableSize(storage.getInfo().allocation), + toHumanReadableSize(storage.getInfo().available)); + + updateLocalPoolIops(pool); return pool; } catch (LibvirtException e) { @@ -663,6 +644,10 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol == null) { + throw new CloudRuntimeException("Volume " + volumeUuid + " not found in libvirt pool"); + } + KVMPhysicalDisk disk; LibvirtStorageVolumeDef voldef = getStorageVolumeDef(libvirtPool.getPool().getConnect(), vol); disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); @@ -693,7 +678,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } return disk; } catch (LibvirtException e) { - logger.debug("Failed to get physical disk:", e); + logger.debug("Failed to get volume from libvirt: " + e.getMessage()); throw new CloudRuntimeException(e.toString()); } } @@ -722,7 +707,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { * Thread-safe increment storage pool usage refcount * @param uuid UUID of the storage pool to increment the count */ - private void incStoragePoolRefCount(String uuid) { + protected void incStoragePoolRefCount(String uuid) { adjustStoragePoolRefCount(uuid, 1); } /** @@ -730,7 +715,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { * @param uuid UUID of the storage pool to decrement the count * @return true if the storage pool is still used, else false. */ - private boolean decStoragePoolRefCount(String uuid) { + protected boolean decStoragePoolRefCount(String uuid) { return adjustStoragePoolRefCount(uuid, -1) > 0; } @@ -814,7 +799,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { sp = createNetfsStoragePool(PoolType.NETFS, conn, name, host, path, nfsMountOpts); } catch (LibvirtException e) { - logger.error("Failed to create netfs mount: " + host + ":" + path , e); + logger.error("Failed to create netfs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -822,7 +807,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { sp = createNetfsStoragePool(PoolType.GLUSTERFS, conn, name, host, path, null); } catch (LibvirtException e) { - logger.error("Failed to create glusterfs mount: " + host + ":" + path , e); + logger.error("Failed to create glusterlvm_fs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -830,8 +815,6 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { sp = createSharedStoragePool(conn, name, host, path); } else if (type == StoragePoolType.RBD) { sp = createRBDStoragePool(conn, name, host, port, userInfo, path); - } else if (type == StoragePoolType.CLVM) { - sp = createCLVMStoragePool(conn, name, host, path); } } @@ -845,7 +828,6 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { // to be always mounted, as long the primary storage isn't fully deleted. incStoragePoolRefCount(name); } - if (sp.isActive() == 0) { logger.debug("Attempting to activate pool " + name); sp.create(0); @@ -1116,6 +1098,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { @Override public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { // this is for managed storage that needs to prep disks prior to use + return true; } @@ -1227,7 +1210,11 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { LibvirtStoragePool libvirtPool = (LibvirtStoragePool)pool; try { StorageVol vol = getVolume(libvirtPool.getPool(), uuid); - logger.debug("Instructing libvirt to remove volume " + uuid + " from pool " + pool.getUuid()); + if (vol == null) { + logger.warn("Volume {} not found in libvirt pool {}, it may have been already deleted", uuid, pool.getUuid()); + return true; + } + logger.debug("Instructing libvirt to remove volume {} from pool {}", uuid, pool.getUuid()); if(Storage.ImageFormat.DIR.equals(format)){ deleteDirVol(libvirtPool, vol); } else { @@ -1420,9 +1407,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { rbd.close(destImage); } else { logger.debug("The source image " + srcPool.getSourceDir() + "/" + template.getName() - + " is RBD format 2. We will perform a RBD clone using snapshot " - + rbdTemplateSnapName); - /* The source image is format 2, we can do a RBD snapshot+clone (layering) */ + + " is RBD format 2. We will perform a RBD snapshot+clone (layering)"); logger.debug("Checking if RBD snapshot " + srcPool.getSourceDir() + "/" + template.getName() @@ -1618,9 +1603,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 destInfo = qemu.info(destFile); Long virtualSize = Long.parseLong(destInfo.get(QemuImg.VIRTUAL_SIZE)); newDisk.setVirtualSize(virtualSize); @@ -1684,8 +1672,8 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } } else { /** - We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning - it doesn't benefit us. It's better to keep the current code in place which works + We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning + it doesn't benefit us. It's better to keep the current code in place which works */ srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool, sourcePath)); srcFile.setFormat(sourceFormat); @@ -1737,6 +1725,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { vol.delete(0); } + private void deleteDirVol(LibvirtStoragePool pool, StorageVol vol) throws LibvirtException { Script.runSimpleBashScript("rm -r --interactive=never " + vol.getPath()); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java index 910f0eb15e0..a8c32baa6ef 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java @@ -213,7 +213,7 @@ public class LibvirtStoragePool implements KVMStoragePool { @Override public boolean isExternalSnapshot() { - if (this.type == StoragePoolType.CLVM || type == StoragePoolType.RBD) { + if (this.type == StoragePoolType.CLVM || this.type == StoragePoolType.CLVM_NG || type == StoragePoolType.RBD) { return true; } return false; @@ -278,6 +278,10 @@ public class LibvirtStoragePool implements KVMStoragePool { return this.type; } + public void setType(StoragePoolType type) { + this.type = type; + } + public StoragePool getPool() { return this._pool; } @@ -420,8 +424,4 @@ public class LibvirtStoragePool implements KVMStoragePool { return true; } } - - public void setType(StoragePoolType type) { - this.type = type; - } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java index 76b5a413e70..fb474a6bc7d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java @@ -148,4 +148,8 @@ public interface StorageAdaptor { default Pair unprepareStorageClient(String uuid, Map details) { return new Pair<>(true, ""); } + + default void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + // no-op + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index b3bdafb7375..ce431d9bab2 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -2728,8 +2728,11 @@ public class LibvirtComputingResourceTest { @Test public void testModifyStoragePoolCommand() { - final StoragePool pool = Mockito.mock(StoragePool.class);; + final StoragePool pool = Mockito.mock(StoragePool.class); final ModifyStoragePoolCommand command = new ModifyStoragePoolCommand(true, pool); + Map details = new HashMap<>(); + details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + command.setDetails(details); final KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); final KVMStoragePool kvmStoragePool = Mockito.mock(KVMStoragePool.class); @@ -2753,8 +2756,11 @@ public class LibvirtComputingResourceTest { @Test public void testModifyStoragePoolCommandFailure() { - final StoragePool pool = Mockito.mock(StoragePool.class);; + final StoragePool pool = Mockito.mock(StoragePool.class); final ModifyStoragePoolCommand command = new ModifyStoragePoolCommand(true, pool); + Map details = new HashMap<>(); + details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + command.setDetails(details); final KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); @@ -7245,6 +7251,307 @@ public class LibvirtComputingResourceTest { libvirtComputingResourceSpy.getInterface(connMock, vmName, invalidMacAddress); } + @Test + public void testExtractVolumeGroupFromPath_ValidPath() { + String devicePath = "/dev/vg1/volume-123"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_ComplexVGName() { + String devicePath = "/dev/cloudstack-vg-primary/volume-456"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("cloudstack-vg-primary", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_MultiLevelPath() { + String devicePath = "/dev/vg-cluster-01/lv-data-001"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg-cluster-01", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_NullPath() { + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(null); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_EmptyPath() { + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(""); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_NonDevPath() { + String devicePath = "/var/lib/libvirt/images/disk.qcow2"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_InvalidFormat() { + String devicePath = "/dev/"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_OnlyVG() { + String devicePath = "/dev/vg1"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + // Implementation extracts parts[2] regardless of whether there's an LV name + assertEquals("vg1", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_MapperPath() { + String devicePath = "/dev/mapper/vg1-volume"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("mapper", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_WithDashes() { + String devicePath = "/dev/vg-name-with-dashes/lv-name"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg-name-with-dashes", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_WithUnderscores() { + String devicePath = "/dev/vg_name_with_underscores/lv_name"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg_name_with_underscores", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_NullVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(null); + assertFalse(result); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_EmptyVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(""); + assertFalse(result); + } + + @Test + public void testActivateClvmVolumeExclusive_ValidPath() { + try { + String volumePath = "/dev/test-vg/test-lv"; + LibvirtComputingResource.activateClvmVolumeExclusive(volumePath); + } catch (Exception e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Should be LVM-related error", + message.contains("lvm") || + message.contains("lvchange") || + message.contains("volume") || + message.contains("not found") || + message.contains("failed")); + } + } + + @Test + public void testDeactivateClvmVolume_ValidPath() { + String volumePath = "/dev/test-vg/test-lv"; + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_ValidPath() { + String volumePath = "/dev/test-vg/test-lv"; + + LibvirtComputingResource.setClvmVolumeToSharedMode(volumePath); + + assertTrue(true); + } + + @Test + public void testDeactivateClvmVolume_NullPath() { + LibvirtComputingResource.deactivateClvmVolume(null); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_NullPath() { + LibvirtComputingResource.setClvmVolumeToSharedMode(null); + assertTrue(true); // Passes if no exception + } + + @Test + public void testDeactivateClvmVolume_EmptyPath() { + LibvirtComputingResource.deactivateClvmVolume(""); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_EmptyPath() { + LibvirtComputingResource.setClvmVolumeToSharedMode(""); + assertTrue(true); + } + + @Test + public void testDeactivateClvmVolume_InvalidPath() { + String invalidPath = "/invalid/path/that/does/not/exist"; + LibvirtComputingResource.deactivateClvmVolume(invalidPath); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_InvalidPath() { + // Should handle invalid path gracefully without throwing + String invalidPath = "/invalid/path/that/does/not/exist"; + LibvirtComputingResource.setClvmVolumeToSharedMode(invalidPath); + assertTrue(true); // Passes if no exception + } + + @Test + public void testExtractVolumeGroupFromPath_RealWorldPaths() { + assertEquals("acsvg", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/acsvg/volume-123")); + assertEquals("cloudstack-primary", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/cloudstack-primary/vm-disk-1")); + assertEquals("ceph-vg", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/ceph-vg/snapshot-456")); + assertEquals("vg01", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg01/data")); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_NonExistentVG() { + String nonExistentVG = "non-existent-vg-" + System.currentTimeMillis(); + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(nonExistentVG); + assertFalse(result); + } + + @Test + public void testActivateClvmVolumeExclusive_ComplexPath() { + try { + String complexPath = "/dev/cloudstack-vg-primary-cluster-01/volume-123-456-789-abc"; + LibvirtComputingResource.activateClvmVolumeExclusive(complexPath); + } catch (Exception e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Should be LVM-related error", + message.contains("lvm") || + message.contains("lvchange") || + message.contains("volume") || + message.contains("not found") || + message.contains("failed")); + } + } + + @Test + public void testDeactivateClvmVolume_ComplexPath() { + String complexPath = "/dev/cloudstack-vg-primary-cluster-01/volume-123-456-789-abc"; + LibvirtComputingResource.deactivateClvmVolume(complexPath); + assertTrue(true); + } + + @Test + public void testExtractVolumeGroupFromPath_SpecialCharacters() { + assertEquals("vg.name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg.name/lv")); + assertEquals("vg_name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg_name/lv")); + assertEquals("vg-name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg-name/lv")); + assertEquals("vg123", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg123/lv456")); + } + + @Test + public void testExtractVolumeGroupFromPath_TrailingSlash() { + String devicePath = "/dev/vg1/volume-123/"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_WhitespaceVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(" "); + assertFalse(result); + } + + @Test + public void testExtractVolumeGroupFromPath_DevMapperExcluded() { + String mapperPath1 = "/dev/mapper/vg1-lv1"; + String mapperPath2 = "/dev/mapper/cloudstack--vg-volume--1"; + + assertEquals("mapper", LibvirtComputingResource.extractVolumeGroupFromPath(mapperPath1)); + assertEquals("mapper", LibvirtComputingResource.extractVolumeGroupFromPath(mapperPath2)); + } + + @Test + public void testExtractVolumeGroupFromPath_EdgeCases() { + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("/dev")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("/dev/")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("dev/vg/lv")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("//dev//vg//lv")); + } + + @Test + public void testClvmVolumeActivationSequence() { + // Test a typical sequence: deactivate -> activate exclusive -> deactivate -> shared + String volumePath = "/dev/test-vg/test-volume"; + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + + try { + LibvirtComputingResource.activateClvmVolumeExclusive(volumePath); + } catch (Exception e) { + // Expected in test environment + } + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + LibvirtComputingResource.setClvmVolumeToSharedMode(volumePath); + + assertTrue(true); // Test passes if sequence completes + } + + @Test + public void testExtractVolumeGroupFromPath_LongVGName() { + String longVGName = "a".repeat(100); + String devicePath = "/dev/" + longVGName + "/volume"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals(longVGName, vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_LongLVName() { + String longLVName = "volume-" + "b".repeat(100); + String devicePath = "/dev/vg1/" + longLVName; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_SpecialCharactersInName() { + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg.test.name")); + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg_test_name")); + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg-test-name")); + } + + @Test + public void testClvmMethodsWithMultiplePaths() { + String[] paths = { + "/dev/vg1/vol1", + "/dev/vg2/vol2", + "/dev/cloudstack-primary/vol3", + "/dev/test-vg/test-vol" + }; + + for (String path : paths) { + LibvirtComputingResource.deactivateClvmVolume(path); + LibvirtComputingResource.setClvmVolumeToSharedMode(path); + + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(path); + assertNotNull("Should extract VG from: " + path, vgName); + + boolean clustered = LibvirtComputingResource.checkIfVolumeGroupIsClustered(vgName); + } + + assertTrue(true); // Passes if all paths processed + } + @Test public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreNotUpdatedWhenLibvirtVersionIsLessThanTheMinimum() throws LibvirtException { libvirtComputingResourceSpy.hypervisorLibvirtVersion = 8999; diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java new file mode 100644 index 00000000000..5d11cf209a4 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java @@ -0,0 +1,462 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.utils.script.Script; + +/** + * Tests for LibvirtClvmLockTransferCommandWrapper + */ +@RunWith(MockitoJUnitRunner.class) +public class LibvirtClvmLockTransferCommandWrapperTest { + + @Mock + private LibvirtComputingResource libvirtComputingResource; + + private LibvirtClvmLockTransferCommandWrapper wrapper; + + private static final String TEST_LV_PATH = "/dev/vg1/volume-123"; + private static final String TEST_VOLUME_UUID = "test-volume-uuid-456"; + + @Before + public void setUp() { + wrapper = new LibvirtClvmLockTransferCommandWrapper(); + } + + @Test + public void testExecute_DeactivateSuccess() { + ClvmLockTransferCommand cmd = new ClvmLockTransferCommand( + ClvmLockTransferCommand.Operation.DEACTIVATE, + TEST_LV_PATH, + TEST_VOLUME_UUID + ); + + try (MockedConstruction