Skip to content

Commit b58fd24

Browse files
Merge branch 'main' of github.com:netapp/cloudstack into sync/apache-main-apr-2026
2 parents ed575cc + a6e4b49 commit b58fd24

42 files changed

Lines changed: 9503 additions & 378 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase<SnapshotDataStoreVO
6767
private SearchBuilder<SnapshotDataStoreVO> searchFilteringStoreIdEqStoreRoleEqStateNeqRefCntNeq;
6868
protected SearchBuilder<SnapshotDataStoreVO> searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq;
6969
private SearchBuilder<SnapshotDataStoreVO> stateSearch;
70+
private SearchBuilder<SnapshotDataStoreVO> idStateNeqSearch;
7071
private SearchBuilder<SnapshotDataStoreVO> idStateNinSearch;
71-
private SearchBuilder<SnapshotDataStoreVO> idEqRoleEqStateInSearch;
7272
protected SearchBuilder<SnapshotVO> snapshotVOSearch;
7373
private SearchBuilder<SnapshotDataStoreVO> snapshotCreatedSearch;
7474
private SearchBuilder<SnapshotDataStoreVO> dataStoreAndInstallPathSearch;
@@ -147,16 +147,16 @@ public boolean configure(String name, Map<String, Object> params) throws Configu
147147
stateSearch.done();
148148

149149

150+
idStateNeqSearch = createSearchBuilder();
151+
idStateNeqSearch.and(SNAPSHOT_ID, idStateNeqSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ);
152+
idStateNeqSearch.and(STATE, idStateNeqSearch.entity().getState(), SearchCriteria.Op.NEQ);
153+
idStateNeqSearch.done();
154+
150155
idStateNinSearch = createSearchBuilder();
151156
idStateNinSearch.and(SNAPSHOT_ID, idStateNinSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ);
152-
idStateNinSearch.and(STATE, idStateNinSearch.entity().getState(), SearchCriteria.Op.NOTIN);
157+
idStateNinSearch.and(STATE, idStateNinSearch.entity().getState(), SearchCriteria.Op.NIN);
153158
idStateNinSearch.done();
154159

155-
idEqRoleEqStateInSearch = createSearchBuilder();
156-
idEqRoleEqStateInSearch.and(SNAPSHOT_ID, idEqRoleEqStateInSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ);
157-
idEqRoleEqStateInSearch.and(STORE_ROLE, idEqRoleEqStateInSearch.entity().getRole(), SearchCriteria.Op.EQ);
158-
idEqRoleEqStateInSearch.and(STATE, idEqRoleEqStateInSearch.entity().getState(), SearchCriteria.Op.IN);
159-
160160
snapshotVOSearch = snapshotDao.createSearchBuilder();
161161
snapshotVOSearch.and(VOLUME_ID, snapshotVOSearch.entity().getVolumeId(), SearchCriteria.Op.EQ);
162162
snapshotVOSearch.done();
@@ -393,15 +393,6 @@ public SnapshotDataStoreVO findBySnapshotIdAndDataStoreRoleAndState(long snapsho
393393
return findOneBy(sc);
394394
}
395395

