CLVM enhancements and fixes (#12617)

This PR enhances the existing CLVM implementation which was based on the deprecated CLVM technology which was based on corosync/pacemaker. With RHEL 7 having reached EOL, CLVM seems to be broken. CLVM supports RAW volumes on LVM , where as CLVM_NG support QCOW2 on LVM.

Further details: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Modernized+CLVM%3A+Enhancements+and+CLVM_NG+support

NOTE: On testing - it was identified that incremental snapshots for clvm-ng do not work as expected. As of now it's been removed from scope. So, CLVM and CLVM_NG would only support full snapshots.


* add support for proper cleanup of snapshots and prevent vol snapshot of running vm

* remove snap vol restriction for sunning vms

* refactor clvm code

* add support for live migration

* add support for migrating lvm lock

* clvm deletion called explicitly

* made necessary changes to allow migration of lock and deletion of detached volumes

* fix create vol from snap and attach

* add support to revert snapshot for clvm

* add support to revert snapshot for clvm

* make zero fill configurable

* make setting non-dynamic & fix test

* fix locking at vol/vm creation

* fix revert snapshot format type and handle revert snapshot functionality for clvm

* 1. Create clvmlockmanager and move common code \n
2. handle attaching volumes to stopped VMs \n
3. Handle lock transfer when VM is started on another host

* add license

* remove command/answer classes from sonar coverage check

* add support for new gen clvm with template (qcow2) backing

* Add support for clvm_ng - which allows qcow2 on block storage , linked clones, etc

* fix test and use physical size + 50% of virtual size for backing file, while virtual size + pe for disk

* migrate clvm volumes as full clone and allow migration from clvm to nfs

* fix clvm_ng to nfs migration, and handle overhead calc

* support live migration from clvm_ng to nfs and vice-versa

* add support to migrate to and from clvm to nfs

* fix creation of volume on destination host during migration to clvm/clvm-ng

* support live vm migration between clvm -> clvm-ng (vice-versa), nfs -> clvm (vice-versa) and nfs->clvm-ng (vice-versa)

* add unit tests for clvm/clvm_ng operations

* Add support for incremental volume snapshots for clvm_ng

* prevent snapshot backup for incremental clvm_ng snaps, fix build failure, add unit tests

* fix lockhost on creation of volumes from snap and fix bitmap issue when migrating a vol with incremental snap

* restrict pre and post migration commands to only kvm hosts where vm has CLVM/CLVM-NG volumes

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

* add test for pre/post migration

* Create a CLVM storage adaptor

* update existing clvm get stats method

* fix precommit check failure

* Apply suggestions from code review

Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>

* Apply suggestions from code review

Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>

* improve lock host retrieval logic and quicker retrival using db host as first check point and then fanning out

* add proper support for resizing of clvm_ng which calculated PE correctly for qcow2 metadata

* fallback to full snapshots for clvm-ng - incremental not supported in 4.23

* expunge volume detail of lock host on vm expunge

* if vmmigration with volume is done to the same clvm volume group, then dont do data transfer, just lock transfer and vm

* add clvm pools with deterministic uuid , so as to prevent adding the same pool twic

* added a small improvement to factor in a senario when lv is inactive on all hosts, could happen in storage outage issue

* address comment - extract common code for endpoint identification if clvm pool type

* Address comments - add early return guard to reduce indentation

* minor improvement - when migrating vm with volumes, if there's a failures, change the clvm vols to exclusive on source from shared, and on success, change dest vol to exclusive only for cross-pool migration

* cleanup unused code and tests for incremental snaps for clvmng and other cleanups

* allow storage browser to list lv in clvm, fix clvm shrink, overprovisioning factor isnt used for clvm pools - so set it to 1 and prevented display of provisioning type for clvm

* no need to have locktransfercommand to execute in sequence

* increase lv cmd timeouts to consider cluster load

---------

Co-authored-by: Pearl Dsilva <pearl1954@gmail.com>
Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>
This commit is contained in:
Pearl Dsilva 2026-06-16 06:46:51 -04:00 committed by GitHub
parent ac6c1c800d
commit ce9793c0be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 11955 additions and 225 deletions

View File

@ -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

View File

@ -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<String, DpdkTO> dpdkInterfaceMapping = new HashMap<>();
private int newVmCpuShares;
private boolean clvmCrossPoolMigration;
Map<String, Boolean> 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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.
*
* <p>Instead of copying volume data (traditional migration), this command simply
* deactivates the LV on the source host and activates it exclusively on the destination host.
*
* <p>This is significantly faster (10-100x) than traditional migration and uses no network bandwidth.
*/
public class ClvmLockTransferCommand extends Command {
/**
* Operation to perform on the CLVM volume.
* Maps to lvchange flags for LVM operations.
*/
public enum Operation {
/** Deactivate the volume on this host (-an) */
DEACTIVATE("-an", "deactivate"),
/** Activate the volume exclusively on this host (-aey) */
ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"),
/** Activate the volume in shared mode on this host (-asy) */
ACTIVATE_SHARED("-asy", "activate in shared mode"),
/** 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;
}
}

View File

@ -103,4 +103,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume {
List<String> getCheckpointPaths();
Set<String> getCheckpointImageStoreUrls();
/**
* Gets the destination host ID hint for CLVM volume creation.
* This is used to route volume creation commands to the specific host where the VM will be deployed.
* Only applicable for CLVM storage pools to avoid shared mode activation.
*
* @return The host ID where the volume should be created, or null if not set
*/
Long getDestinationHostId();
/**
* Sets the destination host ID hint for CLVM volume creation.
* This should be set before volume creation when the destination host is known.
*
* @param hostId The host ID where the volume should be created
*/
void setDestinationHostId(Long hostId);
}

View File

@ -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);
}

View File

@ -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<VolumeVO> 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<VolumeVO> 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<Long, Long> findClusterAndHostIdForVm(long vmId) {
VMInstanceVO vm = _vmDao.findById(vmId);

View File

@ -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<VolumeApiResult> 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<VolumeVO> 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<VolumeVO> 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<StoragePoolHostVO> 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<StoragePoolHostVO> 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<Volume, StoragePool> 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<VolumeVO> 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<VolumeTask> tasks = getTasks(vols, dest.getStorageForDisks(), vm);
Volume vol = null;
PrimaryDataStore store;

View File

@ -44,6 +44,8 @@
value="#{storagePoolAllocatorsRegistry.registered}" />
</bean>
<bean id="clvmPoolManager" class="com.cloud.storage.clvm.ClvmPoolManager" />
<bean id="storageOrchestrator"
class="org.apache.cloudstack.engine.orchestration.StorageOrchestrator"/>
<bean id="dataMigrationHelper"

View File

@ -38,6 +38,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -60,6 +61,7 @@ import com.cloud.ha.HighAvailabilityManager;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.resource.ResourceManager;
import com.cloud.storage.clvm.ClvmPoolManager;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator;
@ -336,6 +338,19 @@ public class VirtualMachineManagerImplTest {
}
}
private ClvmPoolManager injectMockedClvmPoolManager() {
ClvmPoolManager clvmPoolManagerMock = mock(ClvmPoolManager.class);
ReflectionTestUtils.setField(virtualMachineManagerImpl, "clvmPoolManager", clvmPoolManagerMock);
return clvmPoolManagerMock;
}
private Method getUpdateClvmLockHostForVmVolumesMethod() throws NoSuchMethodException {
Method method = VirtualMachineManagerImpl.class.getDeclaredMethod(
"updateClvmLockHostForVmVolumes", long.class, long.class);
method.setAccessible(true);
return method;
}
@Test
public void testaddHostIpToCertDetailsIfConfigAllows() {
Host vmHost = mock(Host.class);
@ -1954,4 +1969,181 @@ public class VirtualMachineManagerImplTest {
}
}
@Test
public void testUpdateClvmLockHostForVmVolumes_WithClvmVolumes() throws Exception {
long vmId = 100L;
long destHostId = 2L;
long poolId = 10L;
VolumeVO clvmVolume1 = mock(VolumeVO.class);
VolumeVO clvmVolume2 = mock(VolumeVO.class);
when(clvmVolume1.getId()).thenReturn(1L);
when(clvmVolume1.getPoolId()).thenReturn(poolId);
when(clvmVolume2.getId()).thenReturn(2L);
when(clvmVolume2.getPoolId()).thenReturn(poolId);
StoragePoolVO clvmPool = mock(StoragePoolVO.class);
when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(clvmVolume1, clvmVolume2));
when(storagePoolDaoMock.findById(poolId)).thenReturn(clvmPool);
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId);
verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(2L, destHostId);
}
@Test
public void testUpdateClvmLockHostForVmVolumes_WithNonClvmVolumes() throws Exception {
long vmId = 100L;
long destHostId = 2L;
long poolId = 10L;
VolumeVO nfsVolume = mock(VolumeVO.class);
when(nfsVolume.getPoolId()).thenReturn(poolId);
StoragePoolVO nfsPool = mock(StoragePoolVO.class);
when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(nfsVolume));
when(storagePoolDaoMock.findById(poolId)).thenReturn(nfsPool);
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong());
}
@Test
public void testUpdateClvmLockHostForVmVolumes_WithMixedVolumes() throws Exception {
long vmId = 100L;
long destHostId = 2L;
long clvmPoolId = 10L;
long nfsPoolId = 20L;
VolumeVO clvmVolume = mock(VolumeVO.class);
VolumeVO nfsVolume = mock(VolumeVO.class);
when(clvmVolume.getId()).thenReturn(1L);
when(clvmVolume.getPoolId()).thenReturn(clvmPoolId);
when(nfsVolume.getPoolId()).thenReturn(nfsPoolId);
StoragePoolVO clvmPool = mock(StoragePoolVO.class);
when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
StoragePoolVO nfsPool = mock(StoragePoolVO.class);
when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(clvmVolume, nfsVolume));
when(storagePoolDaoMock.findById(clvmPoolId)).thenReturn(clvmPool);
when(storagePoolDaoMock.findById(nfsPoolId)).thenReturn(nfsPool);
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId);
verify(clvmPoolManagerMock, never()).setClvmLockHostId(2L, destHostId);
}
@Test
public void testUpdateClvmLockHostForVmVolumes_WithNoVolumes() throws Exception {
long vmId = 100L;
long destHostId = 2L;
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Collections.emptyList());
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong());
}
@Test
public void testUpdateClvmLockHostForVmVolumes_WithNullPoolId() throws Exception {
long vmId = 100L;
long destHostId = 2L;
VolumeVO volumeWithoutPool = mock(VolumeVO.class);
when(volumeWithoutPool.getPoolId()).thenReturn(null);
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volumeWithoutPool));
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(storagePoolDaoMock, never()).findById(anyLong());
verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong());
}
@Test
public void testUpdateClvmLockHostForVmVolumes_WithNullPool() throws Exception {
long vmId = 100L;
long destHostId = 2L;
long poolId = 10L;
VolumeVO volume = mock(VolumeVO.class);
when(volume.getPoolId()).thenReturn(poolId);
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volume));
when(storagePoolDaoMock.findById(poolId)).thenReturn(null);
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong());
}
@Test
public void testUpdateClvmLockHostForVmVolumes_MultipleClvmPools() throws Exception {
long vmId = 100L;
long destHostId = 2L;
long pool1Id = 10L;
long pool2Id = 20L;
VolumeVO volume1 = mock(VolumeVO.class);
VolumeVO volume2 = mock(VolumeVO.class);
VolumeVO volume3 = mock(VolumeVO.class);
when(volume1.getId()).thenReturn(1L);
when(volume1.getPoolId()).thenReturn(pool1Id);
when(volume2.getId()).thenReturn(2L);
when(volume2.getPoolId()).thenReturn(pool2Id);
when(volume3.getId()).thenReturn(3L);
when(volume3.getPoolId()).thenReturn(pool1Id);
StoragePoolVO clvmPool1 = mock(StoragePoolVO.class);
when(clvmPool1.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
StoragePoolVO clvmPool2 = mock(StoragePoolVO.class);
when(clvmPool2.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volume1, volume2, volume3));
when(storagePoolDaoMock.findById(pool1Id)).thenReturn(clvmPool1);
when(storagePoolDaoMock.findById(pool2Id)).thenReturn(clvmPool2);
ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager();
Method method = getUpdateClvmLockHostForVmVolumesMethod();
method.invoke(virtualMachineManagerImpl, vmId, destHostId);
verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId);
verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(2L, destHostId);
verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(3L, destHostId);
}
}

View File

@ -16,6 +16,8 @@
// under the License.
package org.apache.cloudstack.engine.orchestration;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -30,6 +32,7 @@ import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.offering.DiskOffering;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.storage.ScopeType;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage;
@ -42,6 +45,7 @@ import com.cloud.user.ResourceLimitService;
import com.cloud.uservm.UserVm;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.utils.Pair;
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
@ -67,6 +71,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedConstruction;
@ -640,4 +645,305 @@ public class VolumeOrchestratorTest {
Assert.assertEquals(1, result.second().size());
}
@Test
public void testTransferClvmLocksForVmStart_WithClvmVolumes() throws Exception {
Long destHostId = 2L;
Long currentHostId = 1L;
Long poolId = 10L;
VolumeVO clvmVolume1 = Mockito.mock(VolumeVO.class);
VolumeVO clvmVolume2 = Mockito.mock(VolumeVO.class);
Mockito.when(clvmVolume1.getId()).thenReturn(101L);
Mockito.when(clvmVolume1.getPoolId()).thenReturn(poolId);
Mockito.when(clvmVolume1.getUuid()).thenReturn("vol-uuid-1");
Mockito.when(clvmVolume1.getPath()).thenReturn("vol-path-1");
Mockito.when(clvmVolume2.getId()).thenReturn(102L);
Mockito.when(clvmVolume2.getPoolId()).thenReturn(poolId);
Mockito.when(clvmVolume2.getUuid()).thenReturn("vol-uuid-2");
Mockito.when(clvmVolume2.getPath()).thenReturn("vol-path-2");
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.getClvmLockHostId(Mockito.eq(102L), 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(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(clvmVolume1, clvmVolume2), destHostId, vmInstance);
Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock(
Mockito.eq("vol-uuid-1"), Mockito.eq(101L), Mockito.eq("vol-path-1"),
Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId));
Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock(
Mockito.eq("vol-uuid-2"), Mockito.eq(102L), Mockito.eq("vol-path-2"),
Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId));
}
@Test
public void testTransferClvmLocksForVmStart_WithNonClvmVolumes() throws Exception {
Long destHostId = 2L;
Long poolId = 10L;
VolumeVO nfsVolume = Mockito.mock(VolumeVO.class);
Mockito.when(nfsVolume.getPoolId()).thenReturn(poolId);
StoragePoolVO nfsPool = Mockito.mock(StoragePoolVO.class);
Mockito.when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class);
ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class);
Mockito.when(storagePoolDao.findById(poolId)).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(nfsVolume), destHostId, vmInstance);
Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock(
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
}
@Test
public void testTransferClvmLocksForVmStart_NoLockTransferNeeded() 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(destHostId);
Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool);
setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager);
setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao);
java.lang.reflect.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.never()).transferClvmVolumeLock(
Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(),
Mockito.any(), Mockito.anyLong(), Mockito.anyLong());
}
@Test
public void testTransferClvmLocksForVmStart_EmptyVolumeList() throws Exception {
Long destHostId = 2L;
VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class);
ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class);
setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager);
Method method = VolumeOrchestrator.class.getDeclaredMethod(
"transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class);
method.setAccessible(true);
method.invoke(volumeOrchestrator, new ArrayList<VolumeVO>(), 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;
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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<VolumeInfo, VolumeInfo> srcVolumeInfoToDestVolumeInfo = new HashMap<>();
List<VolumeInfo> 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<VolumeInfo, DataStore> 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<VolumeInfo, VolumeInfo> 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<VolumeInfo, VolumeInfo> 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<VolumeInfo, VolumeInfo> 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);

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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<VolumeInfo, DataStore> 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<VolumeInfo, DataStore> 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<VolumeInfo, DataStore> 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<VolumeInfo, DataStore> 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);
}
}

View File

@ -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<SnapshotDataStoreVO> 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()));
}
}

View File

@ -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<VolumeVO> 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;

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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<VolumeVO> 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<VolumeVO> 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<VolumeVO> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<RemoteHostEndPoint> 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);
}
}

View File

@ -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<String, String> nfsMountOpts = storageManager.getStoragePoolNFSMountOpts(pool, null).first();
Optional.ofNullable(nfsMountOpts).ifPresent(detailsMap::putAll);
// Propagate CLVM secure zero-fill setting to the host
// Note: This is done during host connection (agent start, MS restart, host reconnection)
// so the setting is non-dynamic. Changes require host reconnection to take effect.
if (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);

View File

@ -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<String> checkpointPaths;
private Set<String> 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());

View File

@ -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<HostVO> 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;
}
}

View File

@ -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);
}
}

View File

@ -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<String> names = new ArrayList<>();
List<String> paths = new ArrayList<>();
List<String> absPaths = new ArrayList<>();
List<Boolean> isDirs = new ArrayList<>();
List<Long> sizes = new ArrayList<>();
List<Long> 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<String[]> 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<DiskDef> 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/<vgname>/<lvname>
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;
}
}

View File

@ -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<ClvmLockTransferCommand, Answer, LibvirtComputingResource> {
@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 <lvPath>
*
* 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());
}
}
}

View File

@ -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<MigrateCommand, Answer, LibvirtComputingResource> {
@ -117,7 +116,8 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper<MigrateCo
Command.State commandState = null;
List<InterfaceDef> ifaces = null;
List<DiskDef> disks;
List<DiskDef> disks = new ArrayList<>();
VirtualMachineTO to = null;
Domain dm = null;
Connect dconn = null;
@ -136,7 +136,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper<MigrateCo
if (logger.isDebugEnabled()) {
logger.debug(String.format("Found domain with name [%s]. Starting VM migration to host [%s].", vmName, destinationUri));
}
VirtualMachineTO to = command.getVirtualMachine();
to = command.getVirtualMachine();
dm = conn.domainLookupByName(vmName);
/*
@ -338,6 +338,14 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper<MigrateCo
logger.debug(String.format("Cleaning the disks of VM [%s] in the source pool after VM migration finished.", vmName));
}
resumeDomainIfPaused(destDomain, vmName);
// For cross-pool CLVM migration, skip deactivation so the source LV stays
// active (in shared mode) and deletion can route directly to the source host
// without fanning out across the cluster to find an inactive LV.
if (to != null && !command.isClvmCrossPoolMigration()) {
LibvirtComputingResource.modifyClvmVolumesStateForMigration(disks, to, LibvirtComputingResource.ClvmVolumeState.DEACTIVATE);
}
deleteOrDisconnectDisksOnSourcePool(libvirtComputingResource, migrateDiskInfoList, disks);
libvirtComputingResource.cleanOldSecretsByDiskDef(conn, disks);
}
@ -384,6 +392,10 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper<MigrateCo
if (destDomain != null) {
destDomain.free();
}
// Revert CLVM volumes to exclusive mode on failure
if (to != null && result != null) {
LibvirtComputingResource.modifyClvmVolumesStateForMigration(disks, to, LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE);
}
} catch (final LibvirtException e) {
logger.trace("Ignoring libvirt error.", e);
}
@ -683,7 +695,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper<MigrateCo
protected void deleteOrDisconnectDisksOnSourcePool(final LibvirtComputingResource libvirtComputingResource, final List<MigrateDiskInfo> migrateDiskInfoList,
List<DiskDef> 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<MigrateCo
for (int z = 0; z < diskChildNodes.getLength(); z++) {
Node diskChildNode = diskChildNodes.item(z);
if (migrateStorageManaged && "driver".equals(diskChildNode.getNodeName())) {
boolean shouldUpdateDriverType = shouldUpdateDriverTypeForMigration(
migrateStorageManaged, migrateDiskInfo);
if (shouldUpdateDriverType && "driver".equals(diskChildNode.getNodeName())) {
Node driverNode = diskChildNode;
NamedNodeMap driverNodeAttributes = driverNode.getAttributes();
@ -1066,4 +1081,64 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper<MigrateCo
return xmlDesc.replaceAll("(graphics\\s+[^>]*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<String, MigrateCommand.MigrateDiskInfo> mapMigrateStorage) {
if (MapUtils.isEmpty(mapMigrateStorage)) {
return false;
}
try {
for (Map.Entry<String, MigrateCommand.MigrateDiskInfo> 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;
}
}

View File

@ -52,9 +52,19 @@ public final class LibvirtModifyStoragePoolCommandWrapper extends CommandWrapper
final KVMStoragePool storagepool;
try {
Map<String, String> 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");
}

View File

@ -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<PostMigrationCommand, Answer, LibvirtComputingResource> {
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<DiskDef> 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);
}
}
}

View File

@ -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<PreMigrationCommand, Answer, LibvirtComputingResource> {
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<DiskDef> 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());
}
}
}
}
}

View File

@ -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<LibvirtVMDef.DiskDef> 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) {

View File

@ -113,7 +113,8 @@ public final class LibvirtResizeVolumeCommandWrapper extends CommandWrapper<Resi
logger.debug("Resizing volume: " + path + ", from: " + toHumanReadableSize(currentSize) + ", to: " + toHumanReadableSize(newSize) + ", type: " + type + ", name: " + vmInstanceName + ", shrinkOk: " + shrinkOk);
/* libvirt doesn't support resizing (C)LVM devices, and corrupts QCOW2 in some scenarios, so we have to do these via qemu-img */
if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex
if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG
&& pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex
&& vol.getFormat() != PhysicalDiskFormat.QCOW2) {
logger.debug("Volume " + path + " can be resized by libvirt. Asking libvirt to resize the volume.");
try {

View File

@ -117,7 +117,7 @@ public class LibvirtRevertSnapshotCommandWrapper extends CommandWrapper<RevertSn
secondaryStoragePool = storagePoolMgr.getStoragePoolByURI(snapshotImageStore.getUrl());
}
if (primaryPool.getType() == StoragePoolType.CLVM) {
if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) {
Script cmd = new Script(libvirtComputingResource.manageSnapshotPath(), libvirtComputingResource.getCmdsTimeout(), logger);
cmd.add("-v", getFullPathAccordingToStorage(secondaryStoragePool, snapshotRelPath));
cmd.add("-n", snapshotDisk.getName());

View File

@ -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);

View File

@ -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<String, String> 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<Boolean, Map<String, String>, String> prepareStorageClient(StoragePoolType type, String uuid, Map<String, String> details) {
StorageAdaptor adaptor = getStorageAdaptor(type);
return adaptor.prepareStorageClient(uuid, details);

View File

@ -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 {
" </devices>\n" +
"</domain>";
private static final String DUMMY_VM_XML_BLOCK = "<domain type='qemu'>\n" +
" <name>%s</name>\n" +
" <memory unit='MiB'>256</memory>\n" +
" <currentMemory unit='MiB'>256</currentMemory>\n" +
" <vcpu>1</vcpu>\n" +
" <os>\n" +
" <type arch='%s' machine='%s'>hvm</type>\n" +
" <boot dev='hd'/>\n" +
" </os>\n" +
" <devices>\n" +
" <emulator>%s</emulator>\n" +
" <disk type='block' device='disk'>\n" +
" <driver name='qemu' type='qcow2' cache='none'/>\n"+
" <source dev='%s'/>\n" +
" <target dev='sda'/>\n" +
" </disk>\n" +
" <graphics type='vnc' port='-1'/>\n" +
" </devices>\n" +
"</domain>";
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<String, String> 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.<br><br>
* 1st parameter: snapshot's name;<br>
@ -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;

View File

@ -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<String> getNFSMountOptsFromDetails(StoragePoolType type, Map<String, String> details) {
List<String> 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<String, String> 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<String, String> 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());
}

View File

@ -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;
}
}

