Skip to content

Commit 335e0a1

Browse files
Srivastava, PiyushSrivastava, Piyush
authored andcommitted
Merge branch 'main' of https://github.com/NetApp/cloudstack into bugfix/CSTACKEX-235
2 parents 857eea1 + 65cb855 commit 335e0a1

5 files changed

Lines changed: 177 additions & 85 deletions

File tree

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

Lines changed: 102 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public class IscsiAdmStorageAdaptor implements StorageAdaptor {
4747

4848
private static final Map<String, KVMStoragePool> MapStorageUuidToStoragePool = new HashMap<>();
4949

50+
/** iscsiadm's ISCSI_ERR_NO_OBJS_FOUND: returned by "-m session" when no session is established. */
51+
private static final int ISCSI_ERR_NO_OBJS_FOUND = 21;
52+
53+
/** iscsiadm's ISCSI_ERR_SESS_EXISTS: returned by "--login" when the session is already logged in (e.g. Ubuntu). */
54+
private static final int ISCSI_SESSION_EXISTS_CODE = 15;
55+
5056
@Override
5157
public KVMStoragePool createStoragePool(String uuid, String host, int port, String path, String userInfo, StoragePoolType storagePoolType, Map<String, String> details, boolean isPrimaryStorage) {
5258
IscsiAdmStoragePool storagePool = new IscsiAdmStoragePool(uuid, host, port, storagePoolType, this);
@@ -90,12 +96,16 @@ public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, KVMStoragePool pool
9096

9197
@Override
9298
public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<String, String> details, boolean isVMMigrate) {
99+
final String host = pool.getSourceHost();
100+
final int port = pool.getSourcePort();
101+
final String iqn = getIqn(volumeUuid);
102+
93103
// ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 -o new
94104
Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
95105

96106
iScsiAdmCmd.add("-m", "node");
97-
iScsiAdmCmd.add("-T", getIqn(volumeUuid));
98-
iScsiAdmCmd.add("-p", pool.getSourceHost() + ":" + pool.getSourcePort());
107+
iScsiAdmCmd.add("-T", iqn);
108+
iScsiAdmCmd.add("-p", host + ":" + port);
99109
iScsiAdmCmd.add("-o", "new");
100110

101111
String result = iScsiAdmCmd.execute();
@@ -122,28 +132,12 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
122132
}
123133
}
124134

125-
final String host = pool.getSourceHost();
126-
final int port = pool.getSourcePort();
127-
final String iqn = getIqn(volumeUuid);
128-
129-
// Always try to login; treat benign outcomes as success (idempotent)
130-
iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
131-
iScsiAdmCmd.add("-m", "node");
132-
iScsiAdmCmd.add("-T", iqn);
133-
iScsiAdmCmd.add("-p", host + ":" + port);
134-
iScsiAdmCmd.add("--login");
135-
136-
result = iScsiAdmCmd.execute();
137-
138-
if (!handleLoginResult(result, volumeUuid)) {
135+
// Login is always attempted (idempotent). Rescan runs only if the session already existed
136+
// before login (Oracle re-login exits 0; Ubuntu may return ISCSI_ERR_SESS_EXISTS).
137+
if (!loginOrRescanExistingSession(iqn, host, port, volumeUuid)) {
139138
return false;
140139
}
141140

142-
// If the session already existed, a newly mapped LUN won't be visible until a rescan.
143-
if (result != null) {
144-
rescanIscsiSessions(iqn, host, port);
145-
}
146-
147141
// There appears to be a race condition where logging in to the iSCSI volume via iscsiadm
148142
// returns success before the device has been added to the OS.
149143
// What happens is you get logged in and the device shows up, but the device may not
@@ -154,7 +148,13 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map<S
154148
// After a certain number of tries and a certain waiting period in between tries,
155149
// this method could still return (it should not block indefinitely) (the race condition
156150
// isn't solved here, but made highly unlikely to be a problem).
157-
waitForDiskToBecomeAvailable(volumeUuid, pool);
151+
// If the by-path is missing or is a regular file (not the iSCSI block symlink), size
152+
// stays 0. Return false so connect does not succeed and a raw file is not created at
153+
// that by-path in place of the real LUN device.
154+
if (!waitForDiskToBecomeAvailable(volumeUuid, pool)) {
155+
logger.warn("iSCSI device not ready for target {} at {}:{} after wait", volumeUuid, host, port);
156+
return false;
157+
}
158158

159159
return true;
160160
}
@@ -178,23 +178,76 @@ boolean handleNodeCreateResult(String result, String volumeUuid) {
178178
}
179179

