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


##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageUtils.java:
##########
@@ -36,41 +42,201 @@
 public class OntapStorageUtils {
 
     private static final Logger logger = 
LogManager.getLogger(OntapStorageUtils.class);
-
     private static final String BASIC = "Basic";
     private static final String AUTH_HEADER_COLON = ":";
 
+    /**
+     * Method generates authentication headers using storage backend 
credentials passed as normal string
+     *
+     * @param username -->> username of the storage backend
+     * @param password -->> normal decoded password of the storage backend
+     * @return
+     */
     public static String generateAuthHeader (String username, String password) 
{
         byte[] encodedBytes = Base64Utils.encode((username + AUTH_HEADER_COLON 
+ password).getBytes(StandardCharsets.UTF_8));
         return BASIC + StringUtils.SPACE + new String(encodedBytes);
     }
 
+    public static CloudStackVolume 
createCloudStackVolumeRequestByProtocol(StoragePoolVO storagePool, Map<String, 
String> details, DataObject volumeObject) {
+        CloudStackVolume cloudStackVolumeRequest = null;
+
+        String protocol = details.get(OntapStorageConstants.PROTOCOL);
+        ProtocolType protocolType = ProtocolType.valueOf(protocol);
+        switch (protocolType) {
+            case NFS3:
+                cloudStackVolumeRequest = new CloudStackVolume();
+                
cloudStackVolumeRequest.setDatastoreId(String.valueOf(storagePool.getId()));
+                cloudStackVolumeRequest.setVolumeInfo(volumeObject);
+                break;
+            case ISCSI:
+                Svm svm = new Svm();
+                svm.setName(details.get(OntapStorageConstants.SVM_NAME));
+                cloudStackVolumeRequest = new CloudStackVolume();
+                Lun lunRequest = new Lun();
+                lunRequest.setSvm(svm);
+
+                LunSpace lunSpace = new LunSpace();
+                lunSpace.setSize(volumeObject.getSize());
+                lunRequest.setSpace(lunSpace);
+                //Lun name is full path like in unified 
"/vol/VolumeName/LunName"
+                String lunName = 
volumeObject.getName().replace(OntapStorageConstants.HYPHEN, 
OntapStorageConstants.UNDERSCORE);
+                if(!isValidName(lunName)) {
+                    String errMsg = "createAsync: Invalid dataObject name [" + 
lunName + "]. It must start with a letter and can only contain letters, digits, 
and underscores, and be up to 200 characters long.";
+                    throw new InvalidParameterValueException(errMsg);
+                }
+                String lunFullName = getLunName(storagePool.getName(), 
lunName);
+                lunRequest.setName(lunFullName);
+
+                String osType = 
getOSTypeFromHypervisor(storagePool.getHypervisor().name());
+                lunRequest.setOsType(Lun.OsTypeEnum.valueOf(osType));
+
+                cloudStackVolumeRequest.setLun(lunRequest);
+                break;
+            default:
+                throw new CloudRuntimeException("Unsupported protocol " + 
protocol);
+
+        }
+        return cloudStackVolumeRequest;
+    }
+
+    public static boolean isValidName(String name) {
+        // Check for null and length constraint first
+        if (name == null || name.length() > 200) {
+            return false;
+        }
+        // Regex: Starts with a letter, followed by letters, digits, or 
underscores
+        return name.matches(OntapStorageConstants.ONTAP_NAME_REGEX);
+    }
+
+    public static String getOSTypeFromHypervisor(String hypervisorType) {
+        switch (hypervisorType) {
+            case OntapStorageConstants.KVM:
+                return Lun.OsTypeEnum.LINUX.name();
+            default:
+                String errMsg = "getOSTypeFromHypervisor : Unsupported 
hypervisor type " + hypervisorType + " for ONTAP storage";
+                logger.error(errMsg);
+                throw new CloudRuntimeException(errMsg);
+        }
+    }
+
+    /**
+     * Returns a connected {@link StorageStrategy} for operations on an 
existing pool (snapshots,
+     * delete, revert, grant/revoke). Does not require aggregate free space 
for the full pool size.
+     */
     public static StorageStrategy getStrategyByStoragePoolDetails(Map<String, 
String> details) {
+        return getStrategyByStoragePoolDetails(details, false);
+    }
+
+    public static StorageStrategy getStrategyByStoragePoolDetails(Map<String, 
String> details,
+            boolean validateAggregatesForVolumeCreation) {
         if (details == null || details.isEmpty()) {
             logger.error("getStrategyByStoragePoolDetails: Storage pool 
details are null or empty");
-            throw new CloudRuntimeException("getStrategyByStoragePoolDetails: 
Storage pool details are null or empty");
+            throw new CloudRuntimeException("Storage pool details are null or 
empty");
         }
         String protocol = details.get(OntapStorageConstants.PROTOCOL);
         OntapStorage ontapStorage = new 
OntapStorage(details.get(OntapStorageConstants.USERNAME), 
details.get(OntapStorageConstants.PASSWORD),
-                details.get(OntapStorageConstants.MANAGEMENT_LIF), 
details.get(OntapStorageConstants.SVM_NAME), 
Long.parseLong(details.get(OntapStorageConstants.SIZE)),
-                ProtocolType.valueOf(protocol),
-                
Boolean.parseBoolean(details.get(OntapStorageConstants.IS_DISAGGREGATED)));
+                details.get(OntapStorageConstants.STORAGE_IP), 
details.get(OntapStorageConstants.SVM_NAME), 
Long.parseLong(details.get(OntapStorageConstants.SIZE)),
+                ProtocolType.valueOf(protocol));
         StorageStrategy storageStrategy = 
StorageProviderFactory.getStrategy(ontapStorage);
-        boolean isValid = storageStrategy.connect();
+        boolean isValid = 
storageStrategy.connect(validateAggregatesForVolumeCreation);
         if (isValid) {
             logger.info("Connection to Ontap SVM [{}] successful", 
details.get(OntapStorageConstants.SVM_NAME));
             return storageStrategy;
         } else {
             logger.error("getStrategyByStoragePoolDetails: Connection to Ontap 
SVM [" + details.get(OntapStorageConstants.SVM_NAME) + "] failed");
-            throw new CloudRuntimeException("getStrategyByStoragePoolDetails: 
Connection to Ontap SVM [" + details.get(OntapStorageConstants.SVM_NAME) + "] 
failed");
+            throw new CloudRuntimeException("Connection to Ontap SVM [" + 
details.get(OntapStorageConstants.SVM_NAME) + "] failed");
         }
     }
 
-    public static String getIgroupName(String svmName, ScopeType scopeType, 
Long scopeId) {
-        return OntapStorageConstants.CS + OntapStorageConstants.UNDERSCORE + 
svmName + OntapStorageConstants.UNDERSCORE + scopeType.toString().toLowerCase() 
+ OntapStorageConstants.UNDERSCORE + scopeId;
+    public static String getIgroupName(String svmName, String hostUuid) {
+        //Igroup name format: cs_hostUuid_svmName
+        String sanitizedHostUuid = hostUuid.replaceAll("[^a-zA-Z0-9_-]", "_");
+        String igroupName = OntapStorageConstants.CS + 
OntapStorageConstants.UNDERSCORE + sanitizedHostUuid + 
OntapStorageConstants.UNDERSCORE + svmName;
+        // ONTAP igroup names are limited to 96 characters; truncate if longer.
+        if (igroupName.length() > 
OntapStorageConstants.IGROUP_NAME_MAX_LENGTH) {
+            igroupName = igroupName.substring(0, 
OntapStorageConstants.IGROUP_NAME_MAX_LENGTH);
+        }
+        return igroupName;
     }

Review Comment:
   getIgroupName() still throws NullPointerException when hostUuid is null 
(hostUuid.replaceAll(...)). The PR description calls out this NPE; this method 
should validate inputs (and ideally fail fast with a clear error) before 
sanitizing/truncating.



##########
ui/src/views/infra/zone/ZoneWizardLaunchZone.vue:
##########
@@ -1604,6 +1604,18 @@ export default {
         params.url = this.powerflexURL(this.prefillContent.powerflexGateway, 
this.prefillContent.powerflexGatewayUsername,
           this.prefillContent.powerflexGatewayPassword, 
this.prefillContent.powerflexStoragePool)
       }
+      if (this.prefillContent.provider === 'NetApp ONTAP') {
+        params['details[0].storageIP'] = this.prefillContent.ontapIP
+        params['details[0].username'] = this.prefillContent.ontapUsername
+        params['details[0].password'] = btoa(this.prefillContent.ontapPassword)
+        params['details[0].svmName'] = this.prefillContent.ontapSvmName
+        params['details[0].protocol'] = 
this.prefillContent.primaryStorageProtocol

Review Comment:
   Using btoa() directly for the ONTAP password is fragile: it throws for 
non-Latin1 input and bypasses the UI’s existing base64+UTF-8 helper used 
elsewhere. Consider using $toBase64AndURIEncoded (ui/src/utils/plugins.js) for 
consistent encoding.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java:
##########
@@ -36,23 +39,23 @@ public class StorageProviderFactory {
     public static StorageStrategy getStrategy(OntapStorage ontapStorage) {
         ProtocolType protocol = ontapStorage.getProtocol();
         logger.info("Initializing StorageProviderFactory with protocol: " + 
protocol);
+        String decodedPassword = new 
String(java.util.Base64.getDecoder().decode(ontapStorage.getPassword()), 
StandardCharsets.UTF_8);
+        ontapStorage = new OntapStorage(
+            ontapStorage.getUsername(),
+            decodedPassword,
+            ontapStorage.getStorageIP(),
+            ontapStorage.getSvmName(),
+            ontapStorage.getSize(),
+            protocol);

Review Comment:
   StorageProviderFactory decodes ontapStorage.getPassword() as Base64 
unconditionally. If the stored value is not valid Base64 (e.g. created by older 
code paths / API clients), this will throw IllegalArgumentException and break 
all ONTAP operations at runtime.



##########
ui/src/views/infra/AddPrimaryStorage.vue:
##########
@@ -890,6 +935,14 @@ export default {
           params['details[0].api_username'] = values.flashArrayUsername
           params['details[0].api_password'] = values.flashArrayPassword
           url = values.flashArrayURL
+        } else if (values.provider === 'NetApp ONTAP') {
+          params['details[0].storageIP'] = values.ontapIP
+          params['details[0].username'] = values.ontapUsername
+          params['details[0].password'] = btoa(values.ontapPassword)
+          params['details[0].svmName'] = values.ontapSvmName
+          params['details[0].protocol'] = values.protocol
+          values.managed = true
+          url = this.ontapURL(values.ontapIP)
         }

Review Comment:
   ONTAP password is encoded with btoa(), which is not UTF-8 safe and can throw 
for non-Latin1 characters. The UI already provides $toBase64AndURIEncoded for 
consistent base64 encoding across forms.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java:
##########
@@ -207,67 +217,54 @@ private long validateInitializeInputs(Long capacityBytes, 
Long podId, Long clust
             capacityBytes = ONTAP_MIN_VOLUME_SIZE_IN_BYTES;
         }
 
-        // Scope (pod/cluster/zone) validation
+        // Validate scope
         if (podId == null ^ clusterId == null) {
             throw new CloudRuntimeException("Cluster Id or Pod Id is null, 
cannot create primary storage");
         }
-        if (podId == null && clusterId == null) {
-            if (zoneId != null) {
-                logger.info("Both Pod Id and Cluster Id are null, Primary 
storage pool will be associated with a Zone");
-            } else {
-                throw new CloudRuntimeException("Pod Id, Cluster Id and Zone 
Id are all null, cannot create primary storage");
-            }
+
+        if (podId == null && zoneId == null) {
+            throw new CloudRuntimeException("Pod Id, Cluster Id and Zone Id 
are all null, cannot create primary storage");
+        }
+
+        if (podId == null) {
+            logger.info("Both Pod Id and Cluster Id are null, Primary storage 
pool will be associated with a Zone");
         }
 
-        // Basic parameter validation
         if (StringUtils.isBlank(storagePoolName)) {
             throw new CloudRuntimeException("Storage pool name is null or 
empty, cannot create primary storage");
         }
+
         if (StringUtils.isBlank(providerName)) {
             throw new CloudRuntimeException("Provider name is null or empty, 
cannot create primary storage");
         }
+
         logger.debug("ONTAP primary storage will be created as " + (managed ? 
"managed" : "unmanaged"));
         if (!managed) {
             throw new CloudRuntimeException("ONTAP primary storage must be 
managed");
         }
 
-        // Details key validation
+        //Required ONTAP detail keys
         Set<String> requiredKeys = Set.of(
                 OntapStorageConstants.USERNAME,
                 OntapStorageConstants.PASSWORD,
                 OntapStorageConstants.SVM_NAME,
                 OntapStorageConstants.PROTOCOL,
-                OntapStorageConstants.MANAGEMENT_LIF
-        );
-        Set<String> optionalKeys = Set.of(
-                OntapStorageConstants.IS_DISAGGREGATED
+                OntapStorageConstants.STORAGE_IP
         );
-        Set<String> allowedKeys = new java.util.HashSet<>(requiredKeys);
-        allowedKeys.addAll(optionalKeys);
-
-        if (StringUtils.isNotBlank(url)) {
-            for (String segment : url.split(OntapStorageConstants.SEMICOLON)) {
-                if (segment.isEmpty()) {
-                    continue;
-                }
-                String[] kv = segment.split(OntapStorageConstants.EQUALS, 2);
-                if (kv.length == 2) {
-                    details.put(kv[0].trim(), kv[1].trim());
-                }
-            }
-        }
 
+        // Validate existing entries (reject unexpected keys, empty values)
         for (Map.Entry<String, String> e : details.entrySet()) {
             String key = e.getKey();
             String val = e.getValue();
-            if (!allowedKeys.contains(key)) {
+            if (!requiredKeys.contains(key)) {
                 throw new CloudRuntimeException("Unexpected ONTAP detail key 
in URL: " + key);
             }
             if (StringUtils.isBlank(val)) {
                 throw new CloudRuntimeException("ONTAP primary storage 
creation failed, empty detail: " + key);
             }

Review Comment:
   validateInitializeInputs() throws "Unexpected ONTAP detail key in URL" but 
the code no longer parses ONTAP details from the URL; it validates the provided 
details map. This error message is misleading, and details can also be null 
(causing NPE) since it is not validated before iterating.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java:
##########
@@ -87,6 +97,19 @@ CloudStackVolume updateCloudStackVolume(CloudStackVolume 
cloudstackVolume) {
 
     @Override
     public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) {
+        logger.info("deleteCloudStackVolume: Delete cloudstack volume " + 
cloudstackVolume);
+        try {
+            // Step 1: Send command to KVM host to delete qcow2 file using 
qemu-img
+            Answer answer = 
deleteVolumeOnKVMHost(cloudstackVolume.getVolumeInfo());
+            if (answer == null || !answer.getResult()) {
+                String errMsg = answer != null ? answer.getDetails() : "Failed 
to delete qcow2 on KVM host";
+                logger.error("deleteCloudStackVolume: " + errMsg);
+                throw new CloudRuntimeException(errMsg);
+            }
+        } catch (Exception e) {
+            logger.error("deleteCloudStackVolume: error occured " + e);
+            throw new CloudRuntimeException(e);
+        }

Review Comment:
   Same logging issue as createCloudStackVolume(): the catch block logs "error 
occured" (typo) and drops stack trace by string concatenation. Use 
logger.error(..., e).



##########
engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java:
##########
@@ -29,6 +29,11 @@ public interface DataStoreProvider {
     String SAMPLE_IMAGE = "Sample";
     String SMB = "NFS";
     String DEFAULT_PRIMARY = "DefaultPrimary";
+    /**
+     * Primary storage provider name for NetApp ONTAP ({@code 
OntapPrimaryDatastoreProvider#getName}).
+     * Single canonical value; use this instead of duplicating the string 
across modules.
+     */
+    String ONTAP_PLUGIN_NAME = "NetApp ONTAP";

Review Comment:
   This introduces a new canonical provider name value ("NetApp ONTAP"). If any 
existing installations have storage_pool.storage_provider_name persisted as the 
previous ONTAP plugin name (the ONTAP plugin previously used "ONTAP" in 
OntapStorageConstants), equality checks against 
DataStoreProvider.ONTAP_PLUGIN_NAME will stop matching and can break 
snapshot/managed-storage logic after upgrade. Consider supporting both legacy 
and new names (or adding a DB migration) to avoid upgrade regressions.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java:
##########
@@ -77,7 +72,22 @@ public void setOntapStorage(OntapStorage ontapStorage) {
 
     @Override
     public CloudStackVolume createCloudStackVolume(CloudStackVolume 
cloudstackVolume) {
-        return null;
+        logger.info("createCloudStackVolume: Create cloudstack volume " + 
cloudstackVolume);
+        try {
+            // Step 1: set cloudstack volume metadata
+            updateCloudStackVolumeMetadata(cloudstackVolume.getDatastoreId(), 
cloudstackVolume.getVolumeInfo());
+            // Step 2: Send command to KVM host to create qcow2 file using 
qemu-img
+            Answer answer = 
createVolumeOnKVMHost(cloudstackVolume.getVolumeInfo());
+            if (answer == null || !answer.getResult()) {
+                String errMsg = answer != null ? answer.getDetails() : "Failed 
to create qcow2 on KVM host";
+                logger.error("createCloudStackVolume: " + errMsg);
+                throw new CloudRuntimeException(errMsg);
+            }
+            return cloudstackVolume;
+        }catch (Exception e) {
+            logger.error("createCloudStackVolume: error occured " + e);
+            throw new CloudRuntimeException(e);
+        }

Review Comment:
   The catch block logs "error occured" (typo) and drops the stack trace by 
concatenating the exception into the message. Prefer structured logging with 
the throwable so the stack trace is preserved.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java:
##########
@@ -158,6 +159,56 @@ public boolean connectPhysicalDisk(String volumeUuid, 
KVMStoragePool pool, Map<S
         return true;
     }
 
+    /**
+     * Checks the result of an iscsiadm node-create command.
+     * Returns true if the node was created or already exists, false on 
failure.
+     */
+    boolean handleNodeCreateResult(String result, String volumeUuid) {
+        if (result == null) {
+            logger.debug("Successfully added iSCSI node for target {}", 
volumeUuid);
+            return true;
+        }
+        String msg = result.toLowerCase();
+        if (msg.contains("already exists") || msg.contains("database exists") 
|| msg.contains("exists")) {
+            logger.debug("iSCSI node already exists for target {}, 
proceeding", volumeUuid);
+            return true;
+        }

Review Comment:
   handleNodeCreateResult() treats any iscsiadm output containing "exists" as a 
success. This can incorrectly mark failures as success for messages like "does 
not exist" (contains "exist"). Restrict the check to the known benign 
node-exists strings.



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