View File

@ -148,4 +148,8 @@ public interface StorageAdaptor {
default Pair<Boolean, String> unprepareStorageClient(String uuid, Map<String, String> details) {
return new Pair<>(true, "");
}
default void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) {
// no-op
}
}

View File

@ -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<String, String> 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<String, String> 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;

View File

@ -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<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null); // Success
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertTrue(answer.getResult());
assertTrue(answer.getDetails().contains("deactivated"));
assertTrue(answer.getDetails().contains(TEST_VOLUME_UUID));
// Verify script was constructed with correct parameters
assertEquals(1, scriptMock.constructed().size());
Script script = scriptMock.constructed().get(0);
verify(script).add("-an");
verify(script).add(TEST_LV_PATH);
}
}
@Test
public void testExecute_ActivateExclusiveSuccess() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null); // Success
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertTrue(answer.getResult());
assertTrue(answer.getDetails().contains("activated exclusively"));
assertTrue(answer.getDetails().contains(TEST_VOLUME_UUID));
// Verify script was constructed with correct parameters
assertEquals(1, scriptMock.constructed().size());
Script script = scriptMock.constructed().get(0);
verify(script).add("-aey");
verify(script).add(TEST_LV_PATH);
}
}
@Test
public void testExecute_ActivateSharedSuccess() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_SHARED,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null); // Success
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertTrue(answer.getResult());
assertTrue(answer.getDetails().contains("activated in shared mode"));
assertTrue(answer.getDetails().contains(TEST_VOLUME_UUID));
// Verify script was constructed with correct parameters
assertEquals(1, scriptMock.constructed().size());
Script script = scriptMock.constructed().get(0);
verify(script).add("-asy");
verify(script).add(TEST_LV_PATH);
}
}
@Test
public void testExecute_LvchangeFailure() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
String errorMessage = "lvchange: Volume is in use";
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(errorMessage);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains("lvchange -an"));
assertTrue(answer.getDetails().contains(TEST_LV_PATH));
assertTrue(answer.getDetails().contains(errorMessage));
}
}
@Test
public void testExecute_ScriptTimeout() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
String timeoutMessage = "Script timed out after 30000ms";
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(timeoutMessage);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains("failed"));
assertTrue(answer.getDetails().contains(timeoutMessage));
}
}
@Test
public void testExecute_NullLvPath() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
null,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
// Should still execute, but may fail or succeed depending on lvchange behavior
// At minimum, it should handle null gracefully
assertEquals(1, scriptMock.constructed().size());
}
}
@Test
public void testExecute_EmptyLvPath() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
"",
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn("lvchange: Please specify a logical volume path");
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains("failed"));
}
}
@Test
public void testExecute_InvalidLvPath() {
String invalidPath = "/invalid/path/that/does/not/exist";
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
invalidPath,
TEST_VOLUME_UUID
);
String errorMessage = "Failed to find logical volume \"" + invalidPath + "\"";
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(errorMessage);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains("failed"));
assertTrue(answer.getDetails().contains(errorMessage));
}
}
@Test
public void testExecute_ExceptionDuringExecution() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
RuntimeException testException = new RuntimeException("Test exception");
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenThrow(testException);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains("Exception"));
assertTrue(answer.getDetails().contains("Test exception"));
}
}
@Test
public void testExecute_VerifyScriptConstruction() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
// Just set up the mock behavior - don't assert here as it can interfere
when(mock.execute()).thenReturn(null);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
// Verify the answer is successful
assertNotNull("Answer should not be null", answer);
assertTrue("Answer should indicate success. Details: " + answer.getDetails(),
answer.getResult());
// Verify that Script was constructed exactly once
assertEquals("Script should be constructed once", 1, scriptMock.constructed().size());
}
}
@Test
public void testExecute_AllOperationsUseDifferentFlags() {
// Test that each operation uses the correct lvchange flag
// DEACTIVATE -> -an
ClvmLockTransferCommand deactivateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null);
})) {
wrapper.execute(deactivateCmd, libvirtComputingResource);
verify(scriptMock.constructed().get(0)).add("-an");
}
// ACTIVATE_EXCLUSIVE -> -aey
ClvmLockTransferCommand exclusiveCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null);
})) {
wrapper.execute(exclusiveCmd, libvirtComputingResource);
verify(scriptMock.constructed().get(0)).add("-aey");
}
// ACTIVATE_SHARED -> -asy
ClvmLockTransferCommand sharedCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_SHARED,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null);
})) {
wrapper.execute(sharedCmd, libvirtComputingResource);
verify(scriptMock.constructed().get(0)).add("-asy");
}
}
@Test
public void testExecute_LvchangeVolumeInUseError() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
String errorMessage = "Can't deactivate volume group with active logical volumes";
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(errorMessage);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains(errorMessage));
}
}
@Test
public void testExecute_LvchangePermissionDenied() {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
TEST_LV_PATH,
TEST_VOLUME_UUID
);
String errorMessage = "Permission denied";
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(errorMessage);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertFalse(answer.getResult());
assertTrue(answer.getDetails().contains(errorMessage));
}
}
@Test
public void testExecute_ComplexLvPath() {
String complexPath = "/dev/cloudstack-vg-primary/volume-123-456-789-abc-def";
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
complexPath,
TEST_VOLUME_UUID
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertTrue(answer.getResult());
// Verify the complex path was passed correctly
Script script = scriptMock.constructed().get(0);
verify(script).add(complexPath);
}
}
@Test
public void testExecute_SequentialOperations() {
// Test that multiple operations can be executed sequentially
String[] paths = {
"/dev/vg1/vol1",
"/dev/vg1/vol2",
"/dev/vg2/vol3"
};
for (String path : paths) {
ClvmLockTransferCommand cmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE,
path,
"uuid-" + path.hashCode()
);
try (MockedConstruction<Script> scriptMock = Mockito.mockConstruction(Script.class,
(mock, context) -> {
when(mock.execute()).thenReturn(null);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);
assertNotNull(answer);
assertTrue(answer.getResult());
}
}
}
}

View File

@ -0,0 +1,341 @@
// 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.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ModifyStoragePoolAnswer;
import com.cloud.agent.api.ModifyStoragePoolCommand;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
import com.cloud.storage.Storage.StoragePoolType;
@RunWith(MockitoJUnitRunner.class)
public class LibvirtModifyStoragePoolCommandWrapperTest {
@Mock
private LibvirtComputingResource libvirtComputingResource;
@Mock
private KVMStoragePoolManager storagePoolManager;
@Mock
private ModifyStoragePoolCommand command;
@Mock
private StorageFilerTO storageFilerTO;
@Mock
private KVMStoragePool storagePool;
private LibvirtModifyStoragePoolCommandWrapper wrapper;
@Before
public void setUp() {
wrapper = new LibvirtModifyStoragePoolCommandWrapper();
when(libvirtComputingResource.getStoragePoolMgr()).thenReturn(storagePoolManager);
}
@Test
public void testAddClvmStoragePoolWithoutDetails() {
when(command.getAdd()).thenReturn(true);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(null);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getHost()).thenReturn("192.168.1.100");
when(storageFilerTO.getPort()).thenReturn(0);
when(storageFilerTO.getPath()).thenReturn("/vg0");
when(storageFilerTO.getUserInfo()).thenReturn(null);
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM);
when(storagePool.getCapacity()).thenReturn(1000000L);
when(storagePool.getAvailable()).thenReturn(500000L);
when(storagePool.getDetails()).thenReturn(new HashMap<>());
when(storagePoolManager.createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM), anyMap()))
.thenReturn(storagePool);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success. Details: " + answer.getDetails(), answer.getResult());
assertTrue("Answer should be ModifyStoragePoolAnswer", answer instanceof ModifyStoragePoolAnswer);
// Verify the details were passed correctly
ArgumentCaptor<Map<String, String>> detailsCaptor = ArgumentCaptor.forClass(Map.class);
verify(storagePoolManager).createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM), detailsCaptor.capture());
Map<String, String> capturedDetails = detailsCaptor.getValue();
assertNotNull("Details should not be null", capturedDetails);
assertEquals("CLVM_SECURE_ZERO_FILL should default to false",
"false", capturedDetails.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL));
}
@Test
public void testAddClvmNgStoragePoolWithEmptyDetails() {
when(command.getAdd()).thenReturn(true);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(new HashMap<>());
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getHost()).thenReturn("192.168.1.100");
when(storageFilerTO.getPort()).thenReturn(0);
when(storageFilerTO.getPath()).thenReturn("/vg0");
when(storageFilerTO.getUserInfo()).thenReturn(null);
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM_NG);
when(storagePool.getCapacity()).thenReturn(2000000L);
when(storagePool.getAvailable()).thenReturn(1000000L);
when(storagePool.getDetails()).thenReturn(new HashMap<>());
when(storagePoolManager.createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM_NG), anyMap()))
.thenReturn(storagePool);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success", answer.getResult());
ArgumentCaptor<Map<String, String>> detailsCaptor = ArgumentCaptor.forClass(Map.class);
verify(storagePoolManager).createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM_NG), detailsCaptor.capture());
Map<String, String> capturedDetails = detailsCaptor.getValue();
assertNotNull("Details should not be null", capturedDetails);
assertEquals("CLVM_SECURE_ZERO_FILL should default to false",
"false", capturedDetails.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL));
}
@Test
public void testAddClvmStoragePoolWithExistingSecureZeroFillSetting() {
Map<String, String> details = new HashMap<>();
details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "true");
details.put("someOtherKey", "someValue");
when(command.getAdd()).thenReturn(true);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(details);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getHost()).thenReturn("192.168.1.100");
when(storageFilerTO.getPort()).thenReturn(0);
when(storageFilerTO.getPath()).thenReturn("/vg0");
when(storageFilerTO.getUserInfo()).thenReturn(null);
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM);
when(storagePool.getCapacity()).thenReturn(1000000L);
when(storagePool.getAvailable()).thenReturn(500000L);
when(storagePool.getDetails()).thenReturn(new HashMap<>());
when(storagePoolManager.createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM), anyMap()))
.thenReturn(storagePool);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success", answer.getResult());
ArgumentCaptor<Map<String, String>> detailsCaptor = ArgumentCaptor.forClass(Map.class);
verify(storagePoolManager).createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM), detailsCaptor.capture());
Map<String, String> capturedDetails = detailsCaptor.getValue();
assertNotNull("Details should not be null", capturedDetails);
assertEquals("CLVM_SECURE_ZERO_FILL should preserve existing value",
"true", capturedDetails.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL));
assertEquals("Other details should be preserved",
"someValue", capturedDetails.get("someOtherKey"));
}
@Test
public void testAddClvmStoragePoolPreservesOtherDetailsWhenAddingDefault() {
Map<String, String> details = new HashMap<>();
details.put("customKey1", "value1");
details.put("customKey2", "value2");
when(command.getAdd()).thenReturn(true);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(details);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getHost()).thenReturn("192.168.1.100");
when(storageFilerTO.getPort()).thenReturn(0);
when(storageFilerTO.getPath()).thenReturn("/vg0");
when(storageFilerTO.getUserInfo()).thenReturn(null);
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM_NG);
when(storagePool.getCapacity()).thenReturn(1000000L);
when(storagePool.getAvailable()).thenReturn(500000L);
when(storagePool.getDetails()).thenReturn(new HashMap<>());
when(storagePoolManager.createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM_NG), anyMap()))
.thenReturn(storagePool);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success", answer.getResult());
ArgumentCaptor<Map<String, String>> detailsCaptor = ArgumentCaptor.forClass(Map.class);
verify(storagePoolManager).createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM_NG), detailsCaptor.capture());
Map<String, String> capturedDetails = detailsCaptor.getValue();
assertNotNull("Details should not be null", capturedDetails);
assertEquals("CLVM_SECURE_ZERO_FILL should default to false",
"false", capturedDetails.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL));
assertEquals("Custom details should be preserved",
"value1", capturedDetails.get("customKey1"));
assertEquals("Custom details should be preserved",
"value2", capturedDetails.get("customKey2"));
}
@Test
public void testDeleteClvmStoragePoolSuccess() {
when(command.getAdd()).thenReturn(false);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(null);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM);
when(storagePoolManager.deleteStoragePool(StoragePoolType.CLVM, "pool-uuid", null))
.thenReturn(true);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success", answer.getResult());
verify(storagePoolManager).deleteStoragePool(StoragePoolType.CLVM, "pool-uuid", null);
}
@Test
public void testDeleteClvmNgStoragePoolSuccess() {
when(command.getAdd()).thenReturn(false);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(null);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM_NG);
when(storagePoolManager.deleteStoragePool(StoragePoolType.CLVM_NG, "pool-uuid", null))
.thenReturn(true);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success", answer.getResult());
verify(storagePoolManager).deleteStoragePool(StoragePoolType.CLVM_NG, "pool-uuid", null);
}
@Test
public void testDeleteClvmStoragePoolFailure() {
when(command.getAdd()).thenReturn(false);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(null);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM);
when(storagePoolManager.deleteStoragePool(StoragePoolType.CLVM, "pool-uuid", null))
.thenReturn(false);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertFalse("Answer should indicate failure", answer.getResult());
assertEquals("Failed to delete storage pool", answer.getDetails());
}
@Test
public void testAddClvmStoragePoolCreationFailure() {
when(command.getAdd()).thenReturn(true);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(null);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getHost()).thenReturn("192.168.1.100");
when(storageFilerTO.getPort()).thenReturn(0);
when(storageFilerTO.getPath()).thenReturn("/vg0");
when(storageFilerTO.getUserInfo()).thenReturn(null);
when(storageFilerTO.getType()).thenReturn(StoragePoolType.CLVM);
when(storagePoolManager.createStoragePool(eq("pool-uuid"), eq("192.168.1.100"), eq(0),
eq("/vg0"), eq(null), eq(StoragePoolType.CLVM), anyMap()))
.thenReturn(null);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertFalse("Answer should indicate failure", answer.getResult());
assertEquals(" Failed to create storage pool", answer.getDetails());
}
@Test
public void testAddNfsStoragePoolDoesNotSetClvmSecureZeroFill() {
when(command.getAdd()).thenReturn(true);
when(command.getPool()).thenReturn(storageFilerTO);
when(command.getDetails()).thenReturn(null);
when(storageFilerTO.getUuid()).thenReturn("pool-uuid");
when(storageFilerTO.getHost()).thenReturn("nfs.server.com");
when(storageFilerTO.getPort()).thenReturn(0);
when(storageFilerTO.getPath()).thenReturn("/export/nfs");
when(storageFilerTO.getUserInfo()).thenReturn(null);
when(storageFilerTO.getType()).thenReturn(StoragePoolType.NetworkFilesystem);
when(storagePool.getCapacity()).thenReturn(1000000L);
when(storagePool.getAvailable()).thenReturn(500000L);
when(storagePool.getDetails()).thenReturn(new HashMap<>());
when(storagePoolManager.createStoragePool(eq("pool-uuid"), eq("nfs.server.com"), eq(0),
eq("/export/nfs"), eq(null), eq(StoragePoolType.NetworkFilesystem), anyMap()))
.thenReturn(storagePool);
Answer answer = wrapper.execute(command, libvirtComputingResource);
assertTrue("Answer should indicate success", answer.getResult());
ArgumentCaptor<Map<String, String>> detailsCaptor = ArgumentCaptor.forClass(Map.class);
verify(storagePoolManager).createStoragePool(eq("pool-uuid"), eq("nfs.server.com"), eq(0),
eq("/export/nfs"), eq(null), eq(StoragePoolType.NetworkFilesystem), detailsCaptor.capture());
Map<String, String> capturedDetails = detailsCaptor.getValue();
assertNotNull("Details should not be null", capturedDetails);
assertEquals("CLVM_SECURE_ZERO_FILL gets added for all pools",
"false", capturedDetails.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL));
}
}

View File

@ -0,0 +1,363 @@
//
// 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.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 org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.libvirt.Connect;
import org.libvirt.LibvirtException;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LibvirtPostMigrationCommandWrapperTest {
@Mock
private LibvirtComputingResource libvirtComputingResource;
@Mock
private PostMigrationCommand postMigrationCommand;
@Mock
private VirtualMachineTO virtualMachineTO;
@Mock
private Connect connect;
private LibvirtPostMigrationCommandWrapper wrapper;
private static final String VM_NAME = "test-vm";
@Before
public void setUp() {
wrapper = new LibvirtPostMigrationCommandWrapper();
when(postMigrationCommand.getVmName()).thenReturn(VM_NAME);
when(postMigrationCommand.getVirtualMachine()).thenReturn(virtualMachineTO);
}
@Test
public void testExecute_NoClvmVolumes_Success() throws LibvirtException {
List<DiskDef> disks = createNonClvmDisks();
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
Assert.assertTrue(answer instanceof PostMigrationAnswer);
}
}
@Test
public void testExecute_ClvmVolumes_ConvertedToExclusiveMode() throws LibvirtException {
List<DiskDef> disks = createClvmDisks();
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class);
MockedStatic<LibvirtComputingResource> mockedResource = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
mockedResource.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE)
)).then(invocation -> null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedResource.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE)
), times(1));
}
}
@Test
public void testExecute_ClvmNgVolumes_ConvertedToExclusiveMode() throws LibvirtException {
List<DiskDef> disks = createClvmNgDisks();
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class);
MockedStatic<LibvirtComputingResource> mockedResource = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
mockedResource.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
any(),
any(),
any()
)).then(invocation -> null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedResource.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE)
), times(1));
}
}
@Test
public void testExecute_MixedVolumes_OnlyClvmConverted() throws LibvirtException {
List<DiskDef> disks = createMixedDisks();
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class);
MockedStatic<LibvirtComputingResource> mockedResource = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
mockedResource.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
any(),
any(),
any()
)).then(invocation -> null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedResource.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE)
), times(1));
}
}
@Test
public void testExecute_LibvirtException_ReturnsFailure() throws LibvirtException {
LibvirtException libvirtException = Mockito.mock(LibvirtException.class);
when(libvirtException.getMessage()).thenReturn("Connection failed");
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME))
.thenThrow(libvirtException);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertFalse(answer.getResult());
Assert.assertTrue(answer.getDetails().contains("Connection failed"));
}
}
@Test
public void testExecute_RuntimeException_ReturnsFailure() throws LibvirtException {
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME))
.thenThrow(new RuntimeException("Unexpected error"));
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertFalse(answer.getResult());
Assert.assertTrue(answer.getDetails().contains("Unexpected error"));
}
}
@Test
public void testExecute_NullVmName_ReturnsFailure() {
when(postMigrationCommand.getVmName()).thenReturn(null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertFalse(answer.getResult());
Assert.assertTrue(answer.getDetails().contains("VM or VM name is null"));
}
@Test
public void testExecute_NullVirtualMachine_ReturnsFailure() {
when(postMigrationCommand.getVirtualMachine()).thenReturn(null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertFalse(answer.getResult());
Assert.assertTrue(answer.getDetails().contains("VM or VM name is null"));
}
@Test
public void testExecute_EmptyDiskList_Success() throws LibvirtException {
List<DiskDef> disks = new ArrayList<>();
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
}
}
@Test
public void testExecute_MultipleClvmVolumes_AllConverted() throws LibvirtException {
List<DiskDef> disks = new ArrayList<>();
for (int i = 0; i < 5; i++) {
DiskDef disk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk.getDiskPath()).thenReturn("/dev/clvm-vg/volume" + i);
disks.add(disk);
}
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class);
MockedStatic<LibvirtComputingResource> mockedResource = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
mockedResource.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
any(),
any(),
any()
)).then(invocation -> null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedResource.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE)
), times(1));
}
}
@Test
public void testExecute_ClvmAndClvmNgMixed_BothConverted() throws LibvirtException {
List<DiskDef> disks = new ArrayList<>();
DiskDef clvmDisk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmDisk1.getDiskPath()).thenReturn("/dev/clvm-vg/volume1");
disks.add(clvmDisk1);
DiskDef clvmNgDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmNgDisk.getDiskPath()).thenReturn("/dev/clvmng-vg/volume2");
disks.add(clvmNgDisk);
DiskDef clvmDisk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmDisk2.getDiskPath()).thenReturn("/dev/clvm-vg/volume3");
disks.add(clvmDisk2);
try (MockedStatic<LibvirtConnection> mockedConnection = Mockito.mockStatic(LibvirtConnection.class);
MockedStatic<LibvirtComputingResource> mockedResource = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedConnection.when(() -> LibvirtConnection.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
mockedResource.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
any(),
any(),
any()
)).then(invocation -> null);
Answer answer = wrapper.execute(postMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedResource.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE)
), times(1));
}
}
private List<DiskDef> createNonClvmDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef disk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk1.getDiskPath()).thenReturn("/mnt/nfs/volume1.qcow2");
disks.add(disk1);
DiskDef disk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk2.getDiskPath()).thenReturn("/mnt/nfs/volume2.qcow2");
disks.add(disk2);
return disks;
}
private List<DiskDef> createClvmDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef disk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk1.getDiskPath()).thenReturn("/dev/clvm-vg/volume1");
disks.add(disk1);
DiskDef disk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk2.getDiskPath()).thenReturn("/dev/clvm-vg/volume2");
disks.add(disk2);
return disks;
}
private List<DiskDef> createClvmNgDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef disk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk1.getDiskPath()).thenReturn("/dev/clvmng-vg/volume1");
disks.add(disk1);
DiskDef disk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk2.getDiskPath()).thenReturn("/dev/clvmng-vg/volume2");
disks.add(disk2);
return disks;
}
private List<DiskDef> createMixedDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef clvmDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmDisk.getDiskPath()).thenReturn("/dev/clvm-vg/volume1");
disks.add(clvmDisk);
DiskDef nfsDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(nfsDisk.getDiskPath()).thenReturn("/mnt/nfs/volume2.qcow2");
disks.add(nfsDisk);
DiskDef clvmNgDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmNgDisk.getDiskPath()).thenReturn("/dev/clvmng-vg/volume3");
disks.add(clvmNgDisk);
return disks;
}
}

