Copilot commented on code in PR #13897:
URL: https://github.com/apache/cloudstack/pull/13897#discussion_r3835437884


##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java:
##########
@@ -307,10 +442,10 @@ private ExportPolicy 
createExportPolicyRequest(AccessGroup accessGroup,String sv
         List<ExportRule.ExportClient> exportClients = new ArrayList<>();
         List<HostVO> hosts = accessGroup.getHostsToConnect();
         for (HostVO host : hosts) {
-            String hostStorageIp = host.getStorageIpAddress();
+            String hostStorageIp = host.getStorageIpAddress() != null ? 
host.getStorageIpAddress().trim() : null;
             String ip = (hostStorageIp != null && !hostStorageIp.isEmpty())
                     ? hostStorageIp
-                    : host.getPrivateIpAddress();
+                    : (host.getPrivateIpAddress() != null ? 
host.getPrivateIpAddress().trim() : null);
             String ipToUse = ip + "/32";

Review Comment:
   createExportPolicyRequest can throw a NullPointerException when a host has 
neither storageIpAddress nor privateIpAddress: `ip` becomes null and `ip + 
"/32"` will NPE. This can break pool registration / export-policy creation for 
partially configured hosts.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java:
##########
@@ -209,19 +262,23 @@ private void rescanIscsiSessions(String iqn, String host, 
int port) {
         }
     }
 
-    private void waitForDiskToBecomeAvailable(String volumeUuid, 
KVMStoragePool pool) {
+    private boolean waitForDiskToBecomeAvailable(String volumeUuid, 
KVMStoragePool pool) {
         int numberOfTries = 10;
         int timeBetweenTries = 1000;
+        long deviceSize = 0;
 
-        while (getPhysicalDisk(volumeUuid, pool).getSize() == 0 && 
numberOfTries > 0) {
+        while ((deviceSize = getPhysicalDisk(volumeUuid, pool).getSize()) == 0 
&& numberOfTries > 0) {
             numberOfTries--;
 
             try {
                 Thread.sleep(timeBetweenTries);
-            } catch (Exception ex) {
-                // don't do anything
+            } catch (InterruptedException ex) {
+                logger.warn("Interrupted while waiting for iSCSI device {} to 
become available", volumeUuid, ex);
+                return false;

Review Comment:
   waitForDiskToBecomeAvailable swallows InterruptedException without restoring 
the thread interrupt flag. This can interfere with higher-level 
shutdown/timeout handling and leave the thread in an unexpected state.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java:
##########
@@ -178,23 +178,76 @@ boolean handleNodeCreateResult(String result, String 
volumeUuid) {
     }
 
     /**
-     * Checks the result of an iscsiadm login command.
-     * Returns true if the login succeeded or session already exists, false on 
failure.
+     * Checks existing session state, performs login, and rescans only if the 
session already existed.
+     *
+     * Login is always attempted (idempotent). A pre-login session check is 
required on Oracle,
+     * where re-login often exits 0; Ubuntu may instead return 
ISCSI_ERR_SESS_EXISTS (15).
+     * Session-preexisted must be treated as success first: on Ubuntu, 
re-login exits 15 with a
+     * non-null error message that would otherwise be treated as failure.
+     *
+     * @return true if login succeeded (and rescan ran when needed), false on 
login failure
      */
-    boolean handleLoginResult(String result, String volumeUuid) {
-        if (result == null) {
-            logger.debug("Successfully logged in to iSCSI target {}", 
volumeUuid);
+    private boolean loginOrRescanExistingSession(String iqn, String host, int 
port, String volumeUuid) {
+        boolean sessionAlreadyActive = isIscsiSessionActive(iqn, host, port);
+        logger.debug("iSCSI session active check for target {} at {}:{}: {}", 
iqn, host, port, sessionAlreadyActive);
+
+        Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger);
+        iScsiAdmCmd.add("-m", "node");
+        iScsiAdmCmd.add("-T", iqn);
+        iScsiAdmCmd.add("-p", host + ":" + port);
+        iScsiAdmCmd.add("--login");
+
+        String result = iScsiAdmCmd.execute();
+        boolean sessionPreExisted = (iScsiAdmCmd.getExitValue() == 
ISCSI_SESSION_EXISTS_CODE) || sessionAlreadyActive;
+
+        if (sessionPreExisted) {
+            logger.debug("iSCSI session for target {} at {}:{} pre-existed, 
performing rescan", iqn, host, port);
+            rescanIscsiSessions(iqn, host, port);
             return true;
         }
-        String msg = result.toLowerCase();
-        if (msg.contains("already present") || msg.contains("already logged 
in") || msg.contains("session exists")) {
-            logger.debug("iSCSI session already exists for target {}, 
proceeding", volumeUuid);
+        if (result == null) {
+            logger.debug("Successfully logged in to iSCSI target {}", 
volumeUuid);
             return true;
         }
         logger.debug("Failed to log in to iSCSI target {}: {}", volumeUuid, 
result);
         return false;
     }
 
+    /**
+     * Checks whether a session to the given target and portal is already 
established.
+     *
+     * "iscsiadm -m session" exits with ISCSI_ERR_NO_OBJS_FOUND when no 
session exists, which is a
+     * normal outcome here. Any other non-zero exit is logged and treated as 
not confirmed active.
+     */
+    private boolean isIscsiSessionActive(String iqn, String host, int port) {
+        Script sessionCmd = new Script(true, "iscsiadm", 0, logger);
+        sessionCmd.add("-m", "session");
+
+        OutputInterpreter.AllLinesParser parser = new 
OutputInterpreter.AllLinesParser();
+        sessionCmd.executeIgnoreExitValue(parser, ISCSI_ERR_NO_OBJS_FOUND);
+        int exitValue = sessionCmd.getExitValue();
+        if (exitValue != 0 && exitValue != ISCSI_ERR_NO_OBJS_FOUND) {
+            logger.warn("Unable to determine iSCSI session state for target {} 
at {}:{}: 'iscsiadm -m session' exited with {}",
+                    iqn, host, port, exitValue);
+            return false;
+        }
+
+        String sessions = parser.getLines();
+        if (StringUtils.isBlank(sessions)) {
+            return false;
+        }
+        // AllLinesParser uses BufferedReader.readLine() (strips \n, \r\n, and 
\r) and then
+        // appends "\n" after each session. split("\n") depends on that 
separator to walk
+        // one session per line when multiple sessions are listed.
+        for (String line : sessions.split("\n")) {
+            if (line.contains(iqn) && line.contains(host)) {
+                return true;
+            }
+        }

Review Comment:
   isIscsiSessionActive only checks `line.contains(host)` and ignores the 
portal port, which can return a false-positive when the same target IQN is 
logged in to the same host on a different port. That can cause an unnecessary 
rescan path and mask a real login failure.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java:
##########
@@ -131,54 +156,67 @@ public boolean connect() {
                 logger.error("ISCSI protocol is not enabled on SVM " + 
svmName);
                 throw new CloudRuntimeException("ISCSI protocol is not enabled 
on SVM " + svmName);
             }
-            List<Aggregate> aggrs = svm.getAggregates();
-            if (aggrs == null || aggrs.isEmpty()) {
-                logger.error("No aggregates are assigned to SVM " + svmName);
-                throw new CloudRuntimeException("No aggregates are assigned to 
SVM " + svmName);
-            }
-            // Collect all online aggregates assigned to the SVM. 
Capacity-based selection is
-            // intentionally deferred to createStorageVolume(name, size), 
which validates the
-            // available space against the actual requested volume size.
-            List<Aggregate> eligibleAggregates = new ArrayList<>();
-            for (Aggregate aggr : aggrs) {
-                logger.debug("Found aggregate: " + aggr.getName() + " with 
UUID: " + aggr.getUuid());
-                Aggregate aggrResp = 
aggregateFeignClient.getAggregateByUUID(authHeader, aggr.getUuid());
-                if (aggrResp == null) {
-                    logger.warn("Aggregate details response is null for 
aggregate " + aggr.getName() + ". Skipping.");
-                    continue;
-                }
-                if (!Objects.equals(aggrResp.getState(), 
Aggregate.StateEnum.ONLINE)) {
-                    logger.warn("Aggregate " + aggr.getName() + " is not in 
online state. Skipping this aggregate.");
-                    continue;
-                }
-                logger.debug("Aggregate " + aggr.getName() + " is online and 
eligible for volume operations.");
-                eligibleAggregates.add(aggr);
-            }
-            if (eligibleAggregates.isEmpty()) {
-                logger.error("No suitable aggregates found on SVM " + svmName 
+ " for volume operations.");
-                throw new CloudRuntimeException("No suitable aggregates found 
on SVM " + svmName + " for volume operations.");
+            this.resolvedSvmUuid = svm.getUuid();
+
+            if (validateAggregatesForVolumeCreation) {
+                validateAndSelectAggregatesForVolumeCreation(authHeader, 
svmName, svm.getAggregates());
+            } else {
+                logger.debug("Skipping aggregate capacity validation — not 
required for existing-volume operations");
             }
-            this.aggregates = eligibleAggregates;
-            logger.info("Found " + eligibleAggregates.size() + " online 
aggregate(s) on SVM " + svmName + " for volume operations.");
 
             logger.info("Successfully connected to ONTAP cluster and validated 
ONTAP details provided");
+        } catch (CloudRuntimeException e) {
+            throw e;
         } catch (FeignException.Unauthorized e) {
-            logger.error("Authentication failed while connecting to ONTAP 
cluster at " + storage.getStorageIP() +
-                    ". Please verify the username and password.", e);
-            throw new CloudRuntimeException("Authentication failed: Invalid 
credentials for ONTAP cluster at " +
-                    storage.getStorageIP() + ". Please verify the username and 
password.");
-        } catch (FeignException.Forbidden e) {
-            logger.error("Authorization failed while connecting to ONTAP 
cluster at " + storage.getStorageIP() +
-                    ". The user does not have sufficient privileges.", e);
-            throw new CloudRuntimeException("Authorization failed: User does 
not have sufficient privileges on ONTAP cluster at " +
-                    storage.getStorageIP() + ". Please verify user 
permissions.");
+            String msg = "Authentication failed: Invalid credentials. Please 
verify the username and password.";
+            logger.error(msg, e);
+            throw new CloudRuntimeException(msg, e);
         } catch (Exception e) {
             logger.error("Failed to connect to ONTAP cluster: " + 
e.getMessage(), e);
             throw new CloudRuntimeException("Failed to connect to ONTAP 
cluster: " + e.getMessage(), e);
         }
         return true;
     }
 
+    /**
+     * ONTAP SVM UUID resolved during the last successful {@link 
#connect(boolean)} call.
+     */
+    public String getResolvedSvmUuid() {
+        return resolvedSvmUuid;
+    }
+
+    private void validateAndSelectAggregatesForVolumeCreation(String 
authHeader, String svmName, List<Aggregate> aggrs) {
+        if (aggrs == null || aggrs.isEmpty()) {
+            logger.error("No aggregates are assigned to SVM " + svmName);
+            throw new CloudRuntimeException("No aggregates are assigned to SVM 
" + svmName);
+        }
+        for (Aggregate aggr : aggrs) {
+            logger.debug("Found aggregate: " + aggr.getName() + " with UUID: " 
+ aggr.getUuid());
+            Aggregate aggrResp = 
aggregateFeignClient.getAggregateByUUID(authHeader, aggr.getUuid(),
+                        Map.of(OntapStorageConstants.FIELDS, 
OntapStorageConstants.AGGREGATE_NODE
+                                + OntapStorageConstants.COMMA + 
OntapStorageConstants.AGGREGATE_SPACE
+                                + OntapStorageConstants.COMMA + 
OntapStorageConstants.STATE));
+            if (aggrResp == null) {
+                logger.warn("Aggregate details response is null for aggregate 
" + aggr.getName() + ". Skipping.");
+                continue;
+            }
+            if (!Objects.equals(aggrResp.getState(), 
Aggregate.StateEnum.ONLINE)) {
+                logger.warn("Aggregate " + aggr.getName() + " is not in online 
state. Skipping this aggregate.");
+                continue;
+            } else if (aggrResp.getSpace() == null || 
aggrResp.getAvailableBlockStorageSpace() == null ||
+                    aggrResp.getAvailableBlockStorageSpace() <= 
storage.getSize().doubleValue()) {
+                logger.warn("Aggregate " + aggr.getName() + " does not have 
sufficient available space. Skipping this aggregate.");
+                continue;
+            }
+            logger.info("Selected aggregate: " + aggr.getName() + " for volume 
operations.");
+            this.aggregates = List.of(aggr);
+        }

Review Comment:
   validateAndSelectAggregatesForVolumeCreation overwrites `this.aggregates` on 
every eligible aggregate, ending up with the *last* eligible aggregate only. 
That changes aggregate selection from “max available space” 
(createStorageVolume’s behavior) to an arbitrary last match, which can lead to 
provisioning on a suboptimal aggregate and incorrect chosenAggregateNode for 
LIF affinity.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to