180180
/**
181-
* Checks the result of an iscsiadm login command.
182-
* Returns true if the login succeeded or session already exists, false on failure.
181+
* Checks existing session state, performs login, and rescans only if the session already existed.
182+
*
183+
* Login is always attempted (idempotent). A pre-login session check is required on Oracle,
184+
* where re-login often exits 0; Ubuntu may instead return ISCSI_ERR_SESS_EXISTS (15).
185+
* Session-preexisted must be treated as success first: on Ubuntu, re-login exits 15 with a
186+
* non-null error message that would otherwise be treated as failure.
187+
*
188+
* @return true if login succeeded (and rescan ran when needed), false on login failure
183189
*/
184-
boolean handleLoginResult(String result, String volumeUuid) {
185-
if (result == null) {
186-
logger.debug("Successfully logged in to iSCSI target {}", volumeUuid);
190+
private boolean loginOrRescanExistingSession(String iqn, String host, int port, String volumeUuid) {
191+
boolean sessionAlreadyActive = isIscsiSessionActive(iqn, host, port);
192+
logger.debug("iSCSI session active check for target {} at {}:{}: {}", iqn, host, port, sessionAlreadyActive);
193+
194+
Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
195+
iScsiAdmCmd.add("-m", "node");
196+
iScsiAdmCmd.add("-T", iqn);
197+
iScsiAdmCmd.add("-p", host + ":" + port);
198+
iScsiAdmCmd.add("--login");
199+
200+
String result = iScsiAdmCmd.execute();
201+
boolean sessionPreExisted = (iScsiAdmCmd.getExitValue() == ISCSI_SESSION_EXISTS_CODE) || sessionAlreadyActive;
202+
203+
if (sessionPreExisted) {
204+
logger.debug("iSCSI session for target {} at {}:{} pre-existed, performing rescan", iqn, host, port);
205+
rescanIscsiSessions(iqn, host, port);
187206
return true;
188207
}
189-
String msg = result.toLowerCase();
190-
if (msg.contains("already present") || msg.contains("already logged in") || msg.contains("session exists")) {
191-
logger.debug("iSCSI session already exists for target {}, proceeding", volumeUuid);
208+
if (result == null) {
209+
logger.debug("Successfully logged in to iSCSI target {}", volumeUuid);
192210
return true;
193211
}
194212
logger.debug("Failed to log in to iSCSI target {}: {}", volumeUuid, result);
195213
return false;
196214
}
197215

216+
/**
217+
* Checks whether a session to the given target and portal is already established.
218+
*
219+
* "iscsiadm -m session" exits with ISCSI_ERR_NO_OBJS_FOUND when no session exists, which is a
220+
* normal outcome here. Any other non-zero exit is logged and treated as not confirmed active.
221+
*/
222+
private boolean isIscsiSessionActive(String iqn, String host, int port) {
223+
Script sessionCmd = new Script(true, "iscsiadm", 0, logger);
224+
sessionCmd.add("-m", "session");
225+
226+
OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser();
227+
sessionCmd.executeIgnoreExitValue(parser, ISCSI_ERR_NO_OBJS_FOUND);
228+
int exitValue = sessionCmd.getExitValue();
229+
if (exitValue != 0 && exitValue != ISCSI_ERR_NO_OBJS_FOUND) {
230+
logger.warn("Unable to determine iSCSI session state for target {} at {}:{}: 'iscsiadm -m session' exited with {}",
231+
iqn, host, port, exitValue);
232+
return false;
233+
}
234+
235+
String sessions = parser.getLines();
236+
if (StringUtils.isBlank(sessions)) {
237+
return false;
238+
}
239+
// AllLinesParser uses BufferedReader.readLine() (strips \n, \r\n, and \r) and then
240+
// appends "\n" after each session. split("\n") depends on that separator to walk
241+
// one session per line when multiple sessions are listed.
242+
for (String line : sessions.split("\n")) {
243+
if (line.contains(iqn) && line.contains(host)) {
244+
return true;
245+
}
246+
}
247+
248+
return false;
249+
}
250+
198251
private void rescanIscsiSessions(String iqn, String host, int port) {
199252
Script rescanCmd = new Script(true, "iscsiadm", 0, logger);
200253
rescanCmd.add("-m", "node");
@@ -209,19 +262,23 @@ private void rescanIscsiSessions(String iqn, String host, int port) {
209262
}
210263
}
211264

212-
private void waitForDiskToBecomeAvailable(String volumeUuid, KVMStoragePool pool) {
265+
private boolean waitForDiskToBecomeAvailable(String volumeUuid, KVMStoragePool pool) {
213266
int numberOfTries = 10;
214267
int timeBetweenTries = 1000;
268+
long deviceSize = 0;
215269

216-
while (getPhysicalDisk(volumeUuid, pool).getSize() == 0 && numberOfTries > 0) {
270+
while ((deviceSize = getPhysicalDisk(volumeUuid, pool).getSize()) == 0 && numberOfTries > 0) {
217271
numberOfTries--;
218272

219273
try {
220274
Thread.sleep(timeBetweenTries);
221-
} catch (Exception ex) {
222-
// don't do anything
275+
} catch (InterruptedException ex) {
276+
logger.warn("Interrupted while waiting for iSCSI device {} to become available", volumeUuid, ex);
277+
return false;
223278
}
224279
}
280+
281+
return deviceSize > 0;
225282
}
226283

227284
private void waitForDiskToBecomeUnavailable(String host, int port, String iqn, String lun) {
@@ -290,8 +347,17 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) {
290347

291348
private long getDeviceSize(String deviceByPath) {
292349
try {
293-
if (!Files.exists(Paths.get(deviceByPath))) {
294-
logger.debug("Device by-path does not exist yet: " + deviceByPath);
350+
Path devicePath = Paths.get(deviceByPath);
351+
if (!Files.exists(devicePath)) {
352+
logger.debug("Device by-path does not exist yet: {}", deviceByPath);
353+
return 0L;
354+
}
355+
if (Files.isRegularFile(devicePath)) {
356+
logger.warn("Found a corrupt regular file at iSCSI by-path {} (expected block device symlink); it must be removed manually", deviceByPath);
357+
return 0L;
358+
}
359+
if (!Files.isSymbolicLink(devicePath)) {
360+
logger.warn("Path {} exists but is not an iSCSI block device symlink", deviceByPath);
295361
return 0L;
296362
}
297363
} catch (Exception ex) {

plugins/storage/volume/ontap/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ The NetApp ONTAP Storage Plugin provides integration between Apache CloudStack a
6565

6666
### Minimum Volume Size
6767

68-
ONTAP requires a minimum volume size of **1.56 GB** (1,677,721,600 bytes). The plugin will automatically adjust requested sizes below this threshold.
68+
ONTAP requires a minimum volume size of **20 MB** (20,971,520 bytes). Requests below this threshold are rejected.
6969

7070
## Configuration
7171

@@ -116,7 +116,7 @@ username=admin;password=secretpass;svmName=svm1;protocol=ISCSI;managementLIF=192
116116

117117
3. **Capacity Errors**
118118
- Check aggregate space availability
119-
- Ensure requested volume size meets minimum requirements (1.56 GB)
119+
- Ensure requested volume size meets minimum requirements (20 MB)
120120

121121
4. **Host Connection Issues**
122122
- For iSCSI: Verify host IQN is properly configured in host's storage URL

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public class OntapPrimaryDatastoreLifecycle extends BasePrimaryDataStoreLifeCycl
8383
@Inject private AlertManager _alertMgr;
8484
private static final Logger logger = LogManager.getLogger(OntapPrimaryDatastoreLifecycle.class);
8585

86-
private static final long ONTAP_MIN_VOLUME_SIZE_IN_BYTES = 1677721600L;
86+
private static final long ONTAP_MIN_VOLUME_SIZE_IN_BYTES = 20971520L;
8787

8888
/**
8989
* Creates primary storage on NetApp storage
@@ -113,7 +113,7 @@ public DataStore initialize(Map<String, Object> dsInfos) {
113113
@SuppressWarnings("unchecked")
114114
Map<String, String> details = (Map<String, String>) dsInfos.get("details");
115115

116-
capacityBytes = validateInitializeInputs(capacityBytes, podId, clusterId, zoneId, storagePoolName, providerName, managed, details);
116+
validateInitializeInputs(capacityBytes, podId, clusterId, zoneId, storagePoolName, providerName, managed, details);
117117

118118
PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters();
119119
if (clusterId != null) {
@@ -212,16 +212,16 @@ public DataStore initialize(Map<String, Object> dsInfos) {
212212
return _dataStoreHelper.createPrimaryDataStore(parameters);
213213
}
214214

215-
private long validateInitializeInputs(Long capacityBytes, Long podId, Long clusterId, Long zoneId,
215+
private void validateInitializeInputs(Long capacityBytes, Long podId, Long clusterId, Long zoneId,
216216
String storagePoolName, String providerName, boolean managed, Map<String, String> details) {
217217

218-
// Validate and set capacity
219218
if (capacityBytes == null || capacityBytes <= 0) {
220-
logger.warn("capacityBytes not provided or invalid (" + capacityBytes + "), using ONTAP minimum size: " + ONTAP_MIN_VOLUME_SIZE_IN_BYTES);
221-
capacityBytes = ONTAP_MIN_VOLUME_SIZE_IN_BYTES;
222-
} else if (capacityBytes < ONTAP_MIN_VOLUME_SIZE_IN_BYTES) {
223-
logger.warn("capacityBytes (" + capacityBytes + ") is below ONTAP minimum (" + ONTAP_MIN_VOLUME_SIZE_IN_BYTES + "), adjusting to minimum");
224-
capacityBytes = ONTAP_MIN_VOLUME_SIZE_IN_BYTES;
219+
throw new InvalidParameterValueException("Storage pool capacity is required for ONTAP primary storage and must be at least "
220+
+ ONTAP_MIN_VOLUME_SIZE_IN_BYTES + " bytes (20 MB)");
221+
}
222+
if (capacityBytes < ONTAP_MIN_VOLUME_SIZE_IN_BYTES) {
223+
throw new InvalidParameterValueException("Storage pool capacity " + capacityBytes + " bytes is below the ONTAP minimum volume size of "
224+
+ ONTAP_MIN_VOLUME_SIZE_IN_BYTES + " bytes (20 MB)");
225225
}
226226

227227
// Validate scope
@@ -278,8 +278,6 @@ private long validateInitializeInputs(Long capacityBytes, Long podId, Long clust
278278
missing.removeAll(providedKeys);
279279
throw new CloudRuntimeException("ONTAP primary storage creation failed, missing detail(s): " + missing);
280280
}
281-
282-
return capacityBytes;
283281
}
284282

285283
private void processDataLifSelection(Pair<String, String> lifResult, Map<String, String> details,

0 commit comments

Comments
 (0)