View File

@ -0,0 +1,282 @@
//
// 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 org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LibvirtPreMigrationCommandWrapperTest {
@Mock
private LibvirtComputingResource libvirtComputingResource;
@Mock
private PreMigrationCommand preMigrationCommand;
@Mock
private VirtualMachineTO virtualMachineTO;
@Mock
private Connect connect;
@Mock
private Domain domain;
@Mock
private LibvirtUtilitiesHelper libvirtUtilitiesHelper;
private LibvirtPreMigrationCommandWrapper wrapper;
private static final String VM_NAME = "test-vm";
@Before
public void setUp() {
wrapper = new LibvirtPreMigrationCommandWrapper();
when(preMigrationCommand.getVmName()).thenReturn(VM_NAME);
when(preMigrationCommand.getVirtualMachine()).thenReturn(virtualMachineTO);
when(libvirtComputingResource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper);
}
@Test
public void testExecute_NoClvmVolumes_Success() throws LibvirtException {
List<DiskDef> disks = createNonClvmDisks();
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenReturn(domain);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
Assert.assertEquals("Source host prepared for migration", answer.getDetails());
verify(domain, times(1)).free();
}
@Test
public void testExecute_ClvmVolumes_ConvertedToSharedMode() throws LibvirtException {
List<DiskDef> disks = createClvmDisks();
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenReturn(domain);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
try (MockedStatic<LibvirtComputingResource> mockedStatic = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedStatic.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.SHARED)
)).then(invocation -> null);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedStatic.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.SHARED)
), times(1));
verify(domain, times(1)).free();
}
}
@Test
public void testExecute_ClvmNgVolumes_ConvertedToSharedMode() throws LibvirtException {
List<DiskDef> disks = createClvmNgDisks();
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenReturn(domain);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
try (MockedStatic<LibvirtComputingResource> mockedStatic = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedStatic.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
any(),
any(),
any()
)).then(invocation -> null);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedStatic.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.SHARED)
), times(1));
}
}
@Test
public void testExecute_MixedVolumes_OnlyClvmConverted() throws LibvirtException {
List<DiskDef> disks = createMixedDisks();
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenReturn(domain);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
try (MockedStatic<LibvirtComputingResource> mockedStatic = Mockito.mockStatic(LibvirtComputingResource.class)) {
mockedStatic.when(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
any(),
any(),
any()
)).then(invocation -> null);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
mockedStatic.verify(() -> LibvirtComputingResource.modifyClvmVolumesStateForMigration(
eq(disks),
eq(virtualMachineTO),
eq(LibvirtComputingResource.ClvmVolumeState.SHARED)
), times(1));
}
}
@Test
public void testExecute_LibvirtException_ReturnsFailure() throws LibvirtException {
LibvirtException libvirtException = Mockito.mock(LibvirtException.class);
when(libvirtException.getMessage()).thenReturn("Connection failed");
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenThrow(libvirtException);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertFalse(answer.getResult());
Assert.assertTrue(answer.getDetails().contains("Failed to prepare source host"));
Assert.assertTrue(answer.getDetails().contains("Connection failed"));
}
@Test
public void testExecute_DomainFreeFails_StillReturnsSuccess() throws LibvirtException {
List<DiskDef> disks = createNonClvmDisks();
LibvirtException libvirtException = Mockito.mock(LibvirtException.class);
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenReturn(domain);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
Mockito.doThrow(libvirtException).when(domain).free();
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
}
@Test
public void testExecute_NullVmName_ReturnsFailure() throws LibvirtException {
LibvirtException libvirtException = Mockito.mock(LibvirtException.class);
when(preMigrationCommand.getVmName()).thenReturn(null);
when(libvirtUtilitiesHelper.getConnectionByVmName(null)).thenThrow(libvirtException);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertFalse(answer.getResult());
}
@Test
public void testExecute_EmptyDiskList_Success() throws LibvirtException {
List<DiskDef> disks = new ArrayList<>();
when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(connect);
when(connect.domainLookupByName(VM_NAME)).thenReturn(domain);
when(libvirtComputingResource.getDisks(connect, VM_NAME)).thenReturn(disks);
Answer answer = wrapper.execute(preMigrationCommand, libvirtComputingResource);
Assert.assertTrue(answer.getResult());
verify(domain, times(1)).free();
}
private List<DiskDef> createNonClvmDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef disk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk1.getDiskPath()).thenReturn("/mnt/nfs/volume1.qcow2");
disks.add(disk1);
DiskDef disk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk2.getDiskPath()).thenReturn("/mnt/nfs/volume2.qcow2");
disks.add(disk2);
return disks;
}
private List<DiskDef> createClvmDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef disk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk1.getDiskPath()).thenReturn("/dev/clvm-vg/volume1");
disks.add(disk1);
DiskDef disk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk2.getDiskPath()).thenReturn("/dev/clvm-vg/volume2");
disks.add(disk2);
return disks;
}
private List<DiskDef> createClvmNgDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef disk1 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk1.getDiskPath()).thenReturn("/dev/clvmng-vg/volume1");
disks.add(disk1);
DiskDef disk2 = Mockito.mock(DiskDef.class);
Mockito.lenient().when(disk2.getDiskPath()).thenReturn("/dev/clvmng-vg/volume2");
disks.add(disk2);
return disks;
}
private List<DiskDef> createMixedDisks() {
List<DiskDef> disks = new ArrayList<>();
DiskDef clvmDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmDisk.getDiskPath()).thenReturn("/dev/clvm-vg/volume1");
disks.add(clvmDisk);
DiskDef nfsDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(nfsDisk.getDiskPath()).thenReturn("/mnt/nfs/volume2.qcow2");
disks.add(nfsDisk);
DiskDef clvmNgDisk = Mockito.mock(DiskDef.class);
Mockito.lenient().when(clvmNgDisk.getDiskPath()).thenReturn("/dev/clvmng-vg/volume3");
disks.add(clvmNgDisk);
return disks;
}
}

View File

@ -22,6 +22,7 @@ import com.cloud.exception.InternalErrorException;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
import com.cloud.storage.Storage;
import com.cloud.storage.template.TemplateConstants;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
@ -53,6 +54,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import javax.naming.ConfigurationException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@ -499,4 +501,169 @@ public class KVMStorageProcessorTest {
Assert.assertEquals("vda", result);
}
@Test
public void testParseClvmSnapshotPath_ValidPath() {
String snapshotPath = "/dev/vg-storage/volume-uuid-123/snapshot-uuid-456";
Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM_NG;
try {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"parseClvmSnapshotPath", String.class, Storage.StoragePoolType.class);
method.setAccessible(true);
String[] result = (String[]) method.invoke(storageProcessorSpy, snapshotPath, poolType);
Assert.assertNotNull("Should return parsed array for valid path", result);
Assert.assertEquals("Should return 4 elements", 4, result.length);
Assert.assertEquals("VG name should be vg-storage", "vg-storage", result[0]);
Assert.assertEquals("Volume UUID should be volume-uuid-123", "volume-uuid-123", result[1]);
Assert.assertEquals("Snapshot UUID should be snapshot-uuid-456", "snapshot-uuid-456", result[2]);
Assert.assertNotNull("MD5 hash should be computed", result[3]);
} catch (Exception e) {
Assert.fail("Failed to test parseClvmSnapshotPath: " + e.getMessage());
}
}
@Test
public void testParseClvmSnapshotPath_InvalidPathFormat() {
String snapshotPath = "/dev/vg-storage/invalid";
Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM;
try {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"parseClvmSnapshotPath", String.class, Storage.StoragePoolType.class);
method.setAccessible(true);
String[] result = (String[]) method.invoke(storageProcessorSpy, snapshotPath, poolType);
Assert.assertNull("Should return null for invalid path format", result);
} catch (Exception e) {
Assert.fail("Failed to test parseClvmSnapshotPath: " + e.getMessage());
}
}
@Test
public void testDeleteClvmSnapshot_SuccessfulDeletion() {
String snapshotPath = "/dev/vg-storage/volume-uuid/snapshot-uuid";
Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM_NG;
try (MockedConstruction<Script> scriptConstruction = Mockito.mockConstruction(Script.class, (mock, context) -> {
Mockito.when(mock.execute()).thenReturn(null);
})) {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
method.setAccessible(true);
boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, true);
Assert.assertTrue("Should return true for successful deletion", result);
} catch (Exception e) {
Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
}
}
@Test
public void testDeleteClvmSnapshot_SnapshotAlreadyDeleted() {
String snapshotPath = "/dev/vg-storage/volume-uuid/snapshot-uuid";
Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM;
try (MockedConstruction<Script> scriptConstruction = Mockito.mockConstruction(Script.class, (mock, context) -> {
Mockito.when(mock.execute()).thenReturn("Error: snapshot does not exist");
})) {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
method.setAccessible(true);
boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, true);
Assert.assertTrue("Should return true when snapshot already deleted", result);
} catch (Exception e) {
Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
}
}
@Test
public void testDeleteClvmSnapshot_DeletionFailedWithoutCheck() {
String snapshotPath = "/dev/vg-storage/volume-uuid/snapshot-uuid";
Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM_NG;
try (MockedConstruction<Script> scriptConstruction = Mockito.mockConstruction(Script.class, (mock, context) -> {
Mockito.when(mock.execute()).thenReturn("Error: some other error");
})) {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
method.setAccessible(true);
boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, false);
Assert.assertFalse("Should return false when deletion fails", result);
} catch (Exception e) {
Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
}
}
@Test
public void testDeleteClvmSnapshot_InvalidPath() {
String snapshotPath = "/invalid/path";
Storage.StoragePoolType poolType = Storage.StoragePoolType.CLVM;
try {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"deleteClvmSnapshot", String.class, Storage.StoragePoolType.class, boolean.class);
method.setAccessible(true);
boolean result = (boolean) method.invoke(storageProcessorSpy, snapshotPath, poolType, true);
Assert.assertFalse("Should return false for invalid path", result);
} catch (Exception e) {
Assert.fail("Failed to test deleteClvmSnapshot: " + e.getMessage());
}
}
@Test
public void testComputeMd5Hash_ValidInput() {
String input = "snapshot-uuid-123";
try {
Method method = KVMStorageProcessor.class.getDeclaredMethod(
"computeMd5Hash", String.class);
method.setAccessible(true);
String result = (String) method.invoke(storageProcessorSpy, input);
Assert.assertNotNull("Should return non-null hash", result);
Assert.assertEquals("Hash should be 32 characters long (MD5)", 32, result.length());
Assert.assertTrue("Hash should contain only hex characters", result.matches("[0-9a-f]+"));
} catch (Exception e) {
Assert.fail("Failed to test computeMd5Hash: " + e.getMessage());
}
}
@Test
public void testComputeMd5Hash_EmptyInput() {
String input = "";
try {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"computeMd5Hash", String.class);
method.setAccessible(true);
String result = (String) method.invoke(storageProcessorSpy, input);
Assert.assertNotNull("Should return non-null hash even for empty input", result);
Assert.assertEquals("Hash should be 32 characters long (MD5)", 32, result.length());
} catch (Exception e) {
Assert.fail("Failed to test computeMd5Hash: " + e.getMessage());
}
}
@Test
public void testComputeMd5Hash_ConsistentResults() {
String input = "snapshot-uuid-456";
try {
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
"computeMd5Hash", String.class);
method.setAccessible(true);
String result1 = (String) method.invoke(storageProcessorSpy, input);
String result2 = (String) method.invoke(storageProcessorSpy, input);
Assert.assertEquals("Same input should produce same hash", result1, result2);
} catch (Exception e) {
Assert.fail("Failed to test computeMd5Hash: " + e.getMessage());
}
}
}

View File

@ -33,6 +33,7 @@ import org.junit.runner.RunWith;
import org.libvirt.Connect;
import org.libvirt.StoragePool;
import org.mockito.Mock;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@ -58,6 +59,9 @@ public class LibvirtStorageAdaptorTest {
MockedStatic<Script> mockScript;
// For mocking Script constructor
private MockedConstruction<Script> mockScriptConstruction;
@Spy
static LibvirtStorageAdaptor libvirtStorageAdaptor = new LibvirtStorageAdaptor(null);
@ -73,6 +77,9 @@ public class LibvirtStorageAdaptorTest {
public void tearDown() throws Exception {
libvirtConnectionMockedStatic.close();
mockScript.close();
if (mockScriptConstruction != null) {
mockScriptConstruction.close();
}
closeable.close();
}
@ -176,4 +183,168 @@ public class LibvirtStorageAdaptorTest {
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
}
@Test
public void testGetVolumeFromCLVMPool_ReturnsNullForNonCLVMPool() {
Storage.StoragePoolType type = Storage.StoragePoolType.NetworkFilesystem;
assert type != Storage.StoragePoolType.CLVM;
assert type != Storage.StoragePoolType.CLVM_NG;
}
@Test
public void testCLVMPoolTypeDetection_CLVM() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.CLVM);
Storage.StoragePoolType type = mockPool.getType();
assert type == Storage.StoragePoolType.CLVM;
assert type != Storage.StoragePoolType.CLVM_NG;
}
@Test
public void testCLVMPoolTypeDetection_CLVM_NG() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.CLVM_NG);
Storage.StoragePoolType type = mockPool.getType();
assert type == Storage.StoragePoolType.CLVM_NG;
assert type != Storage.StoragePoolType.CLVM;
}
@Test
public void testCLVMPoolLocalPathFormat() {
String vgName = "acsvg";
Mockito.when(mockPool.getLocalPath()).thenReturn(vgName);
String localPath = mockPool.getLocalPath();
assert localPath.equals(vgName);
assert !localPath.startsWith("/");
}
@Test
public void testCLVMNGPoolLocalPathFormat() {
String vgName = "acsvg";
Mockito.when(mockPool.getLocalPath()).thenReturn(vgName);
String localPath = mockPool.getLocalPath();
assert localPath.equals(vgName);
assert !localPath.startsWith("/");
}
@Test
public void testCLVMVolumePathFormat() {
String vgName = "acsvg";
String volumeUuid = UUID.randomUUID().toString();
String expectedPath = "/dev/" + vgName + "/" + volumeUuid;
assert expectedPath.startsWith("/dev/");
assert expectedPath.contains(vgName);
assert expectedPath.endsWith(volumeUuid);
}
@Test
public void testCLVMNGVolumePathFormat() {
String vgName = "acsvg";
String volumeUuid = UUID.randomUUID().toString();
String expectedPath = "/dev/" + vgName + "/" + volumeUuid;
assert expectedPath.startsWith("/dev/");
assert expectedPath.contains(vgName);
assert expectedPath.endsWith(volumeUuid);
}
@Test
public void testCLVMTemplateVolumeNamingConvention() {
String templateUuid = "550e8400-e29b-41d4-a716-446655440000";
String expectedLvName = "template-" + templateUuid;
assert expectedLvName.startsWith("template-");
assert expectedLvName.contains(templateUuid);
assert expectedLvName.equals("template-550e8400-e29b-41d4-a716-446655440000");
}
@Test
public void testCLVMTemplatePathFormat() {
// CLVM_NG template paths: /dev/vgname/template-{uuid}
String vgName = "acsvg";
String templateUuid = "550e8400-e29b-41d4-a716-446655440000";
String expectedPath = "/dev/" + vgName + "/template-" + templateUuid;
assert expectedPath.equals("/dev/acsvg/template-550e8400-e29b-41d4-a716-446655440000");
assert expectedPath.startsWith("/dev/");
assert expectedPath.contains("template-");
}
@Test
public void testCLVMPoolIsBlockDeviceStorage() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.CLVM);
Storage.StoragePoolType type = mockPool.getType();
boolean isBlockBased = (type == Storage.StoragePoolType.CLVM ||
type == Storage.StoragePoolType.CLVM_NG);
assert isBlockBased;
}
@Test
public void testCLVMNGPoolIsBlockDeviceStorage() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.CLVM_NG);
Storage.StoragePoolType type = mockPool.getType();
boolean isBlockBased = (type == Storage.StoragePoolType.CLVM ||
type == Storage.StoragePoolType.CLVM_NG);
assert isBlockBased;
}
@Test
public void testCLVMPoolSupportsSharedStorage() {
Mockito.when(mockPool.getType()).thenReturn(Storage.StoragePoolType.CLVM);
Storage.StoragePoolType type = mockPool.getType();
boolean supportsShared = (type == Storage.StoragePoolType.CLVM ||
type == Storage.StoragePoolType.CLVM_NG);
assert supportsShared;
}
@Test
public void testVolumeGroupNameExtraction() {
String vgName = "acsvg";
Mockito.when(mockPool.getLocalPath()).thenReturn(vgName);
String extractedVgName = mockPool.getLocalPath();
assert extractedVgName.equals("acsvg");
assert extractedVgName.matches("[a-zA-Z0-9_-]+");
}
@Test
public void testCLVMPoolDetailsContainSecureZeroFill() {
Map<String, String> details = new HashMap<>();
details.put("CLVM_SECURE_ZERO_FILL", "true");
boolean secureZeroEnabled = "true".equals(details.get("CLVM_SECURE_ZERO_FILL"));
assert secureZeroEnabled;
}
@Test
public void testCLVMPoolDetailsSecureZeroFillDisabled() {
Map<String, String> details = new HashMap<>();
details.put("CLVM_SECURE_ZERO_FILL", "false");
boolean secureZeroEnabled = "true".equals(details.get("CLVM_SECURE_ZERO_FILL"));
assert !secureZeroEnabled;
}
@Test
public void testCLVMPoolDetailsSecureZeroFillDefault() {
Map<String, String> details = new HashMap<>();
boolean secureZeroEnabled = "true".equals(details.get("CLVM_SECURE_ZERO_FILL"));
assert !secureZeroEnabled;
}
}

View File