396-
@Override
397-
public List<SnapshotDataStoreVO> listBySnapshotIdAndDataStoreRoleAndStateIn(long snapshotId, DataStoreRole role, State... state) {
398-
SearchCriteria<SnapshotDataStoreVO> sc = idEqRoleEqStateInSearch.create();
399-
sc.setParameters(SNAPSHOT_ID, snapshotId);
400-
sc.setParameters(STORE_ROLE, role);
401-
sc.setParameters(STATE, (Object[])state);
402-
return listBy(sc);
403-
}
404-
405396
@Override
406397
public SnapshotDataStoreVO findOneBySnapshotId(long snapshotId, long zoneId) {
407398
try (TransactionLegacy transactionLegacy = TransactionLegacy.currentTxn()) {
@@ -495,7 +486,7 @@ public List<SnapshotDataStoreVO> findBySnapshotId(long snapshotId) {
495486

496487
@Override
497488
public List<SnapshotDataStoreVO> findBySnapshotIdWithNonDestroyedState(long snapshotId) {
498-
SearchCriteria<SnapshotDataStoreVO> sc = idStateNinSearch.create();
489+
SearchCriteria<SnapshotDataStoreVO> sc = idStateNeqSearch.create();
499490
sc.setParameters(SNAPSHOT_ID, snapshotId);
500491
sc.setParameters(STATE, State.Destroyed.name());
501492
return listBy(sc);

engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStra
7777

7878
private static final List<Storage.StoragePoolType> supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.SharedMountPoint);
7979

80+
private static final String ONTAP_PROVIDER_NAME = "NetApp ONTAP";
81+
8082
@Inject
8183
protected SnapshotDataStoreDao snapshotDataStoreDao;
8284

@@ -325,6 +327,11 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe
325327
List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
326328
for (VolumeVO volume : volumes) {
327329
StoragePoolVO storagePoolVO = storagePool.findById(volume.getPoolId());
330+
if (storagePoolVO.isManaged() && ONTAP_PROVIDER_NAME.equals(storagePoolVO.getStorageProviderName())) {
331+
logger.debug(String.format("%s as the VM has a volume on ONTAP managed storage pool [%s]. " +
332+
"ONTAP managed storage has its own dedicated VM snapshot strategy.", cantHandleLog, storagePoolVO.getName()));
333+
return StrategyPriority.CANT_HANDLE;
334+
}
328335
if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) {
329336
logger.debug(String.format("%s as the VM has a volume that is in a storage with unsupported type [%s].", cantHandleLog, storagePoolVO.getPoolType()));
330337
return StrategyPriority.CANT_HANDLE;
@@ -503,8 +510,9 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map<VolumeInf
503510
return processCreateVmSnapshotAnswer(vmSnapshot, volumeInfoToSnapshotObjectMap, createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO, virtualSize, parentSnapshotVo);
504511
}
505512

506-
logger.error("Disk-only VM snapshot for VM [{}] failed{}.", userVm.getUuid(), answer != null ? " due to" + answer.getDetails() : "");
507-
throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed.", userVm.getUuid()));
513+
String details = answer != null ? answer.getDetails() : String.format("No answer received from host [%s]. The host may be unreachable.", hostId);
514+
logger.error("Disk-only VM snapshot for VM [{}] failed due to: {}.", userVm.getUuid(), details);
515+
throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed due to: %s.", userVm.getUuid(), details));
508516
}
509517

510518
/**

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,15 @@ private void createManagedVolumeCopyTemplateAsync(VolumeInfo volumeInfo, Primary
13401340
primaryDataStore.setDetails(details);
13411341

13421342
grantAccess(volumeInfo, destHost, primaryDataStore);
1343+
volumeInfo = volFactory.getVolume(volumeInfo.getId(), primaryDataStore);
1344+
// For Netapp ONTAP iscsiName or Lun path is available only after grantAccess
1345+
String managedStoreTarget = volumeInfo.get_iScsiName() != null ? volumeInfo.get_iScsiName() : volumeInfo.getUuid();
1346+
details.put(PrimaryDataStore.MANAGED_STORE_TARGET, managedStoreTarget);
1347+
primaryDataStore.setDetails(details);
1348+
// Update destTemplateInfo with the iSCSI path from volumeInfo
1349+
if (destTemplateInfo instanceof TemplateObject) {
1350+
((TemplateObject)destTemplateInfo).setInstallPath(volumeInfo.getPath());
1351+
}
13431352

13441353
try {
13451354
motionSrv.copyAsync(srcTemplateInfo, destTemplateInfo, destHost, caller);

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ protected Answer takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotComma
106106
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null);
107107
}
108108
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, e.getMessage(), null);
109+
} catch (Exception e) {
110+
String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage());
111+
logger.error(errorMsg, e);
112+
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null);
109113
} finally {
110114
if (dm != null) {
111115
try {
@@ -146,21 +150,13 @@ protected Answer takeDiskOnlyVmSnapshotOfStoppedVm(CreateDiskOnlyVmSnapshotComma
146150
}
147151
} catch (LibvirtException | QemuImgException e) {
148152
logger.error("Exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
149-
for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
150-
Pair<Long, String> volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid());
151-
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
152-
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
153-
154-
if (volSizeAndNewPath == null) {
155-
continue;
156-
}
157-
try {
158-
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second())));
159-
} catch (IOException ex) {
160-
logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex);
161-
}
162-
}
153+
cleanupLeftoverDeltas(volumeObjectTos, mapVolumeToSnapshotSizeAndNewVolumePath, storagePoolMgr);
163154
return new Answer(cmd, e);
155+
} catch (Exception e) {
156+
logger.error("Unexpected exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
157+
cleanupLeftoverDeltas(volumeObjectTos, mapVolumeToSnapshotSizeAndNewVolumePath, storagePoolMgr);
158+
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false,
159+
String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()), null);
164160
}
165161

166162
return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSizeAndNewVolumePath);
@@ -192,6 +188,23 @@ protected Pair<String, Map<String, Pair<Long, String>>> createSnapshotXmlAndNewV
192188
return new Pair<>(snapshotXml, volumeObjectToNewPathMap);
193189
}
194190

191+
protected void cleanupLeftoverDeltas(List<VolumeObjectTO> volumeObjectTos, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath, KVMStoragePoolManager storagePoolMgr) {
192+
for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
193+
Pair<Long, String> volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid());
194+
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
195+
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());
196+
197+
if (volSizeAndNewPath == null) {
198+
continue;
199+
}
200+
try {
201+
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second())));
202+
} catch (IOException ex) {
203+
logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex);
204+
}
205+
}
206+
}
207+
195208
protected long getFileSize(String path) {
196209
return new File(path).length();
197210
}

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.HashMap;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.nio.file.Files;
23+
import java.nio.file.Paths;
2224

2325
import org.apache.cloudstack.utils.qemu.QemuImg;
2426
import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
@@ -96,10 +98,15 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
9698
String result = iScsiAdmCmd.execute();
9799

98100
if (result != null) {
99-
logger.debug("Failed to add iSCSI target " + volumeUuid);
100-
System.out.println("Failed to add iSCSI target " + volumeUuid);
101+
// Node record may already exist from a previous run; accept and proceed
102+
if (isNonFatalNodeCreate(result)) {
103+
logger.debug("iSCSI node already exists for {}@{}:{}, proceeding", getIqn(volumeUuid), pool.getSourceHost(), pool.getSourcePort());
104+
} else {
105+
logger.debug("Failed to add iSCSI target " + volumeUuid);
106+
System.out.println("Failed to add iSCSI target " + volumeUuid);
101107

102-
return false;
108+
return false;
109+
}
103110
} else {
104111
logger.debug("Successfully added iSCSI target " + volumeUuid);
105112
System.out.println("Successfully added to iSCSI target " + volumeUuid);
@@ -123,21 +130,39 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
123130
}
124131
}
125132

126-
// ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --login
127-
iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
133+
final String host = pool.getSourceHost();
134+
final int port = pool.getSourcePort();
135+
final String iqn = getIqn(volumeUuid);
128136

137+
// Always try to login; treat benign outcomes as success (idempotent)
138+
iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
129139
iScsiAdmCmd.add("-m", "node");
130-
iScsiAdmCmd.add("-T", getIqn(volumeUuid));
131-
iScsiAdmCmd.add("-p", pool.getSourceHost() + ":" + pool.getSourcePort());
140+
iScsiAdmCmd.add("-T", iqn);
141+
iScsiAdmCmd.add("-p", host + ":" + port);
132142
iScsiAdmCmd.add("--login");
133143

134144
result = iScsiAdmCmd.execute();
135145

136146
if (result != null) {
137-
logger.debug("Failed to log in to iSCSI target " + volumeUuid);
138-
System.out.println("Failed to log in to iSCSI target " + volumeUuid);
147+
if (isNonFatalLogin(result)) {
148+
logger.debug("iSCSI login returned benign message for {}@{}:{}: {}", iqn, host, port, result);
149+
// Session already exists — a newly mapped LUN won't be visible until
150+
// the kernel's next periodic SCSI scan (~30-60s).
151+
Script rescanCmd = new Script(true, "iscsiadm", 0, logger);
152+
rescanCmd.add("-m", "session");
153+
rescanCmd.add("--rescan");
154+
String rescanResult = rescanCmd.execute();
155+
if (rescanResult != null) {
156+
logger.warn("iSCSI session rescan returned: {}", rescanResult);
157+
} else {
158+
logger.debug("iSCSI session rescan completed successfully for {}@{}:{}", iqn, host, port);
159+
}
160+
} else {
161+
logger.debug("Failed to log in to iSCSI target " + volumeUuid + ": " + result);
162+
System.out.println("Failed to log in to iSCSI target " + volumeUuid);
139163

140-
return false;
164+
return false;
165+
}
141166
} else {
142167
logger.debug("Successfully logged in to iSCSI target " + volumeUuid);
143168
System.out.println("Successfully logged in to iSCSI target " + volumeUuid);
@@ -158,8 +183,23 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
158183
return true;
159184
}
160185

186+
// Removed sessionExists() call to avoid noisy sudo/iscsiadm session queries that may fail on some setups
187+
188+
private boolean isNonFatalLogin(String result) {
189+
if (result == null) return true;
190+
String msg = result.toLowerCase();
191+
// Accept messages where the session already exists
192+
return msg.contains("already present") || msg.contains("already logged in") || msg.contains("session exists");
193+
}
194+
195+
private boolean isNonFatalNodeCreate(String result) {
196+
if (result == null) return true;
197+
String msg = result.toLowerCase();
198+
return msg.contains("already exists") || msg.contains("database exists") || msg.contains("exists");
199+
}
200+
161201
private void waitForDiskToBecomeAvailable(String volumeUuid, KVMStoragePool pool) {
162-
int numberOfTries = 10;
202+
int numberOfTries = 30;
163203
int timeBetweenTries = 1000;
164204

165205
while (getPhysicalDisk(volumeUuid, pool).getSize() == 0 && numberOfTries > 0) {
@@ -238,6 +278,15 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) {
238278
}
239279

240280
private long getDeviceSize(String deviceByPath) {
281+
try {
282+
if (!Files.exists(Paths.get(deviceByPath))) {
283+
logger.debug("Device by-path does not exist yet: " + deviceByPath);
284+
return 0L;
285+
}
286+
} catch (Exception ignore) {
287+
// If FS check fails for any reason, fall back to blockdev call
288+
}
289+
241290
Script iScsiAdmCmd = new Script(true, "blockdev", 0, logger);
242291

243292
iScsiAdmCmd.add("--getsize64", deviceByPath);
@@ -280,8 +329,47 @@ private String getComponent(String path, int index) {
280329
return tmp[index].trim();
281330
}
282331

332+
/**
333+
* Check if there are other LUNs on the same iSCSI target (IQN) that are still
334+
* visible as block devices. This is needed because ONTAP uses a single IQN per
335+
* SVM — logging out of the target would kill ALL LUNs, not just the one being
336+
* disconnected.
337+
*
338+
* Checks /dev/disk/by-path/ for symlinks matching the same host:port + IQN but
339+
* with a different LUN number.
340+
*/
341+
private boolean hasOtherActiveLuns(String host, int port, String iqn, String lun) {
342+
String prefix = "ip-" + host + ":" + port + "-iscsi-" + iqn + "-lun-";
343+
java.io.File byPathDir = new java.io.File("/dev/disk/by-path");
344+
if (!byPathDir.exists() || !byPathDir.isDirectory()) {
345+
return false;
346+
}
347+
java.io.File[] entries = byPathDir.listFiles();
348+
if (entries == null) {
349+
return false;
350+
}
351+
for (java.io.File entry : entries) {
352+
String name = entry.getName();
353+
if (name.startsWith(prefix) && !name.equals(prefix + lun)) {
354+
logger.debug("Found other active LUN on same target: " + name);
355+
return true;
356+
}
357+
}
358+
return false;
359+
}
360+
283361
private boolean disconnectPhysicalDisk(String host, int port, String iqn, String lun) {
284-
// use iscsiadm to log out of the iSCSI target and un-discover it
362+
// Check if other LUNs on the same IQN target are still in use.
363+
// ONTAP (and similar) uses a single IQN per SVM with multiple LUNs.
364+
// Doing iscsiadm --logout tears down the ENTIRE target session,
365+
// which would destroy access to ALL LUNs — not just the one being disconnected.
366+
if (hasOtherActiveLuns(host, port, iqn, lun)) {
367+
logger.info("Skipping iSCSI logout for /" + iqn + "/" + lun +
368+
" — other LUNs on the same target are still active");
369+
return true;
370+
}
371+
372+
// No other LUNs active on this target — safe to logout and delete the node record.
285373

286374
// ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --logout
287375
Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
@@ -422,6 +510,19 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk srcDisk, String destVolu
422510
try {
423511
QemuImg q = new QemuImg(timeout);
424512
q.convert(srcFile, destFile);
513+
// Below fix is required when vendor depends on host based copy rather than storage CAN_CREATE_VOLUME_FROM_VOLUME capability
514+
// When host based template copy is triggered , small size template sits in RAM(depending on host memory and RAM) and copy is marked successful and by the time flush to storage is triggered
515+
// disconnectPhysicalDisk would disconnect the lun , hence template staying in RAM is not copied to storage lun. Below does flushing of data to storage and marking
516+
// copy as successful once flush is complete.
517+
Script flushCmd = new Script(true, "blockdev", 0, logger);
518+
flushCmd.add("--flushbufs", destDisk.getPath());
519+
String flushResult = flushCmd.execute();
520+
if (flushResult != null) {
521+
logger.warn("iSCSI copyPhysicalDisk: blockdev --flushbufs returned: {}", flushResult);
522+
}
523+
Script syncCmd = new Script(true, "sync", 0, logger);
524+
syncCmd.execute();
525+
logger.info("iSCSI copyPhysicalDisk: flush/sync completed ");
425526
} catch (QemuImgException | LibvirtException ex) {
426527
String msg = "Failed to copy data from " + srcDisk.getPath() + " to " +
427528
destDisk.getPath() + ". The error was the following: " + ex.getMessage();

0 commit comments

Comments
 (0)