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


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

Review Comment:
   StorageProviderFactory always Base64-decodes the password. If the password 
is already plain-text (e.g., API/user input not base64, or older DB entries), 
Base64.getDecoder().decode(...) will throw IllegalArgumentException and prevent 
the ONTAP strategy from being created.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java:
##########
@@ -207,67 +214,64 @@ 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");
         }
+
+        PrimaryDataStoreParameters parameters = new 
PrimaryDataStoreParameters();
+        if (clusterId != null) {
+            ClusterVO clusterVO = _clusterDao.findById(clusterId);
+            Preconditions.checkNotNull(clusterVO, "Unable to locate the 
specified cluster");
+            if (clusterVO.getHypervisorType() != 
Hypervisor.HypervisorType.KVM) {
+                throw new CloudRuntimeException("ONTAP primary storage is 
supported only for KVM hypervisor");
+            }
+            parameters.setHypervisorType(clusterVO.getHypervisorType());
+        }

Review Comment:
   validateInitializeInputs() creates a PrimaryDataStoreParameters instance 
only to set the hypervisor type, but that local variable is never used (the 
method just returns capacityBytes). This is dead code and can be removed while 
keeping the KVM validation.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java:
##########
@@ -411,23 +396,49 @@ private boolean 
validateProtocolSupportAndFetchHostsIdentifier(List<HostVO> host
                 for (HostVO host : hosts) {
                     if (host != null) {
                         ip =  host.getStorageIpAddress() != null ? 
host.getStorageIpAddress().trim() : "";
-                        if (ip.isEmpty()) {
-                            if (host.getPrivateIpAddress() == null || 
host.getPrivateIpAddress().trim().isEmpty()) {
-                                return false;
-                            }
-                            ip = host.getPrivateIpAddress().trim();
+                        if (ip.isEmpty() && 
StringUtils.isBlank(host.getPrivateIpAddress() )) {
+                            // TODO we will inform customer through alert for 
excluded host because of protocol enabled on host
+                            continue;
+                        } else {
+                            ip = ip.isEmpty() ? 
host.getPrivateIpAddress().trim() : ip;
                         }
                     }
                     hostIdentifiers.add(ip);
                 }
                 break;
             default:
-                throw new 
CloudRuntimeException("validateProtocolSupportAndFetchHostsIdentifier : 
Unsupported protocol: " + protocolType.name());
+                throw new CloudRuntimeException("Unsupported protocol: " + 
protocolType.name());
         }
         logger.info("validateProtocolSupportAndFetchHostsIdentifier: All hosts 
support the protocol: " + protocolType.name());
         return true;
     }
 
+    /**
+     * Creates an NFS export policy (access group) on the ONTAP storage if the 
protocol is NFS3
+     * and there are eligible hosts. Skipped for iSCSI (igroups are created 
per-host in grantAccess).
+     */
+    private void createNfsAccessGroupIfNeeded(Map<String, String> details, 
List<String> hostsIdentifier,
+                                              List<HostVO> hostsToConnect, 
Scope scope,
+                                              StoragePoolVO storagePool, 
StorageStrategy strategy) {
+        if 
(!ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL)))
 {
+            return;
+        }
+        if (hostsIdentifier.isEmpty()) {
+            // No eligible hosts — export policy will be created later via 
HostListener when hosts come up
+            return;
+        }
+        try {
+            AccessGroup accessGroupRequest = new AccessGroup();
+            accessGroupRequest.setHostsToConnect(hostsToConnect);
+            accessGroupRequest.setScope(scope);
+            accessGroupRequest.setStoragePoolId(storagePool.getId());
+            strategy.createAccessGroup(accessGroupRequest);
+        } catch (Exception e) {
+            logger.error("Failed to create NFS access group on storage for 
pool {}: {}", storagePool.getName(), e.getMessage());
+            throw new CloudRuntimeException("Failed to create NFS access group 
on storage for pool " + storagePool.getName() + ": " + e.getMessage());
+        }

Review Comment:
   createNfsAccessGroupIfNeeded() passes the full hostsToConnect list into the 
AccessGroup, even though validateProtocolSupportAndFetchHostsIdentifier() can 
skip hosts (e.g., missing storage/private IP). That can lead to export-policy 
rules being generated with null/blank client matches (e.g., "null/32").



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java:
##########
@@ -411,23 +396,49 @@ private boolean 
validateProtocolSupportAndFetchHostsIdentifier(List<HostVO> host
                 for (HostVO host : hosts) {
                     if (host != null) {
                         ip =  host.getStorageIpAddress() != null ? 
host.getStorageIpAddress().trim() : "";
-                        if (ip.isEmpty()) {
-                            if (host.getPrivateIpAddress() == null || 
host.getPrivateIpAddress().trim().isEmpty()) {
-                                return false;
-                            }
-                            ip = host.getPrivateIpAddress().trim();
+                        if (ip.isEmpty() && 
StringUtils.isBlank(host.getPrivateIpAddress() )) {
+                            // TODO we will inform customer through alert for 
excluded host because of protocol enabled on host
+                            continue;
+                        } else {
+                            ip = ip.isEmpty() ? 
host.getPrivateIpAddress().trim() : ip;
                         }
                     }
                     hostIdentifiers.add(ip);
                 }

Review Comment:
   validateProtocolSupportAndFetchHostsIdentifier() (NFS3 case) adds an IP to 
hostIdentifiers even when host is null, and it can also append the previous 
host's IP when a null host is encountered because `ip` is defined outside the 
loop. This can result in incorrect/duplicate host identifiers and later invalid 
export-policy rules.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java:
##########
@@ -96,24 +119,40 @@ public void copyCloudStackVolume(CloudStackVolume 
cloudstackVolume) {
 
     @Override
     public CloudStackVolume getCloudStackVolume(Map<String, String> 
cloudStackVolumeMap) {
-        return null;
+        logger.info("getCloudStackVolume: Get cloudstack volume " + 
cloudStackVolumeMap);
+        CloudStackVolume cloudStackVolume = null;
+        FileInfo fileInfo = 
getFile(cloudStackVolumeMap.get(OntapStorageConstants.VOLUME_UUID),cloudStackVolumeMap.get(OntapStorageConstants.FILE_PATH));
+
+        if (fileInfo != null) {
+            cloudStackVolume = new CloudStackVolume();
+            
cloudStackVolume.setFlexVolumeUuid(cloudStackVolumeMap.get(OntapStorageConstants.VOLUME_UUID));
+            cloudStackVolume.setFile(fileInfo);
+        } else {
+            logger.warn("getCloudStackVolume: File not found for volume UUID: 
{} and file path: {}", 
cloudStackVolumeMap.get(OntapStorageConstants.VOLUME_UUID), 
cloudStackVolumeMap.get(OntapStorageConstants.FILE_PATH));
+        }
+

Review Comment:
   getCloudStackVolume() checks `if (fileInfo != null)` and logs a "File not 
found" warning in the else branch, but getFile() always throws when the file is 
missing (it never returns null). This makes the else branch unreachable and the 
null-check redundant.



##########
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
+        params.managed = true
+        params.url = this.ontapURL(this.prefillContent.ontapIP)
+        if (this.prefillContent.capacityBytes && 
this.prefillContent.capacityBytes.length > 0) {
+          params.capacityBytes = 
this.prefillContent.capacityBytes.split(',').join('')
+        }
+      }

Review Comment:
   ONTAP password is Base64-encoded with btoa(). btoa only supports Latin1 and 
can throw for non-ASCII passwords; additionally Base64 output may need URI-safe 
encoding. Consider using the existing $toBase64AndURIEncoded() helper (used 
elsewhere in the UI utils) for consistent UTF-8-safe encoding.



##########
plugins/storage/volume/ontap/pom.xml:
##########
@@ -151,6 +164,7 @@
                 <version>${maven-surefire-plugin.version}</version>
                 <configuration>
                     <skipTests>false</skipTests>
+                    
<argLine>-javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/${byte-buddy-agent.version}/byte-buddy-agent-${byte-buddy-agent.version}.jar</argLine>
                     <includes>

Review Comment:
   The surefire <argLine> hard-codes the Byte Buddy agent path using 
${settings.localRepository}. That property is often unset, which can produce an 
invalid -javaagent path and fail test JVM startup. Also, this overwrites any 
existing argLine (e.g., from parent configs).



##########
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 Base64-encoded with btoa(). btoa only supports Latin1 and 
can throw for non-ASCII passwords, and raw Base64 may include '+', '/', '=' 
which can be problematic in query/form encoding. The UI already provides 
$toBase64AndURIEncoded() to handle UTF-8 + URI-safe encoding.



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