@ -90,6 +90,13 @@ import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.agent.AgentManager;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver {
@Override
public Map<String, String> getCapabilities() {
@ -133,6 +140,12 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri
@Inject
private VolumeOrchestrationService volumeOrchestrationService;
@Inject
private ClvmPoolManager clvmPoolManager;
@Inject
private AgentManager agentMgr;
@Override
public DataTO getTO(DataObject data) {
return null;
@ -423,12 +436,19 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri
CommandResult result = new CommandResult();
try {
EndPoint ep = null;
if (snapshotOnPrimaryStore != null) {
ep = epSelector.select(snapshotOnPrimaryStore);
} else {
VolumeInfo volumeInfo = volFactory.getVolume(snapshot.getVolumeId(), DataStoreRole.Primary);
VolumeInfo volumeInfo = volFactory.getVolume(snapshot.getVolumeId(), DataStoreRole.Primary);
StoragePoolVO storagePool = primaryStoreDao.findById(volumeInfo.getPoolId());
if (storagePool != null && storagePool.getPoolType() == StoragePoolType.CLVM) {
ep = epSelector.select(volumeInfo);
} else {
if (snapshotOnPrimaryStore != null) {
ep = epSelector.select(snapshotOnPrimaryStore);
} else {
ep = epSelector.select(volumeInfo);
}
}
if ( ep == null ){
String errMsg = "No remote endpoint to send RevertSnapshotCommand, check if host or ssvm is down?";
logger.error(errMsg);
@ -456,6 +476,29 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri
CreateCmdResult result = new CreateCmdResult(null, null);
if (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) {
VirtualMachine attachedVm = vol.getAttachedVM();
boolean vmIsRunning = attachedVm != null && attachedVm.getHostId() != null;
if (!vmIsRunning) {
Long lockHostId = clvmPoolManager.getClvmLockHostId(vol.getId(), vol.getUuid(),
vol.getPath(), pool, true);
if (lockHostId != null) {
HostVO lockHost = hostDao.findById(lockHostId);
if (lockHost != null && lockHost.getStatus() == Status.Up) {
logger.debug("CLVM resize: routing to lock-holding host {} for volume {}",
lockHostId, vol.getUuid());
endpointsToRunResize = new long[]{lockHostId};
} else {
logger.warn("CLVM resize: lock host {} for volume {} is down or missing, " +
"keeping caller-provided hosts", lockHostId, vol.getUuid());
}
} else {
logger.warn("CLVM resize: no lock holder found for volume {}, " +
"keeping caller-provided hosts or epSelector", vol.getUuid());
}
}
}
// if hosts are provided, they are where the VM last ran. We can use that.
if (endpointsToRunResize == null || endpointsToRunResize.length == 0) {
EndPoint ep = epSelector.select(data, encryptionRequired);
@ -474,7 +517,21 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri
resizeCmd.setContextParam(DiskTO.PROTOCOL_TYPE, Storage.StoragePoolType.DatastoreCluster.toString());
}
try {
ResizeVolumeAnswer answer = (ResizeVolumeAnswer) storageMgr.sendToPool(pool, endpointsToRunResize, resizeCmd);
ResizeVolumeAnswer answer;
if (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) {
// For CLVM, send only to the determined host, without falling through to other hosts
try {
answer = (ResizeVolumeAnswer) agentMgr.send(endpointsToRunResize[0], resizeCmd);
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.error("CLVM resize failed to reach host {} for volume {}: {}",
endpointsToRunResize[0], vol.getUuid(), e.getMessage());
result.setResult(e.getMessage());
callback.complete(result);
return;
}
} else {
answer = (ResizeVolumeAnswer) storageMgr.sendToPool(pool, endpointsToRunResize, resizeCmd);
}
if (answer != null && answer.getResult()) {
long finalSize = answer.getNewSize();
logger.debug("Resize: volume started at size: " + toHumanReadableSize(vol.getSize()) + " and ended at size: " + toHumanReadableSize(finalSize));

View File

@ -233,6 +233,11 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStor
parameters.setHost(storageHost);
parameters.setPort(0);
parameters.setPath(hostPath.replaceFirst("/", ""));
} else if (scheme.equalsIgnoreCase("clvm_ng")) {
parameters.setType(StoragePoolType.CLVM_NG);
parameters.setHost(storageHost);
parameters.setPort(0);
parameters.setPath(hostPath.replaceFirst("/", ""));
} else if (scheme.equalsIgnoreCase("rbd")) {
if (port == -1) {
port = 0;
@ -329,7 +334,7 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStor
if (existingUuid != null) {
uuid = (String)existingUuid;
} else if (scheme.equalsIgnoreCase("sharedmountpoint") || scheme.equalsIgnoreCase("clvm")) {
} else if (scheme.equalsIgnoreCase("sharedmountpoint")) {
uuid = UUID.randomUUID().toString();
} else if ("PreSetup".equalsIgnoreCase(scheme) && !HypervisorType.VMware.equals(hypervisorType)) {
uuid = hostPath.replace("/", "");

View File

@ -0,0 +1,286 @@
// 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.datastore.driver;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
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.EndPointSelector;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
import org.apache.cloudstack.storage.volume.VolumeObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.storage.ResizeVolumeAnswer;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.Storage;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.vm.VirtualMachine;
@RunWith(MockitoJUnitRunner.class)
public class CloudStackPrimaryDataStoreDriverImplTest {
@InjectMocks
private CloudStackPrimaryDataStoreDriverImpl driver;
@Mock
private ClvmPoolManager clvmPoolManager;
@Mock
private AgentManager agentMgr;
@Mock
private HostDao hostDao;
@Mock
private EndPointSelector epSelector;
@Mock
private StorageManager storageMgr;
@Mock
private VolumeObject vol;
private StoragePool pool;
@Mock
private HostVO lockHost;
@Mock
private AsyncCompletionCallback<CreateCmdResult> callback;
private static final long LOCK_HOST_ID = 42L;
private static final long OTHER_HOST_ID = 99L;
private static final String VOLUME_UUID = "test-vol-uuid";
private static final String VOLUME_PATH = "vm-1-disk-0";
@Before
public void setUp() {
pool = Mockito.mock(StoragePool.class, withSettings().extraInterfaces(DataStore.class));
when(vol.getUuid()).thenReturn(VOLUME_UUID);
when(vol.getId()).thenReturn(1L);
when(vol.getPath()).thenReturn(VOLUME_PATH);
when(vol.getSize()).thenReturn(10L * 1024 * 1024 * 1024);
when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM_NG);
when(pool.getParent()).thenReturn(0L);
when(pool.getPath()).thenReturn("/vg-test");
when(vol.getDataStore()).thenReturn((DataStore) pool);
when(hostDao.findById(LOCK_HOST_ID)).thenReturn(lockHost);
when(lockHost.getStatus()).thenReturn(Status.Up);
}
private ResizeVolumeAnswer mockSuccessAnswer(long newSize) throws Exception {
ResizeVolumeAnswer answer = Mockito.mock(ResizeVolumeAnswer.class);
when(answer.getResult()).thenReturn(true);
when(answer.getNewSize()).thenReturn(newSize);
return answer;
}
private void stubAgentSend(long hostId, Answer answer) throws Exception {
Mockito.doReturn(answer).when(agentMgr).send(eq(hostId), any(Command.class));
}
private void verifyAgentSend(long hostId) throws Exception {
verify(agentMgr).send(eq(hostId), any(Command.class));
}
private void verifyAgentSendNever(long hostId) throws Exception {
verify(agentMgr, never()).send(eq(hostId), any(Command.class));
}
private ResizeVolumePayload makePayload(long[] hosts) {
ResizeVolumePayload p = new ResizeVolumePayload(
20L * 1024 * 1024 * 1024, null, null, null, false, "test-vm", hosts, false);
when(vol.getpayload()).thenReturn(p);
return p;
}
@Test
public void testResize_clvmNg_vmRunning_usesCallerHost() throws Exception {
VirtualMachine runningVm = Mockito.mock(VirtualMachine.class);
when(runningVm.getHostId()).thenReturn(OTHER_HOST_ID);
when(vol.getAttachedVM()).thenReturn(runningVm);
makePayload(new long[]{OTHER_HOST_ID});
stubAgentSend(OTHER_HOST_ID, mockSuccessAnswer(20L * 1024 * 1024 * 1024));
driver.resize(vol, callback);
verify(clvmPoolManager, never()).getClvmLockHostId(anyLong(), anyString(), anyString(), any(), anyBoolean());
verifyAgentSend(OTHER_HOST_ID);
}
@Test
public void testResize_clvmNg_vmStopped_routesToLockHost() throws Exception {
VirtualMachine stoppedVm = Mockito.mock(VirtualMachine.class);
when(stoppedVm.getHostId()).thenReturn(null);
when(vol.getAttachedVM()).thenReturn(stoppedVm);
makePayload(new long[]{OTHER_HOST_ID}); // stale lastHostId from caller
when(clvmPoolManager.getClvmLockHostId(anyLong(), anyString(), anyString(), any(), eq(true)))
.thenReturn(LOCK_HOST_ID);
stubAgentSend(LOCK_HOST_ID, mockSuccessAnswer(20L * 1024 * 1024 * 1024));
driver.resize(vol, callback);
// Must override stale lastHostId with actual lock host
verifyAgentSend(LOCK_HOST_ID);
verifyAgentSendNever(OTHER_HOST_ID);
}
@Test
public void testResize_clvmNg_vmStopped_lockHostDown_keepsCallerHosts() throws Exception {
VirtualMachine stoppedVm = Mockito.mock(VirtualMachine.class);
when(stoppedVm.getHostId()).thenReturn(null);
when(vol.getAttachedVM()).thenReturn(stoppedVm);
makePayload(new long[]{OTHER_HOST_ID});
when(clvmPoolManager.getClvmLockHostId(anyLong(), anyString(), anyString(), any(), eq(true)))
.thenReturn(LOCK_HOST_ID);
when(lockHost.getStatus()).thenReturn(Status.Disconnected);
stubAgentSend(OTHER_HOST_ID, mockSuccessAnswer(20L * 1024 * 1024 * 1024));
driver.resize(vol, callback);
verifyAgentSend(OTHER_HOST_ID);
}
@Test
public void testResize_clvmNg_detached_routesToLockHost() throws Exception {
when(vol.getAttachedVM()).thenReturn(null);
makePayload(null); // no hosts from caller
when(clvmPoolManager.getClvmLockHostId(anyLong(), anyString(), anyString(), any(), eq(true)))
.thenReturn(LOCK_HOST_ID);
stubAgentSend(LOCK_HOST_ID, mockSuccessAnswer(20L * 1024 * 1024 * 1024));
driver.resize(vol, callback);
verifyAgentSend(LOCK_HOST_ID);
}
@Test
public void testResize_clvmNg_detached_noLockHolder_fallsToEpSelector() throws Exception {
when(vol.getAttachedVM()).thenReturn(null);
makePayload(null);
when(clvmPoolManager.getClvmLockHostId(anyLong(), anyString(), anyString(), any(), eq(true)))
.thenReturn(null); // no lock holder found
EndPoint ep = Mockito.mock(EndPoint.class);
when(ep.getId()).thenReturn(LOCK_HOST_ID);
when(epSelector.select(any(), anyBoolean())).thenReturn(ep);
stubAgentSend(LOCK_HOST_ID, mockSuccessAnswer(20L * 1024 * 1024 * 1024));
driver.resize(vol, callback);
verify(epSelector).select(any(), anyBoolean());
verifyAgentSend(LOCK_HOST_ID);
}
@Test
public void testResize_clvmNg_agentUnavailable_failsFast() throws Exception {
VirtualMachine runningVm = Mockito.mock(VirtualMachine.class);
when(runningVm.getHostId()).thenReturn(OTHER_HOST_ID);
when(vol.getAttachedVM()).thenReturn(runningVm);
makePayload(new long[]{OTHER_HOST_ID});
Mockito.doThrow(new AgentUnavailableException("host down", OTHER_HOST_ID))
.when(agentMgr).send(eq(OTHER_HOST_ID), any(Command.class));
driver.resize(vol, callback);
verify(callback).complete(any());
verify(storageMgr, never()).sendToPool(any(StoragePool.class), any(long[].class), any());
}
@Test
public void testResize_clvmNg_operationTimedOut_failsFast() throws Exception {
VirtualMachine runningVm = Mockito.mock(VirtualMachine.class);
when(runningVm.getHostId()).thenReturn(OTHER_HOST_ID);
when(vol.getAttachedVM()).thenReturn(runningVm);
makePayload(new long[]{OTHER_HOST_ID});
Mockito.doThrow(new OperationTimedoutException(null, OTHER_HOST_ID, 0, 0, false))
.when(agentMgr).send(eq(OTHER_HOST_ID), any(Command.class));
driver.resize(vol, callback);
verify(callback).complete(any());
verify(storageMgr, never()).sendToPool(any(StoragePool.class), any(long[].class), any());
}
@Test
public void testResize_nonClvm_usesSendToPool() throws Exception {
when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
makePayload(null);
EndPoint ep = Mockito.mock(EndPoint.class);
when(ep.getId()).thenReturn(OTHER_HOST_ID);
when(epSelector.select(any(), anyBoolean())).thenReturn(ep);
ResizeVolumeAnswer answer = mockSuccessAnswer(20L * 1024 * 1024 * 1024);
when(storageMgr.sendToPool(any(StoragePool.class), any(long[].class), any())).thenReturn(answer);
driver.resize(vol, callback);
verify(storageMgr).sendToPool(any(StoragePool.class), any(long[].class), any());
verify(agentMgr, never()).send(anyLong(), any(Command.class));
verify(clvmPoolManager, never()).getClvmLockHostId(anyLong(), anyString(), anyString(), any(), anyBoolean());
}
@Test
public void testResize_clvmLegacy_vmStopped_routesToLockHost() throws Exception {
when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
VirtualMachine stoppedVm = Mockito.mock(VirtualMachine.class);
when(stoppedVm.getHostId()).thenReturn(null);
when(vol.getAttachedVM()).thenReturn(stoppedVm);
makePayload(new long[]{OTHER_HOST_ID});
when(clvmPoolManager.getClvmLockHostId(anyLong(), anyString(), anyString(), any(), eq(true)))
.thenReturn(LOCK_HOST_ID);
stubAgentSend(LOCK_HOST_ID, mockSuccessAnswer(20L * 1024 * 1024 * 1024));
driver.resize(vol, callback);
verifyAgentSend(LOCK_HOST_ID);
verifyAgentSendNever(OTHER_HOST_ID);
}
}

View File

@ -55,6 +55,10 @@
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.exclusions>engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java</sonar.exclusions>
<sonar.exclusions>api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java</sonar.exclusions>
<sonar.exclusions>core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java</sonar.exclusions>
<sonar.exclusions>core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java</sonar.exclusions>
<sonar.exclusions>core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java</sonar.exclusions>
<sonar.exclusions>core/src/main/java/org/apache/cloudstack/storage/command/ClvmLockTransferCommand.java</sonar.exclusions>
<!-- Build properties -->
<cs.jdk.version>11</cs.jdk.version>

View File

@ -58,15 +58,25 @@ is_lv() {
}
get_vg() {
lvm lvs --noheadings --unbuffered --separator=/ "${1}" | cut -d '/' -f 2
lvm lvs --noheadings --unbuffered --separator=/ "${1}" | cut -d '/' -f 2 | tr -d ' '
}
get_lv() {
lvm lvs --noheadings --unbuffered --separator=/ "${1}" | cut -d '/' -f 1
lvm lvs --noheadings --unbuffered --separator=/ "${1}" | cut -d '/' -f 1 | tr -d ' '
}
double_hyphens() {
echo ${1} | sed -e "s/-/--/g"
# Check if a block device contains QCOW2 data (CLVM_NG)
is_qcow2_on_block_device() {
local disk=$1
# Must be a block device
if [ ! -b "${disk}" ]; then
return 1
fi
# Check if it contains QCOW2 data using qemu-img info
${qemu_img} info "${disk}" 2>/dev/null | grep -q "file format: qcow2"
return $?
}
create_snapshot() {
@ -79,28 +89,36 @@ create_snapshot() {
if [ ${dmsnapshot} = "yes" ] && [ "$islv_ret" == "1" ]; then
local lv=`get_lv ${disk}`
local vg=`get_vg ${disk}`
local lv_dm=`double_hyphens ${lv}`
local vg_dm=`double_hyphens ${vg}`
local lvdevice=/dev/mapper/${vg_dm}-${lv_dm}
local lv_bytes=`blockdev --getsize64 ${lvdevice}`
local lv_sectors=`blockdev --getsz ${lvdevice}`
local lv_bytes=`blockdev --getsize64 ${disk}`
lvm lvcreate --size ${lv_bytes}b --name "${snapshotname}-cow" ${vg} >&2 || return 2
dmsetup suspend ${vg_dm}-${lv_dm} >&2
if dmsetup info -c --noheadings -o name ${vg_dm}-${lv_dm}-real > /dev/null 2>&1; then
echo "0 ${lv_sectors} snapshot ${lvdevice}-real /dev/mapper/${vg_dm}-${snapshotname}--cow p 64" | \
dmsetup create "${vg_dm}-${snapshotname}" >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
dmsetup resume "${vg_dm}-${snapshotname}" >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
else
dmsetup table ${vg_dm}-${lv_dm} | dmsetup create ${vg_dm}-${lv_dm}-real >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
dmsetup resume ${vg_dm}-${lv_dm}-real >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
echo "0 ${lv_sectors} snapshot ${lvdevice}-real /dev/mapper/${vg_dm}-${snapshotname}--cow p 64" | \
dmsetup create "${vg_dm}-${snapshotname}" >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
echo "0 ${lv_sectors} snapshot-origin ${lvdevice}-real" | \
dmsetup load ${vg_dm}-${lv_dm} >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
dmsetup resume "${vg_dm}-${snapshotname}" >&2 || ( destroy_snapshot ${disk} "${snapshotname}"; return 2 )
# Calculate snapshot size (10% of origin size, minimum 100MB, maximum 10GB)
local snapshot_size=$((lv_bytes / 10))
local min_size=$((100 * 1024 * 1024)) # 100MB
local max_size=$((10 * 1024 * 1024 * 1024)) # 10GB
if [ ${snapshot_size} -lt ${min_size} ]; then
snapshot_size=${min_size}
elif [ ${snapshot_size} -gt ${max_size} ]; then
snapshot_size=${max_size}
fi
# Round to nearest 512-byte multiple (LVM requirement)
snapshot_size=$(((snapshot_size + 511) / 512 * 512))
# Create LVM snapshot using native command
lvm lvcreate -L ${snapshot_size}b -s -n "${snapshotname}" "${vg}/${lv}" >&2
if [ $? -gt 0 ]; then
printf "***Failed to create LVM snapshot ${snapshotname} for ${vg}/${lv}\n" >&2
return 2
fi
# Activate the snapshot
lvm lvchange --yes -ay "${vg}/${snapshotname}" >&2
if [ $? -gt 0 ]; then
printf "***Failed to activate LVM snapshot ${snapshotname}\n" >&2
lvm lvremove -f "${vg}/${snapshotname}" >&2
return 2
fi
dmsetup resume "${vg_dm}-${lv_dm}" >&2
elif [ -f "${disk}" ]; then
$qemu_img snapshot -c "$snapshotname" $disk
@ -128,29 +146,32 @@ destroy_snapshot() {
local disk=$1
local snapshotname="$2"
local failed=0
# If the disk path does not exist any more, assume volume was deleted and
# the snapshot is already gone — return success to let caller proceed.
if [ ! -e "${disk}" ]; then
printf "Disk %s does not exist; assuming volume removed and snapshot %s is deleted\n" "${disk}" "${snapshotname}" >&2
return 0
fi
is_lv ${disk}
islv_ret=$?
if [ "$islv_ret" == "1" ]; then
local lv=`get_lv ${disk}`
local vg=`get_vg ${disk}`
local lv_dm=`double_hyphens ${lv}`
local vg_dm=`double_hyphens ${vg}`
if [ -e /dev/mapper/${vg_dm}-${lv_dm}-real ]; then
local dm_refcount=`dmsetup info -c --noheadings -o open ${vg_dm}-${lv_dm}-real`
if [ ${dm_refcount} -le 2 ]; then
dmsetup suspend ${vg_dm}-${lv_dm} >&2
dmsetup table ${vg_dm}-${lv_dm}-real | dmsetup load ${vg_dm}-${lv_dm} >&2
dmsetup resume ${vg_dm}-${lv_dm}
dmsetup remove "${vg_dm}-${snapshotname}"
dmsetup remove ${vg_dm}-${lv_dm}-real
else
dmsetup remove "${vg_dm}-${snapshotname}"
fi
else
dmsetup remove "${vg_dm}-${snapshotname}"
# Check if snapshot exists
if ! lvm lvs "${vg}/${snapshotname}" > /dev/null 2>&1; then
printf "Snapshot ${vg}/${snapshotname} does not exist or was already deleted\n" >&2
return 0
fi
lvm lvremove -f "${vg}/${snapshotname}" >&2
if [ $? -gt 0 ]; then
printf "***Failed to remove LVM snapshot ${vg}/${snapshotname}\n" >&2
return 2
fi
lvm lvremove -f "${vg}/${snapshotname}-cow"
elif [ -f $disk ]; then
#delete all the existing snapshots
$qemu_img snapshot -l $disk |tail -n +3|awk '{print $1}'|xargs -I {} $qemu_img snapshot -d {} $disk >&2
@ -170,12 +191,36 @@ rollback_snapshot() {
local disk=$1
local snapshotname="$2"
local failed=0
is_lv ${disk}
islv_ret=$?
$qemu_img snapshot -a $snapshotname $disk
if [ ${dmrollback} = "yes" ] && [ "$islv_ret" == "1" ]; then
local lv=`get_lv ${disk}`
local vg=`get_vg ${disk}`
if [ $? -gt 0 ]
then
printf "***Failed to apply snapshot $snapshotname for path $disk\n" >&2
# Check if snapshot exists
if ! lvm lvs "${vg}/${snapshotname}" > /dev/null 2>&1; then
printf "***Snapshot ${vg}/${snapshotname} does not exist\n" >&2
return 1
fi
# Use lvconvert --merge to rollback
lvm lvconvert --merge "${vg}/${snapshotname}" >&2
if [ $? -gt 0 ]; then
printf "***Failed to merge/rollback snapshot ${snapshotname} for ${vg}/${lv}\n" >&2
return 1
fi
elif [ -f ${disk} ]; then
# File-based snapshot rollback using qemu-img
$qemu_img snapshot -a $snapshotname $disk
if [ $? -gt 0 ]
then
printf "***Failed to apply snapshot $snapshotname for path $disk\n" >&2
failed=1
fi
else
printf "***Failed to rollback snapshot $snapshotname, undefined type $disk\n" >&2
failed=1
fi
@ -202,22 +247,31 @@ backup_snapshot() {
is_lv ${disk}
islv_ret=$?
# Both CLVM and CLVM_NG use LVM snapshot backup, but with different formats
if [ ${dmsnapshot} = "yes" ] && [ "$islv_ret" == "1" ] ; then
local vg=`get_vg ${disk}`
local vg_dm=`double_hyphens ${vg}`
local scriptdir=`dirname ${0}`
local input_format="raw"
if ! dmsetup info -c --noheadings -o name ${vg_dm}-${snapshotname} > /dev/null 2>&1; then
# Check if snapshot exists using native LVM command
if ! lvm lvs "${vg}/${snapshotname}" > /dev/null 2>&1; then
printf "Disk ${disk} has no snapshot called ${snapshotname}.\n" >&2
return 1
fi
qemuimg_ret=$($qemu_img $forceShareFlag -f raw -O qcow2 "/dev/mapper/${vg_dm}-${snapshotname}" "${destPath}/${destName}")
# Detect if this is CLVM_NG (QCOW2 on block device)
if is_qcow2_on_block_device "${disk}"; then
input_format="qcow2"
printf "Detected CLVM_NG volume, using qcow2 format for backup\n" >&2
fi
# Use native LVM path for backup with appropriate format
qemuimg_ret=$($qemu_img convert $forceShareFlag -f ${input_format} -O qcow2 "/dev/${vg}/${snapshotname}" "${destPath}/${destName}" 2>&1)
ret_code=$?
if [ $ret_code -gt 0 ] && [[ $qemuimg_ret == *"snapshot: invalid option -- 'U'"* ]]
if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]])
then
forceShareFlag=""
$qemu_img $forceShareFlag -f raw -O qcow2 "/dev/mapper/${vg_dm}-${snapshotname}" "${destPath}/${destName}"
$qemu_img convert $forceShareFlag -f ${input_format} -O qcow2 "/dev/${vg}/${snapshotname}" "${destPath}/${destName}"
ret_code=$?
fi
if [ $ret_code -gt 0 ]
@ -240,9 +294,9 @@ backup_snapshot() {
# Backup VM snapshot
qemuimg_ret=$($qemu_img snapshot $forceShareFlag -l $disk 2>&1)
ret_code=$?
if [ $ret_code -gt 0 ] && [[ $qemuimg_ret == *"snapshot: invalid option -- 'U'"* ]]; then
if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]]); then
forceShareFlag=""
qemuimg_ret=$($qemu_img snapshot $forceShareFlag -l $disk)
qemuimg_ret=$($qemu_img snapshot $forceShareFlag -l $disk 2>&1)
ret_code=$?
fi
@ -251,11 +305,11 @@ backup_snapshot() {
return 1
fi
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1 > /dev/null)
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1)
ret_code=$?
if [ $ret_code -gt 0 ] && [[ $qemuimg_ret == *"convert: invalid option -- 'U'"* ]]; then
if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]]); then
forceShareFlag=""
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1 > /dev/null)
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1)
ret_code=$?
fi
@ -279,7 +333,19 @@ backup_snapshot() {
revert_snapshot() {
local snapshotPath=$1
local destPath=$2
${qemu_img} convert -f qcow2 -O qcow2 "$snapshotPath" "$destPath" || \
local output_format="qcow2"
# Check if destination is a block device
if [ -b "$destPath" ]; then
if is_qcow2_on_block_device "${destPath}"; then
output_format="qcow2"
printf "Detected CLVM_NG volume %s, preserving QCOW2 format for revert\n" "${destPath}" >&2
else
output_format="raw"
fi
fi
${qemu_img} convert -f qcow2 -O ${output_format} "$snapshotPath" "$destPath" || \
( printf "${qemu_img} failed to revert snapshot ${snapshotPath} to disk ${destPath}.\n" >&2; return 2 )
return 0
}

View File

@ -156,6 +156,147 @@ resizelvm() {
log "performed successful resize - dm:$dmname currentsize:$currentsize newsize:$newsize path:$path type:$ptype vmname:$vmname live:$liveresize shrink:$shrink"
}
resizeclvmng() {
local liveresize='false'
if ! `lvresize --version > /dev/null 2>&1`
then
log "unable to resolve executable 'lvresize'" 1
exit 1
fi
if ! `qemu-img info /dev/null > /dev/null 2>&1`
then
log "unable to resolve executable 'qemu-img'" 1
exit 1
fi
if ! `virsh --version > /dev/null 2>&1`
then
log "unable to resolve executable 'virsh'" 1
exit 1
fi
vgname=$(echo "$path" | awk -F'/' '{print $3}')
if [[ -z "$vgname" ]]
then
log "unable to derive VG name from path $path" 1
exit 1
fi
# Query PE size in bytes (strip trailing 'B' if present)
pe_size_raw=$(vgdisplay --units b -C --noheadings -o vg_extent_size "$vgname" 2>/dev/null | tr -d ' ')
pe_size_raw="${pe_size_raw%B}"
if [[ -z "$pe_size_raw" || ! "$pe_size_raw" =~ ^[0-9]+$ ]]
then
log "could not query PE size for VG $vgname, defaulting to 4MiB"
pe_size=$((4 * 1024 * 1024))
else
pe_size=$pe_size_raw
fi
# Calculate new LV size: newsize (virtual) + QCOW2 metadata overhead, rounded up to PE
# QCOW2 cluster size = 64KiB, L2 table covers 4096 clusters each
cluster_size=$((64 * 1024))
l2_multiplier=4096
num_data_clusters=$(( (newsize + cluster_size - 1) / cluster_size ))
num_l2_clusters=$(( (num_data_clusters + l2_multiplier - 1) / l2_multiplier ))
l2_table_size=$(( num_l2_clusters * cluster_size ))
refcount_table_size=$l2_table_size
header_overhead=$(( 2 * 1024 * 1024 ))
metadata_overhead=$(( l2_table_size + refcount_table_size + header_overhead ))
target_lv_size=$(( newsize + metadata_overhead ))
# Round up to PE boundary
lv_size=$(( ((target_lv_size + pe_size - 1) / pe_size) * pe_size ))
pe_aligned_newsize=$(( ((newsize + pe_size - 1) / pe_size) * pe_size ))
log "CLVM_NG resize: path=$path vg=$vgname pe_size=${pe_size}B virtual=${newsize}B pe_aligned_virtual=${pe_aligned_newsize}B metadata=${metadata_overhead}B lv_size=${lv_size}B"
# Use -U (force-share) if qemu-img >= 2.10 so we can read info even when
# QEMU has the file open with an exclusive lock (VM running case)
qemu_force_share_flag=""
regex=".*version\s([0-9]+)\.([0-9]+).*"
content=$(qemu-img --version | grep version)
if [[ $content =~ $regex ]]
then
ver_major="${BASH_REMATCH[1]}"
ver_minor="${BASH_REMATCH[2]}"
if [[ ${ver_major} -gt 2 ]] || [[ ${ver_major} -eq 2 && ${ver_minor} -ge 10 ]]
then
qemu_force_share_flag="-U"
fi
fi
actualsize=`qemu-img info $qemu_force_share_flag $path | grep "virtual size" | sed -re 's/^.*\(([0-9]+).*$/\1/g'`
if [[ -z "$actualsize" ]]
then
log "unable to determine current QCOW2 virtual size for $path" 1
exit 1
fi
if [ $actualsize -ne $currentsize ]
then
log "disk isn't the size we think it is: cloudstack said $currentsize, disk said $actualsize."
fi
# Shrink guard on virtual size
if [ $actualsize -gt $newsize ]
then
if [ "$shrink" == "false" ]
then
log "result would shrink the volume from $actualsize to $newsize, but shrink wasn't confirmed" 1
exit 1
fi
fi
# Step 1: resize the LV to accommodate new virtual size + overhead
output=`lvresize -f -L ${lv_size}B $path 2>&1`
retval=$?
if [ -z $retval ] || [ $retval -ne 0 ]
then
log "lvresize failed: $output" 1
exit 1
fi
log "lvresize succeeded: $path to ${lv_size}B"
# Step 2: resize the QCOW2 virtual disk.
# If the VM is running QEMU has the file open, calling qemu-img resize on an open file
# is unsafe. Instead use virsh blockresize which tells QEMU to resize the virtual disk
# safely from within. If the VM is stopped, qemu-img resize is safe to use directly.
if `virsh domstate $vmname >/dev/null 2>&1`
then
log "VM $vmname is running, using virsh blockresize for safe live QCOW2 resize"
sizeinkb=$(($pe_aligned_newsize/1024))
devicepath=$(virsh domblklist $vmname | grep $path | awk '{print $1}')
if [[ -z "$devicepath" ]]
then
log "could not find device alias for $path in VM $vmname domblklist" 1
exit 1
fi
output=`virsh blockresize --path $devicepath --size $sizeinkb $vmname 2>&1`
retval=$?
if [ -z $retval ] || [ $retval -ne 0 ]
then
log "virsh blockresize failed: $output" 1
exit 1
fi
liveresize='true'
log "virsh blockresize succeeded: $vmname $devicepath to ${sizeinkb}KiB virtual"
else
log "VM $vmname is not running, using qemu-img resize"
output=`qemu-img resize $path $pe_aligned_newsize 2>&1`
retval=$?
if [ -z $retval ] || [ $retval -ne 0 ]
then
log "qemu-img resize failed: $output" 1
exit 1
fi
log "qemu-img resize succeeded: $path to ${pe_aligned_newsize}B virtual"
fi
log "performed successful CLVM_NG resize - currentsize:$currentsize newsize:$newsize lv_size:$lv_size path:$path vmname:$vmname live:$liveresize shrink:$shrink"
}
resizeqcow2() {
##### sanity checks #####
@ -271,6 +412,9 @@ shouldwelog=1 #set this to 1 while debugging to get output in /var/log/cloudstac
if [ "$ptype" == "CLVM" ]
then
resizelvm
elif [ "$ptype" == "CLVM_NG" ]
then
resizeclvmng
elif [ "$ptype" == "QCOW2" ]
then
resizeqcow2

View File

@ -23,6 +23,7 @@ import java.util.Map;
import javax.inject.Inject;
import com.cloud.storage.clvm.ClvmPoolManager;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.response.StoragePoolResponse;
@ -183,7 +184,11 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase<StoragePoolJoinVO, Lo
poolResponse.setTags(pool.getTag());
poolResponse.setStorageAccessGroups(pool.getStorageAccessGroup());
poolResponse.setIsTagARule(pool.getIsTagARule());
poolResponse.setOverProvisionFactor(Double.toString(CapacityManager.StorageOverprovisioningFactor.valueIn(pool.getId())));
if (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) {
poolResponse.setOverProvisionFactor(String.valueOf(1));
} else {
poolResponse.setOverProvisionFactor(Double.toString(CapacityManager.StorageOverprovisioningFactor.valueIn(pool.getId())));
}
poolResponse.setManaged(storagePool.isManaged());
Map<String, String> details = ApiDBUtils.getResourceDetails(pool.getId(), ResourceTag.ResourceObjectType.Storage);
poolResponse.setDetails(details);
@ -274,7 +279,11 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase<StoragePoolJoinVO, Lo
}
}
poolResponse.setOverProvisionFactor(Double.toString(CapacityManager.StorageOverprovisioningFactor.valueIn(pool.getId())));
if (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) {
poolResponse.setOverProvisionFactor(String.valueOf(1));
} else {
poolResponse.setOverProvisionFactor(Double.toString(CapacityManager.StorageOverprovisioningFactor.valueIn(pool.getId())));
}
// TODO: StatsCollector does not persist data
StorageStats stats = ApiDBUtils.getStoragePoolStatistics(pool.getId());

View File

@ -22,6 +22,7 @@ import java.util.List;
import javax.inject.Inject;
import com.cloud.storage.clvm.ClvmPoolManager;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
@ -134,7 +135,11 @@ public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation<VolumeJo
}
if (volume.getProvisioningType() != null) {
volResponse.setProvisioningType(volume.getProvisioningType().toString());
Long poolId = volume.getPoolId();
StoragePoolVO poolVO = primaryDataStoreDao.findById(poolId);
if (poolVO == null || !ClvmPoolManager.isClvmPoolType(poolVO.getPoolType())) {
volResponse.setProvisioningType(volume.getProvisioningType().toString());
}
}
// Show the virtual size of the volume

View File

@ -1058,6 +1058,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
params.put("managed", cmd.isManaged());
params.put("capacityBytes", cmd.getCapacityBytes());
params.put("capacityIops", cmd.getCapacityIops());
params.put("scheme", uriParams.get("scheme"));
if (MapUtils.isNotEmpty(uriParams)) {
params.putAll(uriParams);
}
@ -1143,6 +1144,10 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C
if (isHostOrPathBlank) {
throw new InvalidParameterValueException("host or path is null, should be gluster://hostname/volume");
}
} else if (scheme.equalsIgnoreCase("clvm") || scheme.equalsIgnoreCase("clvm_ng")) {
if (storagePath == null) {
throw new InvalidParameterValueException("path is null, should be " + scheme.toLowerCase() + "://localhost/volume-group-name");
}
}
String hostPath = null;

View File

@ -35,6 +35,7 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.storage.clvm.ClvmPoolManager;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.InternalIdentity;
@ -370,6 +371,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
@Inject
EndPointSelector _epSelector;
@Inject
ClvmPoolManager clvmPoolManager;
@Inject
private ReservationDao reservationDao;
@Inject
private VMSnapshotDetailsDao vmSnapshotDetailsDao;
@ -1836,6 +1839,11 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
if (DataStoreRole.Image.equals(role)) {
_resourceLimitMgr.decrementResourceCount(volOnStorage.getAccountId(), ResourceType.secondary_storage, volOnStorage.getSize());
}
// Clean up CLVM lock host tracking detail after successful deletion from primary storage
if (DataStoreRole.Primary.equals(role)) {
clvmPoolManager.clearClvmLockHostDetail(volume);
}
}
}
@ -2738,21 +2746,42 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
logger.trace(String.format("is it needed to move the volume: %b?", moveVolumeNeeded));
}
if (moveVolumeNeeded) {
// Check if CLVM lock transfer is needed (even if moveVolumeNeeded is false)
// This handles the case where the volume is already on the correct storage pool
// but the VM is running on a different host, requiring only a lock transfer
boolean isClvmLockTransferNeeded = !moveVolumeNeeded &&
isClvmLockTransferRequired(newVolumeOnPrimaryStorage, existingVolumeOfVm, vm);
if (isClvmLockTransferNeeded) {
// CLVM lock transfer - no data copy, no pool change needed
newVolumeOnPrimaryStorage = executeLightweightLockMigration(
newVolumeOnPrimaryStorage, vm, existingVolumeOfVm,
"CLVM lock transfer", "same pool to different host");
} else if (moveVolumeNeeded) {
PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)newVolumeOnPrimaryStorage.getDataStore();
if (primaryStore.isLocal()) {
throw new CloudRuntimeException(
"Failed to attach local data volume " + volumeToAttach.getName() + " to VM " + vm.getDisplayName() + " as migration of local data volume is not allowed");
}
StoragePoolVO vmRootVolumePool = _storagePoolDao.findById(existingVolumeOfVm.getPoolId());
try {
HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId());
newVolumeOnPrimaryStorage = _volumeMgr.moveVolume(newVolumeOnPrimaryStorage, vmRootVolumePool.getDataCenterId(), vmRootVolumePool.getPodId(), vmRootVolumePool.getClusterId(),
volumeToAttachHyperType);
} catch (ConcurrentOperationException | StorageUnavailableException e) {
logger.debug("move volume failed", e);
throw new CloudRuntimeException("move volume failed", e);
boolean isClvmLightweightMigration = isClvmLightweightMigrationNeeded(
newVolumeOnPrimaryStorage, existingVolumeOfVm);
if (isClvmLightweightMigration) {
newVolumeOnPrimaryStorage = executeLightweightLockMigration(
newVolumeOnPrimaryStorage, vm, existingVolumeOfVm,
"CLVM lightweight migration", "different pools, same VG");
} else {
StoragePoolVO vmRootVolumePool = _storagePoolDao.findById(existingVolumeOfVm.getPoolId());
try {
HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId());
newVolumeOnPrimaryStorage = _volumeMgr.moveVolume(newVolumeOnPrimaryStorage, vmRootVolumePool.getDataCenterId(), vmRootVolumePool.getPodId(), vmRootVolumePool.getClusterId(),
volumeToAttachHyperType);
} catch (ConcurrentOperationException | StorageUnavailableException e) {
logger.debug("move volume failed", e);
throw new CloudRuntimeException("move volume failed", e);
}
}
}
VolumeVO newVol = _volsDao.findById(newVolumeOnPrimaryStorage.getId());
@ -2767,6 +2796,183 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
return newVol;
}
/**
* Helper method to get storage pools for volume and VM.
*
* @param volumeToAttach The volume being attached
* @param vmExistingVolume The VM's existing volume
* @return Pair of StoragePoolVO objects (volumePool, vmPool), or null if either pool is missing
*/
private Pair<StoragePoolVO, StoragePoolVO> getStoragePoolsForVolumeAttachment(VolumeInfo volumeToAttach, VolumeVO vmExistingVolume) {
if (volumeToAttach == null || vmExistingVolume == null) {
return null;
}
StoragePoolVO volumePool = _storagePoolDao.findById(volumeToAttach.getPoolId());
StoragePoolVO vmPool = _storagePoolDao.findById(vmExistingVolume.getPoolId());
if (volumePool == null || vmPool == null) {
return null;
}
return new Pair<>(volumePool, vmPool);
}
/**
* Determines if a CLVM volume needs lightweight lock migration instead of full data copy.
*
* Lightweight migration is needed when:
* 1. Volume is on CLVM storage
* 2. Source and destination are in the same Volume Group
* 3. Only the host/lock needs to change (not the storage pool)
*
* @param volumeToAttach The volume being attached
* @param vmExistingVolume The VM's existing volume (typically root volume)
* @param vm The VM to attach the volume to
* @return true if lightweight CLVM lock migration should be used
*/
private boolean isClvmLightweightMigrationNeeded(VolumeInfo volumeToAttach, VolumeVO vmExistingVolume) {
Pair<StoragePoolVO, StoragePoolVO> pools = getStoragePoolsForVolumeAttachment(volumeToAttach, vmExistingVolume);
if (pools == null) {
return false;
}
StoragePoolVO volumePool = pools.first();
StoragePoolVO vmPool = pools.second();
return volService.isLightweightMigrationNeeded(volumePool.getPoolType(), vmPool.getPoolType(),
volumePool.getPath(), vmPool.getPath());
}
/**
* Determines if a CLVM volume requires lock transfer when already on the correct storage pool.
*
* Lock transfer is needed when:
* 1. Volume is already on the same CLVM storage pool as VM's volumes
* 2. But the volume lock is held by a different host than where the VM is running
* 3. Only the lock needs to change (no pool change, no data copy)
*
* @param volumeToAttach The volume being attached
* @param vmExistingVolume The VM's existing volume (typically root volume)
* @param vm The VM to attach the volume to
* @return true if CLVM lock transfer is needed (but not full migration)
*/
private boolean isClvmLockTransferRequired(VolumeInfo volumeToAttach, VolumeVO vmExistingVolume, UserVmVO vm) {
if (vm == null) {
return false;
}
Pair<StoragePoolVO, StoragePoolVO> pools = getStoragePoolsForVolumeAttachment(volumeToAttach, vmExistingVolume);
if (pools == null) {
return false;
}
StoragePoolVO volumePool = pools.first();
StoragePoolVO vmPool = pools.second();
Long vmHostId = vm.getHostId();
if (vmHostId == null) {
vmHostId = vm.getLastHostId();
}
return volService.isLockTransferRequired(volumeToAttach, volumePool.getPoolType(), vmPool.getPoolType(),
volumePool.getId(), vmPool.getId(), vmHostId);
}
/**
* Determines the destination host for CLVM lock migration.
*
* If VM is running, uses the VM's current host.
* If VM is stopped, picks an available UP host from the storage pool's cluster.
*
* @param vm The VM
* @param vmExistingVolume The VM's existing volume (to determine cluster)
* @return Host ID, or null if cannot be determined
*/
private Long determineClvmLockDestinationHost(UserVmVO vm, VolumeVO vmExistingVolume) {
Long destHostId = vm.getHostId();
if (destHostId != null) {
return destHostId;
}
if (vmExistingVolume == null || vmExistingVolume.getPoolId() == null) {
return null;
}
StoragePoolVO pool = _storagePoolDao.findById(vmExistingVolume.getPoolId());
if (pool == null || pool.getClusterId() == null) {
return null;
}
List<HostVO> hosts = _hostDao.findByClusterId(pool.getClusterId());
if (hosts == null || hosts.isEmpty()) {
return null;
}
// Pick first available UP host
for (HostVO host : hosts) {
if (host.getStatus() == Status.Up) {
destHostId = host.getId();
logger.debug("VM {} is stopped, selected host {} from cluster {} for CLVM lock migration",
vm.getUuid(), destHostId, pool.getClusterId());
return destHostId;
}
}
return null;
}
/**
* Executes CLVM lightweight migration with consistent logging and error handling.
*
* This helper method wraps the actual migration logic to eliminate code duplication
* between different CLVM migration scenarios (lock transfer vs. lightweight migration).
*
* @param volume The volume to migrate locks for
* @param vm The VM to attach the volume to
* @param vmExistingVolume The VM's existing volume (to determine target host)
* @param operationType Description of the operation type for logging (e.g., "CLVM lock transfer")
* @param scenarioDescription Description of the scenario for logging (e.g., "same pool to different host")
* @return Updated VolumeInfo after lock migration
* @throws CloudRuntimeException if migration fails
*/
private VolumeInfo executeLightweightLockMigration(VolumeInfo volume, UserVmVO vm, VolumeVO vmExistingVolume,
String operationType, String scenarioDescription) {
logger.info("Performing {} for volume {} to VM {} ({})",
operationType, volume.getUuid(), vm.getUuid(), scenarioDescription);
try {
return performLightweightLockMigration(volume, vm, vmExistingVolume);
} catch (Exception e) {
logger.error("{} failed for volume {}: {}",
operationType, volume.getUuid(), e.getMessage(), e);
throw new CloudRuntimeException(operationType + " failed", e);
}
}
/**
* Performs lightweight CLVM lock migration for volume attachment.
* Delegates to VolumeService for the actual lock migration.
*
* @param volume The volume to migrate locks for
* @param vm The VM to attach the volume to
* @param vmExistingVolume The VM's existing volume (to determine target host)
* @return Updated VolumeInfo after lock migration
* @throws Exception if lock migration fails
*/
private VolumeInfo performLightweightLockMigration(VolumeInfo volume, UserVmVO vm, VolumeVO vmExistingVolume) throws CloudRuntimeException {
Long destHostId = determineClvmLockDestinationHost(vm, vmExistingVolume);
if (destHostId == null) {
throw new CloudRuntimeException(
"Cannot determine destination host for CLVM lock migration - VM has no host and no available cluster hosts");
}
try {
return volService.performLockMigration(volume, destHostId);
} catch (CloudRuntimeException e) {
logger.error("CLVM lock migration failed for volume {}: {}", volume.getUuid(), e.getMessage(), e);
throw e;
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_ATTACH, eventDescription = "attaching volume", async = true)
public Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS) {

View File

@ -0,0 +1,485 @@
/*
* 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.storage.clvm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePool;
import com.cloud.storage.VolumeDetailVO;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDetailsDao;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand;
import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferAnswer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
@Component
public class ClvmPoolManager implements Configurable {
@Inject
private VolumeDetailsDao _volsDetailsDao;
@Inject
private AgentManager _agentMgr;
@Inject
private HostDao _hostDao;
protected Logger logger = LogManager.getLogger(getClass());
/**
* Constant for the volume detail key that stores the host ID currently holding the CLVM exclusive lock.
* This is used during lightweight lock migration to determine the source host for lock transfer.
*/
public static final String CLVM_LOCK_HOST_ID = "clvmLockHostId";
public static final ConfigKey<Boolean> CLVMSecureZeroFill = new ConfigKey<>("Advanced", Boolean.class, "clvm.secure.zero.fill", "false",
"When enabled, CLVM volumes to be zero-filled at the time of deletion to prevent data from being recovered by VMs reusing the space, as thick LVM volumes write data linearly. Note: This setting is propagated to hosts when they connect to the storage pool. Changing this setting requires disconnecting and reconnecting hosts or restarting the KVM agent for it to take effect.", false, ConfigKey.Scope.StoragePool);
public static boolean isClvmPoolType(Storage.StoragePoolType poolType) {
return Arrays.asList(Storage.StoragePoolType.CLVM, Storage.StoragePoolType.CLVM_NG).contains(poolType);
}
/**
* Gets the CLVM lock host ID for a volume, optionally querying actual LVM state.
*
* @param volumeId The volume ID
* @param volumeUuid The volume UUID
* @return Host ID that holds the lock, or null if not found
* @deprecated Use getClvmLockHostId(volumeId, volumeUuid, volumePath, pool, queryActual) instead
*/
public Long getClvmLockHostId(Long volumeId, String volumeUuid) {
VolumeDetailVO detail = _volsDetailsDao.findDetail(volumeId, CLVM_LOCK_HOST_ID);
if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) {
try {
return Long.parseLong(detail.getValue());
} catch (NumberFormatException e) {
logger.warn("Invalid clvmLockHostId in volume_details for volume {}: {}",
volumeUuid, detail.getValue());
}
}
return null;
}
/**
* Gets the CLVM lock host ID for a volume, optionally querying actual LVM state.
* This method can query the actual lock state from LVM (source of truth) instead of
* relying solely on potentially stale database records.
*
* @param volumeId The volume ID
* @param volumeUuid The volume UUID
* @param volumePath The LV path (required if queryActual is true)
* @param pool The storage pool (required if queryActual is true)
* @param queryActual If true, queries actual LVM state instead of database
* @return Host ID that holds the lock, or null if not found
*/
public Long getClvmLockHostId(Long volumeId, String volumeUuid, String volumePath,
StoragePool pool, boolean queryActual) {
if (queryActual) {
if (volumePath == null || pool == null) {
logger.warn("Cannot query actual CLVM lock state for volume {} - missing volumePath or pool", volumeUuid);
return getClvmLockHostId(volumeId, volumeUuid);
}
return queryCurrentLockHolder(volumeId, volumeUuid, volumePath, pool, true);
}
return getClvmLockHostId(volumeId, volumeUuid);
}
/**
* Safely sets or updates the CLVM_LOCK_HOST_ID detail for a volume.
* If the detail already exists, it will be updated. Otherwise, it will be created.
*
* @param volumeId The ID of the volume
* @param hostId The host ID that holds/should hold the CLVM exclusive lock
*/
public void setClvmLockHostId(long volumeId, long hostId) {
VolumeDetailVO existingDetail = _volsDetailsDao.findDetail(volumeId, CLVM_LOCK_HOST_ID);
if (existingDetail != null) {
existingDetail.setValue(String.valueOf(hostId));
_volsDetailsDao.update(existingDetail.getId(), existingDetail);
logger.debug("Updated CLVM_LOCK_HOST_ID for volume {} to host {}", volumeId, hostId);
return;
}
_volsDetailsDao.addDetail(volumeId, CLVM_LOCK_HOST_ID, String.valueOf(hostId), false);
logger.debug("Created CLVM_LOCK_HOST_ID for volume {} with host {}", volumeId, hostId);
}
/**
* Query LVM to find the actual current lock holder for a volume.
* This is the SOURCE OF TRUTH - it queries the actual LVM state via sanlock/lvmlockd.
*
* <p>If no host holds the exclusive lock (e.g. after a storage outage), this method attempts
* an exclusive activation on the best available host before giving up. Activation failure
* is non-fatal: the method returns null so callers can apply their own fallback logic.
*
* @param volumeId The volume ID
* @param volumeUuid The volume UUID
* @param volumePath The LV path (e.g., "vm-123-disk-0")
* @param pool The storage pool
* @param updateDatabase If true, persists the discovered or newly-activated lock host to the DB
* @return Host ID of current or newly-activated lock holder, or null if none found/activated
*/
public Long queryCurrentLockHolder(Long volumeId, String volumeUuid, String volumePath,
StoragePool pool, boolean updateDatabase) {
if (pool == null) {
logger.error("Cannot query CLVM lock for volume {} - pool is null", volumeUuid);
return null;
}
String vgName = pool.getPath();
if (vgName.startsWith("/")) {
vgName = vgName.substring(1);
}
String lvPath = String.format("/dev/%s/%s", vgName, volumePath);
// Fast path: trust the DB record and verify with a single host query
Long dbHostId = getClvmLockHostId(volumeId, volumeUuid);
if (dbHostId != null) {
HostVO dbHost = _hostDao.findById(dbHostId);
if (dbHost != null && dbHost.getStatus() == Status.Up
&& dbHost.getHypervisorType() == Hypervisor.HypervisorType.KVM) {
Boolean active = querySingleHostLockState(dbHostId, lvPath, volumeUuid);
if (Boolean.TRUE.equals(active)) {
logger.debug("Fast path: volume {} confirmed active on DB host {}", volumeUuid, dbHostId);
return dbHostId;
}
logger.info("Fast path miss: volume {} not active on DB host {} - falling back to full fan-out",
volumeUuid, dbHostId);
} else {
logger.info("Fast path skip: DB host {} for volume {} is down/missing — falling back to full fan-out",
dbHostId, volumeUuid);
}
}
List<HostVO> hosts = null;
Long clusterId = pool.getClusterId();
if (clusterId != null) {
hosts = _hostDao.findByClusterId(clusterId, Host.Type.Routing);
} else if (pool.getDataCenterId() > 0) {
hosts = _hostDao.findByDataCenterId(pool.getDataCenterId());
}
if (hosts == null || hosts.isEmpty()) {
logger.warn("No KVM routing hosts found to query CLVM lock state for volume {} (pool: {}, cluster: {}, zone: {})",
volumeUuid, pool.getName(), clusterId, pool.getDataCenterId());
return null;
}
List<Long> activeHostIds = new ArrayList<>();
for (HostVO host : hosts) {
if (host.getStatus() != Status.Up ||
host.getType() != Host.Type.Routing ||
host.getHypervisorType() != Hypervisor.HypervisorType.KVM) {
continue;
}
// Skip the DB host, already confirmed inactive in the fast path above
if (dbHostId != null && host.getId() == dbHostId) {
continue;
}
Boolean active = querySingleHostLockState(host.getId(), lvPath, volumeUuid);
if (Boolean.TRUE.equals(active)) {
logger.debug("Volume {} is locally active on host {} (fan-out)", volumeUuid, host.getId());
activeHostIds.add(host.getId());
}
}
if (activeHostIds.isEmpty()) {
logger.debug("Volume {} is not active on any reachable host — no exclusive lock held", volumeUuid);
// Recovery: attempt exclusive activation on the best available host before giving up.
Long targetHostId = selectActivationTargetHost(dbHostId, hosts);
if (targetHostId != null) {
Long recoveredHostId = tryActivateExclusivelyOnHost(volumeId, volumeUuid, lvPath,
targetHostId, updateDatabase);
if (recoveredHostId != null) {
return recoveredHostId;
}
}
// Activation failed or no eligible host - clean up stale DB record and give up
if (updateDatabase && dbHostId != null) {
VolumeDetailVO detail = _volsDetailsDao.findDetail(volumeId, CLVM_LOCK_HOST_ID);
if (detail != null && String.valueOf(dbHostId).equals(detail.getValue())) {
_volsDetailsDao.remove(detail.getId());
}
}
return null;
}
if (activeHostIds.size() > 1) {
logger.warn("Volume {} is active on {} hosts {}, shared-mode LV (template?). "
+ "Skipping exclusive lock transfer.",
volumeUuid, activeHostIds.size(), activeHostIds);
return null;
}
Long lockHostId = activeHostIds.get(0);
logger.info("Volume {} is exclusively active on host {} (found via fan-out, DB had {})",
volumeUuid, lockHostId, dbHostId);
if (updateDatabase) {
if (dbHostId == null || !dbHostId.equals(lockHostId)) {
logger.info("Correcting database: volume {} lock host: {} -> {} (actual)",
volumeUuid, dbHostId, lockHostId);
setClvmLockHostId(volumeId, lockHostId);
}
}
return lockHostId;
}
/**
* Queries a single host for the CLVM LV activation state.
*
* @return {@code Boolean.TRUE} if the LV is active on that host,
* {@code Boolean.FALSE} if reachable but inactive,
* {@code null} if the host is unreachable or returned an error
*/
private Boolean querySingleHostLockState(Long hostId, String lvPath, String volumeUuid) {
try {
ClvmLockTransferCommand queryCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE, lvPath, volumeUuid);
Answer answer = _agentMgr.send(hostId, queryCmd);
if (answer == null || !answer.getResult()) {
logger.debug("Failed to query lock state from host {}: {}",
hostId, answer != null ? answer.getDetails() : "null answer");
return null;
}
if (!(answer instanceof ClvmLockTransferAnswer)) {
logger.warn("Unexpected answer type from host {} for QUERY_LOCK_STATE: {}",
hostId, answer.getClass());
return null;
}
ClvmLockTransferAnswer queryAnswer = (ClvmLockTransferAnswer) answer;
logger.debug("Host {} reports volume {} active={} (attr={})",
hostId, volumeUuid, queryAnswer.isActive(), queryAnswer.getLvAttributes());
return queryAnswer.isActive();
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.debug("Could not query host {} for lock state: {}", hostId, e.getMessage());
return null;
}
}
/**
* Selects the best host on which to exclusively activate an inactive CLVM volume.
*
* <p>Priority 1: the last known lock holder ({@code clvmLockHostId} from DB), if that host
* is UP and KVM.
*
* <p>Priority 2: a random UP KVM routing host from the cluster/zone list (fallback).
*
* @param dbHostId last known lock holder host ID from the DB (may be null)
* @param hosts routing hosts in the cluster or zone collected during fan-out
* @return host ID to activate on, or null if no eligible host found
*/
private Long selectActivationTargetHost(Long dbHostId, List<HostVO> hosts) {
if (dbHostId != null) {
HostVO dbHost = _hostDao.findById(dbHostId);
if (dbHost != null && dbHost.getStatus() == Status.Up
&& dbHost.getHypervisorType() == Hypervisor.HypervisorType.KVM) {
logger.debug("selectActivationTargetHost: preferring DB host {} (last known lock holder)", dbHostId);
return dbHostId;
}
}
if (hosts != null) {
List<HostVO> eligible = hosts.stream()
.filter(h -> h.getStatus() == Status.Up
&& h.getType() == Host.Type.Routing
&& h.getHypervisorType() == Hypervisor.HypervisorType.KVM)
.collect(Collectors.toList());
if (!eligible.isEmpty()) {
Collections.shuffle(eligible);
HostVO chosen = eligible.get(0);
logger.debug("selectActivationTargetHost: falling back to random UP KVM host {} in cluster/zone",
chosen.getId());
return chosen.getId();
}
}
logger.warn("selectActivationTargetHost: no eligible UP KVM host found");
return null;
}
/**
* Sends an {@code ACTIVATE_EXCLUSIVE} command to {@code targetHostId} and optionally
* persists the new lock host to the database.
*
* <p>Activation failure is non-fatal: returns null so the caller can apply its own fallback.
*
* @param volumeId volume DB ID
* @param volumeUuid volume UUID (for logging)
* @param lvPath full LV device path, e.g. {@code /dev/vgname/vol-path}
* @param targetHostId host to activate on
* @param updateDatabase if true, persists the new lock host on success
* @return {@code targetHostId} on success, {@code null} if the command failed or threw
*/
private Long tryActivateExclusivelyOnHost(Long volumeId, String volumeUuid, String lvPath,
Long targetHostId, boolean updateDatabase) {
try {
ClvmLockTransferCommand activateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE, lvPath, volumeUuid);
Answer activateAnswer = _agentMgr.send(targetHostId, activateCmd);
if (activateAnswer != null && activateAnswer.getResult()) {
logger.info("Recovery: exclusively activated volume {} on host {} (was inactive on all hosts)",
volumeUuid, targetHostId);
if (updateDatabase) {
setClvmLockHostId(volumeId, targetHostId);
}
return targetHostId;
}
logger.warn("Recovery activation of volume {} on host {} failed: {}",
volumeUuid, targetHostId,
activateAnswer != null ? activateAnswer.getDetails() : "null answer");
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.warn("Recovery activation of volume {} on host {} threw exception: {}",
volumeUuid, targetHostId, e.getMessage());
}
return null;
}
/**
* Cleans up CLVM lock host tracking detail from volume_details table.
* Called after successful volume deletion to prevent orphaned records.
*
* @param volume The volume being deleted
*/
public void clearClvmLockHostDetail(VolumeVO volume) {
try {
VolumeDetailVO detail = _volsDetailsDao.findDetail(volume.getId(), CLVM_LOCK_HOST_ID);
if (detail != null) {
logger.debug("Removing CLVM lock host detail for deleted volume {}", volume.getUuid());
_volsDetailsDao.remove(detail.getId());
}
} catch (Exception e) {
logger.warn("Failed to clean up CLVM lock host detail for volume {}: {}",
volume.getUuid(), e.getMessage());
}
}
/**
* Transfers the CLVM exclusive lock for a volume from the source host to the destination host.
*
* @param volumeUuid The volume UUID
* @param volumeId The volume DB ID
* @param volumePath The LV name within the VG (e.g. "vm-123-disk-0")
* @param pool The storage pool
* @param sourceHostId The host currently holding the lock (pre-validated by caller)
* @param destHostId The host that should hold the lock after transfer
* @return true if the lock was successfully transferred and activated on the destination
*/
public boolean transferClvmVolumeLock(String volumeUuid, Long volumeId, String volumePath,
StoragePool pool, Long sourceHostId, Long destHostId) {
if (pool == null) {
logger.error("Cannot transfer CLVM lock for volume {} - pool is null", volumeUuid);
return false;
}
String vgName = pool.getPath();
if (vgName.startsWith("/")) {
vgName = vgName.substring(1);
}
String lvPath = String.format("/dev/%s/%s", vgName, volumePath);
try {
// sourceHostId is trusted as pre-validated by the caller
Long hostToDeactivate = sourceHostId;
logger.info("Transferring CLVM lock for volume {}: source={}, destination={}",
volumeUuid, sourceHostId, destHostId);
if (hostToDeactivate != null && !hostToDeactivate.equals(destHostId)) {
HostVO deactivateHost = _hostDao.findById(hostToDeactivate);
if (deactivateHost != null && deactivateHost.getStatus() == Status.Up) {
ClvmLockTransferCommand deactivateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.DEACTIVATE, lvPath, volumeUuid);
Answer deactivateAnswer = _agentMgr.send(hostToDeactivate, deactivateCmd);
if (deactivateAnswer == null || !deactivateAnswer.getResult()) {
logger.warn("Failed to deactivate CLVM volume {} on host {}. Will attempt activation on destination.",
volumeUuid, hostToDeactivate);
} else {
logger.debug("Successfully deactivated volume {} on host {}", volumeUuid, hostToDeactivate);
}
} else {
logger.warn("Host {} (current lock holder) is down. Will attempt force claim on destination host {}",
hostToDeactivate, destHostId);
}
} else if (hostToDeactivate == null) {
logger.debug("Volume {} has no active lock holder, will directly activate on destination", volumeUuid);
}
ClvmLockTransferCommand activateCmd = new ClvmLockTransferCommand(
ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE,
lvPath,
volumeUuid
);
Answer activateAnswer = _agentMgr.send(destHostId, activateCmd);
if (activateAnswer == null || !activateAnswer.getResult()) {
String error = activateAnswer != null ? activateAnswer.getDetails() : "null answer";
logger.error("Failed to activate CLVM volume {} exclusively on dest host {}: {}",
volumeUuid, destHostId, error);
return false;
}
setClvmLockHostId(volumeId, destHostId);
logger.info("Successfully transferred CLVM lock for volume {} from host {} to host {}",
volumeUuid, sourceHostId != null ? sourceHostId : "none", destHostId);
return true;
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.error("Exception during CLVM lock transfer for volume {}: {}", volumeUuid, e.getMessage(), e);
return false;
}
}
@Override
public String getConfigComponentName() {
return ClvmPoolManager.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {
CLVMSecureZeroFill
};
}
}

View File

@ -165,6 +165,7 @@ import com.cloud.utils.DateUtil;
import com.cloud.utils.DateUtil.IntervalType;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.utils.Ternary;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.DB;
@ -1633,6 +1634,8 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
boolean isKvmAndFileBasedStorage = isHypervisorKvmAndFileBasedStorage(volume, storagePool);
boolean backupSnapToSecondary = isBackupSnapshotToSecondaryForZone(volume.getDataCenterId());
StoragePoolType poolType = volume.getStoragePoolType();
if (isKvmAndFileBasedStorage && backupSnapToSecondary) {
DataStore imageStore = snapshotSrv.findSnapshotImageStore(snapshot);
if (imageStore == null) {
@ -1641,7 +1644,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
snapshot.setImageStore(imageStore);
}
updateSnapshotPayload(volume.getPoolId(), payload, isKvmAndFileBasedStorage, clusterId);
updateSnapshotPayload(volume.getPoolId(), payload, isKvmAndFileBasedStorage, poolType, clusterId);
snapshot.addPayload(payload);
try {
@ -1659,6 +1662,9 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
if (backupSnapToSecondary) {
if (!isKvmAndFileBasedStorage) {
backupSnapshotToSecondary(payload.getAsyncBackup(), snapshotStrategy, snapshotOnPrimary, payload.getZoneIds(), payload.getStoragePoolIds());
if (!payload.getAsyncBackup() && ClvmPoolManager.isClvmPoolType(storagePool.getPoolType())) {
_snapshotStoreDao.removeBySnapshotStore(snapshotId, snapshotOnPrimary.getDataStore().getId(), snapshotOnPrimary.getDataStore().getRole());
}
} else {
postSnapshotDirectlyToSecondary(snapshot, snapshotOnPrimary, snapshotId);
}
@ -1678,7 +1684,12 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
postCreateSnapshot(volume.getId(), snapshotId, payload.getSnapshotPolicyId(), clusterId);
snapshotZoneDao.addSnapshotToZone(snapshotId, snapshot.getDataCenterId());
DataStoreRole dataStoreRole = backupSnapToSecondary ? snapshotHelper.getDataStoreRole(snapshot) : DataStoreRole.Primary;
DataStoreRole dataStoreRole;
if (payload.getAsyncBackup() && backupSnapToSecondary && !isKvmAndFileBasedStorage) {
dataStoreRole = DataStoreRole.Primary;
} else {
dataStoreRole = backupSnapToSecondary ? snapshotHelper.getDataStoreRole(snapshot) : DataStoreRole.Primary;
}
List<SnapshotDataStoreVO> snapshotStoreRefs = _snapshotStoreDao.listReadyBySnapshot(snapshotId, dataStoreRole);
if (CollectionUtils.isEmpty(snapshotStoreRefs)) {
@ -1816,6 +1827,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
SnapshotInfo backupedSnapshot = snapshotStrategy.backupSnapshot(snapshotOnPrimary);
if (backupedSnapshot != null) {
snapshotStrategy.postSnapshotCreation(snapshotOnPrimary);
removeClvmPrimarySnapshotStoreRefIfNeeded(snapshotOnPrimary);
copyNewSnapshotToZones(snapshotOnPrimary.getId(), snapshotOnPrimary.getDataCenterId(), zoneIds);
}
}
@ -1841,7 +1853,15 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
}
}
private void updateSnapshotPayload(long storagePoolId, CreateSnapshotPayload payload, boolean isKvmAndFileBasedStorage, Long clusterId) {
void removeClvmPrimarySnapshotStoreRefIfNeeded(SnapshotInfo snapshotOnPrimary) {
DataStore dataStore = snapshotOnPrimary.getDataStore();
StoragePoolVO pool = _storagePoolDao.findById(dataStore.getId());
if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) {
_snapshotStoreDao.removeBySnapshotStore(snapshotOnPrimary.getId(), dataStore.getId(), dataStore.getRole());
}
}
private void updateSnapshotPayload(long storagePoolId, CreateSnapshotPayload payload, boolean isKvmAndFileBasedStorage, StoragePoolType poolType, Long clusterId) {
StoragePoolVO storagePoolVO = _storagePoolDao.findById(storagePoolId);
if (storagePoolVO.isManaged()) {

View File

@ -343,6 +343,7 @@ import com.cloud.storage.VMTemplateZoneVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeApiService;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSCategoryDao;
import com.cloud.storage.dao.GuestOSDao;
@ -651,6 +652,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Inject
ExtensionHelper extensionHelper;
@Inject
ClvmPoolManager clvmPoolManager;
private ScheduledExecutorService _executor = null;
private ScheduledExecutorService _vmIpFetchExecutor = null;
private int _expungeInterval;
@ -2617,6 +2621,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
List<VolumeVO> rootVol = _volsDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT);
// expunge the vm
_itMgr.advanceExpunge(vm.getUuid());
for (VolumeVO volume : rootVol) {
clvmPoolManager.clearClvmLockHostDetail(volume);
}
// Only if vm is not expunged already, cleanup it's resources
if (vm.getRemoved() == null) {

View File

@ -436,6 +436,12 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme
vmSnapshotType = VMSnapshot.Type.Disk;
}
// CLVM_NG: Block VM snapshots until Phase 2 implementation is complete
if (rootVolumePool.getPoolType() == Storage.StoragePoolType.CLVM_NG) {
throw new InvalidParameterValueException("VM snapshots are not yet supported on CLVM_NG storage pools. " +
"This feature will be available in a future release.");
}
try {
return createAndPersistVMSnapshot(userVmVo, vsDescription, vmSnapshotName, vsDisplayName, vmSnapshotType);
} catch (Exception e) {

View File

@ -183,6 +183,8 @@
<bean id="oCFS2ManagerImpl" class="com.cloud.storage.OCFS2ManagerImpl" />
<bean id="clvmPoolManager" class="com.cloud.storage.clvm.ClvmPoolManager" />
<bean id="outOfBandManagementServiceImpl" class="org.apache.cloudstack.outofbandmanagement.OutOfBandManagementServiceImpl">
<property name="outOfBandManagementDrivers" value="#{outOfBandManagementDriversRegistry.registered}" />
</bean>

View File

@ -0,0 +1,936 @@
// 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.storage;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.storage.dao.VolumeDetailsDao;
import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferAnswer;
import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ClvmPoolManagerTest {
@Mock
private VolumeDetailsDao volsDetailsDao;
@Mock
private AgentManager agentMgr;
@Mock
private HostDao hostDao;
@InjectMocks
private ClvmPoolManager clvmPoolManager;
private static final Long VOLUME_ID = 100L;
private static final Long HOST_ID_1 = 1L;
private static final Long HOST_ID_2 = 2L;
private static final String VOLUME_UUID = "test-volume-uuid";
private static final String VOLUME_PATH = "test-volume-path";
private static final String VG_NAME = "acsvg";
@Before
public void setUp() {
Mockito.reset(volsDetailsDao, agentMgr, hostDao);
}
@Test
public void testGetClvmLockHostId_Success() {
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue("123");
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
Long result = clvmPoolManager.getClvmLockHostId(VOLUME_ID, VOLUME_UUID);
Assert.assertEquals(Long.valueOf(123), result);
}
@Test
public void testGetClvmLockHostId_NoDetail() {
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
Long result = clvmPoolManager.getClvmLockHostId(VOLUME_ID, VOLUME_UUID);
Assert.assertNull(result);
}
@Test
public void testGetClvmLockHostId_InvalidNumber() {
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue("invalid");
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
Long result = clvmPoolManager.getClvmLockHostId(VOLUME_ID, VOLUME_UUID);
Assert.assertNull(result);
}
@Test
public void testSetClvmLockHostId_NewDetail() {
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
clvmPoolManager.setClvmLockHostId(VOLUME_ID, HOST_ID_1);
verify(volsDetailsDao, times(1)).addDetail(eq(VOLUME_ID), eq(ClvmPoolManager.CLVM_LOCK_HOST_ID),
eq(String.valueOf(HOST_ID_1)), eq(false));
verify(volsDetailsDao, never()).update(anyLong(), any());
}
@Test
public void testSetClvmLockHostId_UpdateExisting() {
VolumeDetailVO existingDetail = Mockito.mock(VolumeDetailVO.class);
when(existingDetail.getId()).thenReturn(50L);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(existingDetail);
clvmPoolManager.setClvmLockHostId(VOLUME_ID, HOST_ID_2);
verify(existingDetail, times(1)).setValue(String.valueOf(HOST_ID_2));
verify(volsDetailsDao, times(1)).update(eq(50L), eq(existingDetail));
verify(volsDetailsDao, never()).addDetail(anyLong(), any(), any(), Mockito.anyBoolean());
}
@Test
public void testClearClvmLockHostDetail_Success() {
VolumeVO volume = Mockito.mock(VolumeVO.class);
when(volume.getId()).thenReturn(VOLUME_ID);
when(volume.getUuid()).thenReturn(VOLUME_UUID);
VolumeDetailVO detail = Mockito.mock(VolumeDetailVO.class);
when(detail.getId()).thenReturn(99L);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
clvmPoolManager.clearClvmLockHostDetail(volume);
verify(volsDetailsDao, times(1)).remove(99L);
}
@Test
public void testClearClvmLockHostDetail_NoDetail() {
VolumeVO volume = Mockito.mock(VolumeVO.class);
when(volume.getId()).thenReturn(VOLUME_ID);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
clvmPoolManager.clearClvmLockHostDetail(volume);
verify(volsDetailsDao, never()).remove(anyLong());
}
@Test
public void testTransferClvmVolumeLock_Success() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getPath()).thenReturn("/" + VG_NAME);
HostVO sourceHost = Mockito.mock(HostVO.class);
when(sourceHost.getStatus()).thenReturn(Status.Up);
when(hostDao.findById(HOST_ID_1)).thenReturn(sourceHost);
Answer deactivateAnswer = new Answer(null, true, null);
Answer activateAnswer = new Answer(null, true, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(deactivateAnswer);
when(agentMgr.send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class))).thenReturn(activateAnswer);
boolean result = clvmPoolManager.transferClvmVolumeLock(VOLUME_UUID, VOLUME_ID,
VOLUME_PATH, pool, HOST_ID_1, HOST_ID_2);
Assert.assertTrue(result);
verify(agentMgr, times(2)).send(anyLong(), any(ClvmLockTransferCommand.class));
}
@Test
public void testTransferClvmVolumeLock_NullPool() {
boolean result = clvmPoolManager.transferClvmVolumeLock(VOLUME_UUID, VOLUME_ID,
VOLUME_PATH, null, HOST_ID_1, HOST_ID_2);
Assert.assertFalse(result);
}
@Test
public void testTransferClvmVolumeLock_SameHost() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getPath()).thenReturn("/" + VG_NAME);
Answer activateAnswer = new Answer(null, true, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(activateAnswer);
boolean result = clvmPoolManager.transferClvmVolumeLock(VOLUME_UUID, VOLUME_ID,
VOLUME_PATH, pool, HOST_ID_1, HOST_ID_1);
Assert.assertTrue(result);
verify(agentMgr, times(1)).send(anyLong(), any(ClvmLockTransferCommand.class));
}
@Test
public void testTransferClvmVolumeLock_ActivationFails() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getPath()).thenReturn(VG_NAME);
Answer activateAnswer = new Answer(null, false, "Activation failed");
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(activateAnswer);
boolean result = clvmPoolManager.transferClvmVolumeLock(VOLUME_UUID, VOLUME_ID,
VOLUME_PATH, pool, HOST_ID_1, HOST_ID_1);
Assert.assertFalse(result);
}
@Test
public void testTransferClvmVolumeLock_AgentUnavailable() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getPath()).thenReturn(VG_NAME);
when(agentMgr.send(anyLong(), any(ClvmLockTransferCommand.class)))
.thenThrow(new AgentUnavailableException("Agent unavailable", HOST_ID_2));
boolean result = clvmPoolManager.transferClvmVolumeLock(VOLUME_UUID, VOLUME_ID,
VOLUME_PATH, pool, HOST_ID_1, HOST_ID_2);
Assert.assertFalse(result);
}
@Test
public void testQueryCurrentLockHolder_NullPool() {
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, null, false);
Assert.assertNull(result);
verify(hostDao, never()).findByClusterId(anyLong(), any());
}
@Test
public void testQueryCurrentLockHolder_NoHostsInCluster() {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getDataCenterId()).thenReturn(1L);
when(pool.getName()).thenReturn("test-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.emptyList());
lenient().when(hostDao.findByDataCenterId(1L)).thenReturn(Collections.emptyList());
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
verify(hostDao, times(1)).findByClusterId(10L, Host.Type.Routing);
verify(hostDao, times(0)).findByDataCenterId(1L);
}
@Test
public void testQueryCurrentLockHolder_ZoneScopedPool() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(null);
when(pool.getDataCenterId()).thenReturn(1L);
Mockito.lenient().when(pool.getName()).thenReturn("zone-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByDataCenterId(1L)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(hostDao, never()).findByClusterId(anyLong(), any());
verify(hostDao, times(1)).findByDataCenterId(1L);
}
@Test
public void testQueryCurrentLockHolder_SuccessfulQuery() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
verify(hostDao, never()).findByName(any());
}
@Test
public void testQueryCurrentLockHolder_VolumeNotLocked() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
// QUERY reports inactive; ACTIVATE_EXCLUSIVE left unstubbed null answer recovery fails returns null
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(inactiveAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_EmptyHostname() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
// QUERY reports inactive with empty hostname; ACTIVATE_EXCLUSIVE left unstubbed null answer recovery fails returns null
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "", false, false, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_HostnameNotResolved() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "unknown-host", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(hostDao, never()).findByName(any());
}
@Test
public void testQueryCurrentLockHolder_QueryFails() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
Answer failedAnswer = new Answer(null, false, "Query failed");
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(failedAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_NullAnswer() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(null);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_AgentUnavailableException() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class)))
.thenThrow(new AgentUnavailableException("Host unavailable", HOST_ID_1));
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_OperationTimedoutException() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class)))
.thenThrow(new OperationTimedoutException(null, HOST_ID_1, 0, 0, false));
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_MatchingValue() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
Mockito.lenient().when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
Mockito.lenient().when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
// DB has correct value, fast path: query HOST_ID_1, returns active
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
when(hostDao.findById(HOST_ID_1)).thenReturn(host);
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_1, result);
// Fast path succeeded - no DB write needed, no fan-out
verify(volsDetailsDao, never()).update(anyLong(), any());
verify(volsDetailsDao, never()).addDetail(anyLong(), any(), any(), Mockito.anyBoolean());
verify(hostDao, never()).findByClusterId(anyLong(), any());
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_DifferentValue() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
// DB says HOST_ID_1, but actual lock is on HOST_ID_2
// Fast path: query HOST_ID_1, inactive: fall back to fan-out
VolumeDetailVO detail = Mockito.mock(VolumeDetailVO.class);
when(detail.getValue()).thenReturn(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
when(detail.getId()).thenReturn(99L);
HostVO host1 = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
HostVO host2 = createMockHost(HOST_ID_2, "host2", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(host1);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(host1, host2));
// HOST_ID_1 reports inactive (fast path miss), HOST_ID_2 reports active (fan-out)
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, "host1", false, false, null);
ClvmLockTransferAnswer activeAnswer = new ClvmLockTransferAnswer(null, true, null, "host2", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(inactiveAnswer);
when(agentMgr.send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class))).thenReturn(activeAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_2, result);
// DB should be corrected to HOST_ID_2
verify(detail, times(1)).setValue(String.valueOf(HOST_ID_2));
verify(volsDetailsDao, times(1)).update(eq(99L), eq(detail));
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_NoExistingDetail() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
// No DB record, fast path skipped, fan-out finds HOST_ID_1
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_1, result);
verify(volsDetailsDao, times(1)).addDetail(eq(VOLUME_ID), eq(ClvmPoolManager.CLVM_LOCK_HOST_ID),
eq(String.valueOf(HOST_ID_1)), eq(false));
}
@Test
public void testQueryCurrentLockHolder_UpdateDatabase_RemoveDetailWhenUnlocked() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
// DB has HOST_ID_1, fast path query returns inactive: fan-out also finds nothing
VolumeDetailVO detail = Mockito.mock(VolumeDetailVO.class);
when(detail.getId()).thenReturn(99L);
when(detail.getValue()).thenReturn(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(host);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
// QUERY_LOCK_STATE: inactive (fast path miss; HOST_ID_1 skipped in fan-out as dbHostId)
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(inactiveAnswer);
// ACTIVATE_EXCLUSIVE: recovery attempt fails stale DB record must still be removed
Answer failedActivation = new Answer(null, false, "Simulated activation failure for test");
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE)))
.thenReturn(failedActivation);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertNull(result);
verify(volsDetailsDao, times(1)).remove(99L);
}
@Test
public void testQueryCurrentLockHolder_SkipsNonKVMHosts() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO xenHost = createMockHost(10L, "xen-host", Status.Up, Hypervisor.HypervisorType.XenServer);
HostVO kvmHost = createMockHost(HOST_ID_1, "kvm-host", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(xenHost, kvmHost));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "kvm-host", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(agentMgr, never()).send(eq(10L), any(ClvmLockTransferCommand.class));
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
}
@Test
public void testQueryCurrentLockHolder_SkipsDownHosts() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO downHost = createMockHost(10L, "down-host", Status.Down, Hypervisor.HypervisorType.KVM);
HostVO upHost = createMockHost(HOST_ID_1, "up-host", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(downHost, upHost));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "up-host", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
verify(agentMgr, never()).send(eq(10L), any(ClvmLockTransferCommand.class));
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
}
@Test
public void testQueryCurrentLockHolder_PathWithLeadingSlash() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn("/" + VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host));
ClvmLockTransferAnswer answer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(answer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
}
/**
* Fast path: DB has the correct host, single query confirms isActive=true.
* No fan-out should occur.
*/
@Test
public void testQueryCurrentLockHolder_FastPath_HitOnDbHost() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getPath()).thenReturn(VG_NAME);
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
HostVO host = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(host);
ClvmLockTransferAnswer activeAnswer = new ClvmLockTransferAnswer(null, true, null, "host1", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(activeAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_1, result);
// Only one agent call, no cluster host lookup, no fan-out
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
verify(hostDao, never()).findByClusterId(anyLong(), any());
verify(hostDao, never()).findByDataCenterId(anyLong());
}
/**
* Fast path miss: DB has HOST_ID_1 but it's inactive. Fan-out finds HOST_ID_2.
* HOST_ID_1 should NOT be queried again during fan-out.
*/
@Test
public void testQueryCurrentLockHolder_FastPath_MissDbHost_FanOutFindsOther() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getPath()).thenReturn(VG_NAME);
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
HostVO host1 = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
HostVO host2 = createMockHost(HOST_ID_2, "host2", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(host1);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(host1, host2));
// Fast path: HOST_ID_1 inactive
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, "host1", false, false, null);
// Fan-out: HOST_ID_2 active (HOST_ID_1 skipped)
ClvmLockTransferAnswer activeAnswer = new ClvmLockTransferAnswer(null, true, null, "host2", true, false, null);
when(agentMgr.send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class))).thenReturn(inactiveAnswer);
when(agentMgr.send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class))).thenReturn(activeAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_2, result);
// HOST_ID_1 queried once (fast path only), HOST_ID_2 queried once (fan-out)
verify(agentMgr, times(1)).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
verify(agentMgr, times(1)).send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class));
}
/**
* Fast path skip: DB host is DOWN. Fan-out proceeds to all UP hosts.
*/
@Test
public void testQueryCurrentLockHolder_FastPath_DbHostDown_FanOut() throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
when(pool.getPath()).thenReturn(VG_NAME);
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
HostVO downHost = createMockHost(HOST_ID_1, "host1", Status.Down, Hypervisor.HypervisorType.KVM);
HostVO upHost = createMockHost(HOST_ID_2, "host2", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(downHost);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(downHost, upHost));
ClvmLockTransferAnswer activeAnswer = new ClvmLockTransferAnswer(null, true, null, "host2", true, false, null);
when(agentMgr.send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class))).thenReturn(activeAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertEquals(HOST_ID_2, result);
// No query to the down host at all
verify(agentMgr, never()).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
verify(agentMgr, times(1)).send(eq(HOST_ID_2), any(ClvmLockTransferCommand.class));
}
/**
* Inactive everywhere, DB host is UP: recovery activates exclusively on the DB host.
*/
@Test
public void testQueryCurrentLockHolder_InactiveEverywhere_ActivatesOnDbHost()
throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
VolumeDetailVO detail = Mockito.mock(VolumeDetailVO.class);
when(detail.getId()).thenReturn(99L);
when(detail.getValue()).thenReturn(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
HostVO host1 = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(host1);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host1));
// Fast path QUERY inactive
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(inactiveAnswer);
// Recovery ACTIVATE_EXCLUSIVE succeeds
Answer activateAnswer = new Answer(null, true, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE)))
.thenReturn(activateAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_1, result);
verify(detail, times(1)).setValue(String.valueOf(HOST_ID_1));
verify(volsDetailsDao, times(1)).update(eq(99L), eq(detail));
verify(volsDetailsDao, never()).remove(anyLong());
}
/**
* Inactive everywhere, no DB record: recovery falls back to the first UP KVM host in cluster.
*/
@Test
public void testQueryCurrentLockHolder_InactiveEverywhere_ActivatesOnClusterHostWhenNoDbRecord()
throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host1 = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host1));
// Fan-out QUERY inactive
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(inactiveAnswer);
// Recovery ACTIVATE_EXCLUSIVE succeeds
Answer activateAnswer = new Answer(null, true, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE)))
.thenReturn(activateAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_1, result);
verify(volsDetailsDao, times(1)).addDetail(eq(VOLUME_ID), eq(ClvmPoolManager.CLVM_LOCK_HOST_ID),
eq(String.valueOf(HOST_ID_1)), eq(false));
}
/**
* Inactive everywhere, DB host is DOWN: selectActivationTargetHost skips it and picks
* the first UP host from the cluster list.
*/
@Test
public void testQueryCurrentLockHolder_InactiveEverywhere_SkipsDownDbHost_ActivatesOnClusterHost()
throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
VolumeDetailVO detail = new VolumeDetailVO();
detail.setValue(String.valueOf(HOST_ID_1));
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(detail);
HostVO downHost = createMockHost(HOST_ID_1, "host1", Status.Down, Hypervisor.HypervisorType.KVM);
HostVO upHost = createMockHost(HOST_ID_2, "host2", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findById(HOST_ID_1)).thenReturn(downHost);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Arrays.asList(downHost, upHost));
// Fan-out QUERY on HOST_ID_2 inactive (HOST_ID_1 filtered by Status.Down)
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_2), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(inactiveAnswer);
// Recovery ACTIVATE_EXCLUSIVE on HOST_ID_2 succeeds
Answer activateAnswer = new Answer(null, true, null);
when(agentMgr.send(eq(HOST_ID_2), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE)))
.thenReturn(activateAnswer);
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, true);
Assert.assertEquals(HOST_ID_2, result);
verify(agentMgr, never()).send(eq(HOST_ID_1), any(ClvmLockTransferCommand.class));
}
/**
* Inactive everywhere, recovery activation throws AgentUnavailableException: returns null,
* no crash, no DB side-effects (updateDatabase=false).
*/
@Test
public void testQueryCurrentLockHolder_InactiveEverywhere_ActivationThrows_ReturnsNull()
throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
HostVO host1 = createMockHost(HOST_ID_1, "host1", Status.Up, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(host1));
ClvmLockTransferAnswer inactiveAnswer = new ClvmLockTransferAnswer(null, true, null, null, false, false, null);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE)))
.thenReturn(inactiveAnswer);
when(agentMgr.send(eq(HOST_ID_1), Mockito.<Command>argThat(cmd ->
cmd instanceof ClvmLockTransferCommand
&& ((ClvmLockTransferCommand) cmd).getOperation()
== ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE)))
.thenThrow(new AgentUnavailableException("Host unreachable during recovery", HOST_ID_1));
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
verify(volsDetailsDao, never()).remove(anyLong());
verify(volsDetailsDao, never()).addDetail(anyLong(), any(), any(), Mockito.anyBoolean());
}
/**
* Inactive everywhere, no UP KVM host available: selectActivationTargetHost returns null,
* no activation is attempted, returns null immediately.
*/
@Test
public void testQueryCurrentLockHolder_InactiveEverywhere_NoEligibleHost_ReturnsNull()
throws AgentUnavailableException, OperationTimedoutException {
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
when(pool.getClusterId()).thenReturn(10L);
Mockito.lenient().when(pool.getName()).thenReturn("cluster-pool");
when(pool.getPath()).thenReturn(VG_NAME);
when(volsDetailsDao.findDetail(VOLUME_ID, ClvmPoolManager.CLVM_LOCK_HOST_ID)).thenReturn(null);
// Only a DOWN host exists selectActivationTargetHost finds no eligible host
HostVO downHost = createMockHost(HOST_ID_1, "host1", Status.Down, Hypervisor.HypervisorType.KVM);
when(hostDao.findByClusterId(10L, Host.Type.Routing)).thenReturn(Collections.singletonList(downHost));
Long result = clvmPoolManager.queryCurrentLockHolder(VOLUME_ID, VOLUME_UUID, VOLUME_PATH, pool, false);
Assert.assertNull(result);
verify(agentMgr, never()).send(anyLong(), any(ClvmLockTransferCommand.class));
}
// Helper method to create mock hosts
private HostVO createMockHost(Long id, String name, Status status, Hypervisor.HypervisorType hypervisor) {
HostVO host = Mockito.mock(HostVO.class);
Mockito.lenient().when(host.getId()).thenReturn(id);
Mockito.lenient().when(host.getName()).thenReturn(name);
Mockito.lenient().when(host.getStatus()).thenReturn(status);
Mockito.lenient().when(host.getType()).thenReturn(Host.Type.Routing);
Mockito.lenient().when(host.getHypervisorType()).thenReturn(hypervisor);
return host;
}
}

View File

@ -40,6 +40,16 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.host.HostVO;
import com.cloud.resourcelimit.CheckedReservation;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
@ -99,24 +109,18 @@ import com.cloud.dc.HostPodVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.offering.DiskOffering;
import com.cloud.org.Grouping;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectManager;
import com.cloud.resourcelimit.CheckedReservation;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.server.TaggedResourceService;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.Storage.ProvisioningType;
import com.cloud.storage.Volume.Type;
import com.cloud.storage.dao.DiskOfferingDao;
@ -144,11 +148,8 @@ import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
@RunWith(MockitoJUnitRunner.class)
public class VolumeApiServiceImplTest {
@ -228,6 +229,8 @@ public class VolumeApiServiceImplTest {
ClusterDao clusterDao;
@Mock
VolumeOrchestrationService volumeOrchestrationService;
@Mock
ClvmPoolManager clvmPoolManager;
private DetachVolumeCmd detachCmd = new DetachVolumeCmd();
@ -2423,4 +2426,290 @@ public class VolumeApiServiceImplTest {
Mockito.doReturn(1L).when(mock2).getId();
return List.of(mock1, mock2);
}
@Test
public void testIsClvmLightweightMigrationNeeded_SameVG() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 200L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(volumePool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(volumePool.getPath()).thenReturn("/vg1");
Mockito.when(vmPool.getPath()).thenReturn("/vg1");
Mockito.when(volumeServiceMock.isLightweightMigrationNeeded(
Storage.StoragePoolType.CLVM, Storage.StoragePoolType.CLVM,
"/vg1", "/vg1")).thenReturn(true);
boolean result = invokePrivateMethod("isClvmLightweightMigrationNeeded",
new Class[]{VolumeInfo.class, VolumeVO.class},
volumeInfo, vmExistingVolume);
Assert.assertTrue(result);
Mockito.verify(volumeServiceMock).isLightweightMigrationNeeded(
Storage.StoragePoolType.CLVM, Storage.StoragePoolType.CLVM, "/vg1", "/vg1");
}
@Test
public void testIsClvmLightweightMigrationNeeded_DifferentVG() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 200L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(volumePool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(volumePool.getPath()).thenReturn("/vg1");
Mockito.when(vmPool.getPath()).thenReturn("/vg2");
Mockito.when(volumeServiceMock.isLightweightMigrationNeeded(
Storage.StoragePoolType.CLVM, Storage.StoragePoolType.CLVM,
"/vg1", "/vg2")).thenReturn(false);
boolean result = invokePrivateMethod("isClvmLightweightMigrationNeeded",
new Class[]{VolumeInfo.class, VolumeVO.class},
volumeInfo, vmExistingVolume);
Assert.assertFalse(result);
}
@Test
public void testIsClvmLightweightMigrationNeeded_CLVM_NG_SameVG() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 200L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(volumePool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM_NG);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM_NG);
Mockito.when(volumePool.getPath()).thenReturn("/vg1");
Mockito.when(vmPool.getPath()).thenReturn("/vg1");
Mockito.when(volumeServiceMock.isLightweightMigrationNeeded(
Storage.StoragePoolType.CLVM_NG, Storage.StoragePoolType.CLVM_NG,
"/vg1", "/vg1")).thenReturn(true);
boolean result = invokePrivateMethod("isClvmLightweightMigrationNeeded",
new Class[]{VolumeInfo.class, VolumeVO.class},
volumeInfo, vmExistingVolume);
Assert.assertTrue(result);
}
@Test
public void testIsClvmLockTransferRequired_DifferentHosts() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 100L; // Same pool
Long vmHostId = 10L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(vm.getHostId()).thenReturn(vmHostId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getId()).thenReturn(vmPoolId);
Mockito.when(volumeServiceMock.isLockTransferRequired(
eq(volumeInfo), eq(Storage.StoragePoolType.CLVM), eq(Storage.StoragePoolType.CLVM),
eq(volumePoolId), eq(vmPoolId), eq(vmHostId))).thenReturn(true);
boolean result = invokePrivateMethod("isClvmLockTransferRequired",
new Class[]{VolumeInfo.class, VolumeVO.class, UserVmVO.class},
volumeInfo, vmExistingVolume, vm);
Assert.assertTrue(result);
Mockito.verify(volumeServiceMock).isLockTransferRequired(
eq(volumeInfo), eq(Storage.StoragePoolType.CLVM), eq(Storage.StoragePoolType.CLVM),
eq(volumePoolId), eq(vmPoolId), eq(vmHostId));
}
@Test
public void testIsClvmLockTransferRequired_SameHost() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 100L;
Long vmHostId = 10L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(vm.getHostId()).thenReturn(vmHostId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getId()).thenReturn(vmPoolId);
Mockito.when(volumeServiceMock.isLockTransferRequired(
eq(volumeInfo), eq(Storage.StoragePoolType.CLVM), eq(Storage.StoragePoolType.CLVM),
eq(volumePoolId), eq(vmPoolId), eq(vmHostId))).thenReturn(false);
boolean result = invokePrivateMethod("isClvmLockTransferRequired",
new Class[]{VolumeInfo.class, VolumeVO.class, UserVmVO.class},
volumeInfo, vmExistingVolume, vm);
Assert.assertFalse(result);
}
@Test
public void testIsClvmLockTransferRequired_DifferentPools() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 200L; // Different pool
Long vmHostId = 10L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(vm.getHostId()).thenReturn(vmHostId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(volumePool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(volumePool.getId()).thenReturn(volumePoolId);
Mockito.when(vmPool.getId()).thenReturn(vmPoolId);
Mockito.when(volumeServiceMock.isLockTransferRequired(
eq(volumeInfo), eq(Storage.StoragePoolType.CLVM), eq(Storage.StoragePoolType.CLVM),
eq(volumePoolId), eq(vmPoolId), eq(vmHostId))).thenReturn(false);
boolean result = invokePrivateMethod("isClvmLockTransferRequired",
new Class[]{VolumeInfo.class, VolumeVO.class, UserVmVO.class},
volumeInfo, vmExistingVolume, vm);
Assert.assertFalse(result);
}
@Test
public void testIsClvmLockTransferRequired_NonCLVMPool() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 100L;
Long vmHostId = 10L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(vm.getHostId()).thenReturn(vmHostId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getId()).thenReturn(vmPoolId);
boolean result = invokePrivateMethod("isClvmLockTransferRequired",
new Class[]{VolumeInfo.class, VolumeVO.class, UserVmVO.class},
volumeInfo, vmExistingVolume, vm);
Assert.assertFalse(result);
}
@Test
public void testIsClvmLockTransferRequired_NullVM() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
boolean result = invokePrivateMethod("isClvmLockTransferRequired",
new Class[]{VolumeInfo.class, VolumeVO.class, UserVmVO.class},
volumeInfo, vmExistingVolume, null);
Assert.assertFalse(result);
}
@Test
public void testIsClvmLockTransferRequired_VMStoppedUsesLastHostId() {
VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class);
VolumeVO vmExistingVolume = Mockito.mock(VolumeVO.class);
UserVmVO vm = Mockito.mock(UserVmVO.class);
StoragePoolVO volumePool = Mockito.mock(StoragePoolVO.class);
StoragePoolVO vmPool = Mockito.mock(StoragePoolVO.class);
Long volumePoolId = 100L;
Long vmPoolId = 100L;
Long lastHostId = 10L;
Mockito.when(volumeInfo.getPoolId()).thenReturn(volumePoolId);
Mockito.when(vmExistingVolume.getPoolId()).thenReturn(vmPoolId);
Mockito.when(vm.getHostId()).thenReturn(null); // VM is stopped
Mockito.when(vm.getLastHostId()).thenReturn(lastHostId);
Mockito.when(primaryDataStoreDaoMock.findById(volumePoolId)).thenReturn(volumePool);
Mockito.when(primaryDataStoreDaoMock.findById(vmPoolId)).thenReturn(vmPool);
Mockito.when(vmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM);
Mockito.when(vmPool.getId()).thenReturn(vmPoolId);
Mockito.when(volumeServiceMock.isLockTransferRequired(
eq(volumeInfo), eq(Storage.StoragePoolType.CLVM), eq(Storage.StoragePoolType.CLVM),
eq(volumePoolId), eq(vmPoolId), eq(lastHostId))).thenReturn(true);
boolean result = invokePrivateMethod("isClvmLockTransferRequired",
new Class[]{VolumeInfo.class, VolumeVO.class, UserVmVO.class},
volumeInfo, vmExistingVolume, vm);
Assert.assertTrue(result);
Mockito.verify(volumeServiceMock).isLockTransferRequired(
eq(volumeInfo), eq(Storage.StoragePoolType.CLVM), eq(Storage.StoragePoolType.CLVM),
eq(volumePoolId), eq(vmPoolId), eq(lastHostId));
}
private <T> T invokePrivateMethod(String methodName, Class<?>[] paramTypes, Object... params) {
try {
java.lang.reflect.Method method = VolumeApiServiceImpl.class.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return (T) method.invoke(volumeApiServiceImpl, params);
} catch (Exception e) {
throw new RuntimeException("Failed to invoke method: " + methodName, e);
}
}
}

View File

@ -26,6 +26,7 @@ import com.cloud.exception.ResourceUnavailableException;
import com.cloud.org.Grouping;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Snapshot;
import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.storage.SnapshotPolicyVO;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.VolumeVO;
@ -55,8 +56,10 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.junit.Assert;
import org.junit.After;
@ -105,6 +108,8 @@ public class SnapshotManagerImplTest {
SnapshotScheduler snapshotScheduler;
@Mock
TaggedResourceService taggedResourceService;
@Mock
PrimaryDataStoreDao primaryDataStoreDao;
@InjectMocks
SnapshotManagerImpl snapshotManager = new SnapshotManagerImpl();
@ -115,6 +120,7 @@ public class SnapshotManagerImplTest {
snapshotManager._accountMgr = accountManager;
snapshotManager._snapSchedMgr = snapshotScheduler;
snapshotManager.taggedResourceService = taggedResourceService;
snapshotManager._storagePoolDao = primaryDataStoreDao;
}
@After
@ -610,4 +616,57 @@ public class SnapshotManagerImplTest {
snapshotManager.deleteSnapshotPolicies(cmd);
}
@Test
public void testRemoveClvmPrimarySnapshotStoreRefIfNeeded_ClvmPool() {
SnapshotInfo snapshot = Mockito.mock(SnapshotInfo.class);
DataStore dataStore = Mockito.mock(DataStore.class);
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
Mockito.when(snapshot.getDataStore()).thenReturn(dataStore);
Mockito.when(snapshot.getId()).thenReturn(1L);
Mockito.when(dataStore.getId()).thenReturn(2L);
Mockito.when(dataStore.getRole()).thenReturn(DataStoreRole.Primary);
Mockito.when(primaryDataStoreDao.findById(2L)).thenReturn(pool);
Mockito.when(pool.getPoolType()).thenReturn(StoragePoolType.CLVM);
snapshotManager.removeClvmPrimarySnapshotStoreRefIfNeeded(snapshot);
Mockito.verify(snapshotStoreDao).removeBySnapshotStore(1L, 2L, DataStoreRole.Primary);
}
@Test
public void testRemoveClvmPrimarySnapshotStoreRefIfNeeded_ClvmNgPool() {
SnapshotInfo snapshot = Mockito.mock(SnapshotInfo.class);
DataStore dataStore = Mockito.mock(DataStore.class);
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
Mockito.when(snapshot.getDataStore()).thenReturn(dataStore);
Mockito.when(snapshot.getId()).thenReturn(3L);
Mockito.when(dataStore.getId()).thenReturn(4L);
Mockito.when(dataStore.getRole()).thenReturn(DataStoreRole.Primary);
Mockito.when(primaryDataStoreDao.findById(4L)).thenReturn(pool);
Mockito.when(pool.getPoolType()).thenReturn(StoragePoolType.CLVM_NG);
snapshotManager.removeClvmPrimarySnapshotStoreRefIfNeeded(snapshot);
Mockito.verify(snapshotStoreDao).removeBySnapshotStore(3L, 4L, DataStoreRole.Primary);
}
@Test
public void testRemoveClvmPrimarySnapshotStoreRefIfNeeded_NonClvmPool() {
SnapshotInfo snapshot = Mockito.mock(SnapshotInfo.class);
DataStore dataStore = Mockito.mock(DataStore.class);
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
Mockito.when(snapshot.getDataStore()).thenReturn(dataStore);
Mockito.when(dataStore.getId()).thenReturn(5L);
Mockito.when(primaryDataStoreDao.findById(5L)).thenReturn(pool);
Mockito.when(pool.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem);
snapshotManager.removeClvmPrimarySnapshotStoreRefIfNeeded(snapshot);
Mockito.verify(snapshotStoreDao, Mockito.never()).removeBySnapshotStore(
Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
}
}

View File

@ -383,7 +383,7 @@
<a-input v-model:value="form.radossecret" :placeholder="$t('label.rados.secret')" />
</a-form-item>
</div>
<div v-if="form.protocol === 'CLVM'">
<div v-if="form.protocol === 'CLVM' || form.protocol === 'CLVM_NG'">
<a-form-item name="volumegroup" ref="volumegroup" :label="$t('label.volumegroup')">
<a-input v-model:value="form.volumegroup" :placeholder="$t('label.volumegroup')" />
</a-form-item>
@ -607,7 +607,7 @@ export default {
const cluster = this.clusters.find(cluster => cluster.id === this.form.cluster)
this.hypervisorType = cluster.hypervisortype
if (this.hypervisorType === 'KVM') {
this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'Gluster', 'Linstor', 'custom', 'FiberChannel']
this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'CLVM_NG', 'Gluster', 'Linstor', 'custom', 'FiberChannel']
if (this.form.scope === 'host') {
this.protocols.push('Filesystem')
}
@ -729,6 +729,15 @@ export default {
}
return url
},
clvmNgURL (vgname) {
var url
if (vgname.indexOf('://') === -1) {
url = 'clvm_ng://localhost/' + vgname
} else {
url = vgname
}
return url
},
vmfsURL (server, path) {
var url
if (server.indexOf('://') === -1) {
@ -853,6 +862,9 @@ export default {
} else if (values.protocol === 'CLVM') {
var vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' + values.volumegroup) : values.volumegroup
url = this.clvmURL(vg)
} else if (values.protocol === 'CLVM_NG') {
vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' + values.volumegroup) : values.volumegroup
url = this.clvmNgURL(vg)
} else if (values.protocol === 'RBD') {
url = this.rbdURL(values.radosmonitor, values.radospool, values.radosuser, values.radossecret)
if (values.datapool) {

View File

@ -641,6 +641,9 @@ public class UriUtils {
if (url.startsWith("rbd://")) {
return getRbdUrlInfo(url);
}
if (url.toLowerCase().startsWith("clvm://") || url.toLowerCase().startsWith("clvm_ng://")) {
return getClvmUrlInfo(url);
}
URI uri = new URI(UriUtils.encodeURIComponent(url));
return new UriInfo(uri.getScheme(), uri.getHost(), uri.getPath(), uri.getUserInfo(), uri.getPort());
} catch (URISyntaxException e) {
@ -678,6 +681,36 @@ public class UriUtils {
}
}
private static UriInfo getClvmUrlInfo(String url) {
if (url == null || (!url.toLowerCase().startsWith("clvm://") && !url.toLowerCase().startsWith("clvm_ng://"))) {
throw new CloudRuntimeException("CLVM URL must start with \"clvm://\" or \"clvm_ng://\"");
}
String scheme;
String remainder;
if (url.toLowerCase().startsWith("clvm_ng://")) {
scheme = "clvm_ng";
remainder = url.substring(10);
} else {
scheme = "clvm";
remainder = url.substring(7);
}
int firstSlash = remainder.indexOf('/');
String host = (firstSlash == -1) ? remainder : remainder.substring(0, firstSlash);
String path = (firstSlash == -1) ? "/" : remainder.substring(firstSlash);
if (host.isEmpty()) {
host = "localhost";
}
while (path.startsWith("//")) {
path = path.substring(1);
}
return new UriInfo(scheme, host, path, null, -1);
}
public static boolean isUrlForCompressedFile(String url) {
return UriUtils.COMPRESSION_FORMATS.stream().anyMatch(f -> url.toLowerCase().endsWith(f));
}

View File

@ -283,4 +283,180 @@ public class UriUtilsTest {
Pair<String, Integer> url2 = UriUtils.validateUrl("https://www.apache.org");
Assert.assertEquals(url2.first(), "www.apache.org");
}
@Test
public void testGetClvmUriInfoBasic() {
String host = "10.11.12.13";
String url1 = String.format("clvm://%s/vg0/lv0", host);
String url2 = String.format("clvm://%s/vg0", host);
String url3 = String.format("clvm://%s", host);
UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1);
Assert.assertEquals("clvm", info1.getScheme());
Assert.assertEquals(host, info1.getStorageHost());
Assert.assertEquals("/vg0/lv0", info1.getStoragePath());
Assert.assertNull(info1.getUserInfo());
Assert.assertEquals(-1, info1.getPort());
Assert.assertEquals(url1, info1.toString());
UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2);
Assert.assertEquals("clvm", info2.getScheme());
Assert.assertEquals(host, info2.getStorageHost());
Assert.assertEquals("/vg0", info2.getStoragePath());
UriUtils.UriInfo info3 = UriUtils.getUriInfo(url3);
Assert.assertEquals("clvm", info3.getScheme());
Assert.assertEquals(host, info3.getStorageHost());
Assert.assertEquals("/", info3.getStoragePath());
}
@Test
public void testGetClvmNgUriInfoBasic() {
String host = "10.11.12.13";
String url1 = String.format("clvm_ng://%s/vg0/lv0", host);
String url2 = String.format("clvm_ng://%s/vg0", host);
String url3 = String.format("clvm_ng://%s", host);
UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1);
Assert.assertEquals("clvm_ng", info1.getScheme());
Assert.assertEquals(host, info1.getStorageHost());
Assert.assertEquals("/vg0/lv0", info1.getStoragePath());
Assert.assertNull(info1.getUserInfo());
Assert.assertEquals(-1, info1.getPort());
Assert.assertEquals(url1, info1.toString());
UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2);
Assert.assertEquals("clvm_ng", info2.getScheme());
Assert.assertEquals(host, info2.getStorageHost());
Assert.assertEquals("/vg0", info2.getStoragePath());
UriUtils.UriInfo info3 = UriUtils.getUriInfo(url3);
Assert.assertEquals("clvm_ng", info3.getScheme());
Assert.assertEquals(host, info3.getStorageHost());
Assert.assertEquals("/", info3.getStoragePath());
}
@Test
public void testGetClvmUriInfoNoHost() {
String url1 = "clvm:///vg0/lv0";
String url2 = "clvm_ng:///vg0/lv0";
UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1);
Assert.assertEquals("clvm", info1.getScheme());
Assert.assertEquals("localhost", info1.getStorageHost());
Assert.assertEquals("/vg0/lv0", info1.getStoragePath());
UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2);
Assert.assertEquals("clvm_ng", info2.getScheme());
Assert.assertEquals("localhost", info2.getStorageHost());
Assert.assertEquals("/vg0/lv0", info2.getStoragePath());
}
@Test
public void testGetClvmUriInfoMultipleSlashes() {
String url1 = "clvm://host1//vg0//lv0";
String url2 = "clvm_ng://host2///vg1///lv1";
UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1);
Assert.assertEquals("clvm", info1.getScheme());
Assert.assertEquals("host1", info1.getStorageHost());
Assert.assertEquals("/vg0//lv0", info1.getStoragePath());
UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2);
Assert.assertEquals("clvm_ng", info2.getScheme());
Assert.assertEquals("host2", info2.getStorageHost());
Assert.assertEquals("/vg1///lv1", info2.getStoragePath());
}
@Test
public void testGetClvmUriInfoComplexPaths() {
String host = "storage-node1";
String url1 = String.format("clvm://%s/vg-name-with-dashes/lv_name_with_underscores", host);
String url2 = String.format("clvm_ng://%s/vg.name.with.dots/lv-123-456", host);
UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1);
Assert.assertEquals("clvm", info1.getScheme());
Assert.assertEquals(host, info1.getStorageHost());
Assert.assertEquals("/vg-name-with-dashes/lv_name_with_underscores", info1.getStoragePath());
UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2);
Assert.assertEquals("clvm_ng", info2.getScheme());
Assert.assertEquals(host, info2.getStorageHost());
Assert.assertEquals("/vg.name.with.dots/lv-123-456", info2.getStoragePath());
}
@Test
public void testGetClvmUriInfoHostnames() {
String[] hosts = {
"localhost",
"node1",
"storage-node-1",
"storage.example.com",
"10.0.0.1",
"192.168.1.100"
};
for (String host : hosts) {
String clvmUrl = String.format("clvm://%s/vg0/lv0", host);
String clvmNgUrl = String.format("clvm_ng://%s/vg0/lv0", host);
UriUtils.UriInfo clvmInfo = UriUtils.getUriInfo(clvmUrl);
Assert.assertEquals("clvm", clvmInfo.getScheme());
Assert.assertEquals(host, clvmInfo.getStorageHost());
Assert.assertEquals("/vg0/lv0", clvmInfo.getStoragePath());
UriUtils.UriInfo clvmNgInfo = UriUtils.getUriInfo(clvmNgUrl);
Assert.assertEquals("clvm_ng", clvmNgInfo.getScheme());
Assert.assertEquals(host, clvmNgInfo.getStorageHost());
Assert.assertEquals("/vg0/lv0", clvmNgInfo.getStoragePath());
}
}
@Test
public void testGetClvmUriInfoToString() {
String url1 = "clvm://host1/vg0/lv0";
String url2 = "clvm_ng://host2/vg1/lv1";
String url3 = "clvm://localhost/vg0";
Assert.assertEquals(url1, UriUtils.getUriInfo(url1).toString());
Assert.assertEquals(url2, UriUtils.getUriInfo(url2).toString());
Assert.assertEquals(url3, UriUtils.getUriInfo(url3).toString());
}
@Test
public void testGetClvmUriInfoCaseInsensitive() {
String url1 = "CLVM://host1/vg0/lv0";
String url2 = "ClVm://host2/vg1/lv1";
String url3 = "CLVM_NG://host3/vg2/lv2";
String url4 = "clvm_NG://host4/vg3/lv3";
UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1);
Assert.assertEquals("clvm", info1.getScheme());
Assert.assertEquals("host1", info1.getStorageHost());
UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2);
Assert.assertEquals("clvm", info2.getScheme());
Assert.assertEquals("host2", info2.getStorageHost());
UriUtils.UriInfo info3 = UriUtils.getUriInfo(url3);
Assert.assertEquals("clvm_ng", info3.getScheme());
Assert.assertEquals("host3", info3.getStorageHost());
UriUtils.UriInfo info4 = UriUtils.getUriInfo(url4);
Assert.assertEquals("clvm_ng", info4.getScheme());
Assert.assertEquals("host4", info4.getStorageHost());
}
@Test
public void testGetClvmUriInfoIntegration() {
String host = "clvm-host";
String clvmUrl = String.format("clvm://%s/vg0/lv0", host);
String clvmNgUrl = String.format("clvm_ng://%s/vg1/lv1", host);
testGetUriInfoInternal(clvmUrl, host);
testGetUriInfoInternal(clvmNgUrl, host);
}
}