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


##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageUtils.java:
##########
@@ -19,58 +19,137 @@
 
 package org.apache.cloudstack.storage.utils;
 
-import com.cloud.storage.ScopeType;
+import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.utils.StringUtils;
 import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.feign.model.Lun;
+import org.apache.cloudstack.storage.feign.model.LunSpace;
 import org.apache.cloudstack.storage.feign.model.OntapStorage;
+import org.apache.cloudstack.storage.feign.model.Svm;
 import org.apache.cloudstack.storage.provider.StorageProviderFactory;
 import org.apache.cloudstack.storage.service.StorageStrategy;
+import org.apache.cloudstack.storage.service.model.CloudStackVolume;
 import org.apache.cloudstack.storage.service.model.ProtocolType;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.springframework.util.Base64Utils;
-
-import java.nio.charset.StandardCharsets;
 import java.util.Map;
 
 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));
+        byte[] encodedBytes = Base64Utils.encode((username + AUTH_HEADER_COLON 
+ password).getBytes());
         return BASIC + StringUtils.SPACE + new String(encodedBytes);

Review Comment:
   `generateAuthHeader` uses `String.getBytes()` and `new String(byte[])` 
without an explicit charset. This makes auth header generation platform-default 
dependent and can break for non-ASCII credentials on systems with non-UTF-8 
defaults. Please use `StandardCharsets.UTF_8` explicitly for both conversions.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java:
##########
@@ -36,23 +39,25 @@ 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.getStrategy` unconditionally Base64-decodes 
`ontapStorage.getPassword()`. This will throw an IllegalArgumentException if 
the password is provided in plain text (e.g., via API clients or existing DB 
data), making pool creation/attachment fail. Consider either storing a clear 
contract for password encoding (and enforcing it at input), or decoding 
conditionally (try/catch with fallback to the original string).



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java:
##########
@@ -490,16 +504,19 @@ public boolean deleteDataStore(DataStore store) {
                         storagePoolId, e.getMessage(), e);
             }
             AccessGroup accessGroup = new AccessGroup();
-            accessGroup.setPrimaryDataStoreInfo(primaryDataStoreInfo);
+            accessGroup.setStoragePoolId(storagePoolId);
+            // Delete access groups associated with this storage pool
             storageStrategy.deleteAccessGroup(accessGroup);
             logger.info("deleteDataStore: Successfully deleted access groups 
for storage pool '{}'", storagePool.getName());

Review Comment:
   `deleteDataStore` calls `storageStrategy.deleteAccessGroup(accessGroup)` but 
the `AccessGroup` instance only has `storagePoolId` set; `hostsToConnect` is 
never populated. The SAN implementation 
(`UnifiedSANStrategy.deleteAccessGroup`) iterates 
`accessGroup.getHostsToConnect()` and will throw at runtime for iSCSI pools. 
Either populate `hostsToConnect` (e.g., from the cluster/zone scope or 
storage_pool_host_ref) before calling deleteAccessGroup, or make SAN 
deleteAccessGroup handle a null/empty host list safely (and/or skip SAN 
access-group deletion here if it cannot be determined).
   ```suggestion
               // Delete access groups associated with this storage pool only 
when host information is available.
               // Some SAN strategy implementations iterate hostsToConnect and 
can fail at runtime if it is null.
               if (accessGroup.getHostsToConnect() == null || 
accessGroup.getHostsToConnect().isEmpty()) {
                   logger.warn("deleteDataStore: Skipping access-group deletion 
for storage pool '{}' because hostsToConnect is not populated",
                           storagePool.getName());
               } else {
                   storageStrategy.deleteAccessGroup(accessGroup);
                   logger.info("deleteDataStore: Successfully deleted access 
groups for storage pool '{}'", storagePool.getName());
               }
   ```



##########
server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java:
##########
@@ -390,6 +390,15 @@ public VMSnapshot allocVMSnapshot(Long vmId, String 
vsDisplayName, String vsDesc
             //Other Storage volume plugins could integrate this with their own 
functionality for group snapshots
             VMSnapshotStrategy snapshotStrategy = 
storageStrategyFactory.getVmSnapshotStrategy(userVmVo.getId(), 
rootVolumePool.getId(), snapshotMemory);
             if (snapshotStrategy == null) {
+                // Check if this is ONTAP managed storage with memory snapshot 
request - provide specific error message
+                if (snapshotMemory && rootVolumePool.isManaged() &&
+                        
"ONTAP".equals(rootVolumePool.getStorageProviderName())) {
+                    String message = String.format("Memory snapshots 
(snapshotmemory=true) are not supported for VMs on ONTAP managed storage. " +
+                            "Instance [%s] uses ONTAP storage which only 
supports disk-only (crash-consistent) snapshots. " +
+                            "Please use snapshotmemory=false for disk-only 
snapshots.", userVmVo.getUuid());
+                    logger.error(message);
+                    throw new CloudRuntimeException(message);

Review Comment:
   The provider-name check uses the literal string "ONTAP", but elsewhere in 
this PR the provider is identified as "NetApp ONTAP" (UI, 
KvmFileBasedStorageVmSnapshotStrategy, OntapStorageConstants). As-is, this 
branch will never trigger and the error message for unsupported memory 
snapshots on ONTAP managed storage will be missed. Consider comparing against a 
shared constant (e.g., OntapStorageConstants.ONTAP_PLUGIN_NAME) or updating the 
literal to the actual provider name used by storage pools.



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/listener/OntapHostListener.java:
##########
@@ -87,11 +99,7 @@ public boolean hostConnect(long hostId, long poolId)  {
                         "Unable to establish a connection from agent to 
storage pool %s due to %s", pool, answer.getDetails()));
             }
 
-            if (!(answer instanceof ModifyStoragePoolAnswer)) {
-                logger.error("Received unexpected answer type {} for storage 
pool {}", answer.getClass().getName(), pool.getName());
-                throw new CloudRuntimeException("Failed to connect to storage 
pool. Please check agent logs for details.");
-            }
-
+            // Get the mount path from the answer

Review Comment:
   `hostConnect` casts `answer` to `ModifyStoragePoolAnswer` without checking 
`instanceof`. If the agent returns a different `Answer` type, this will throw 
`ClassCastException` and be caught by the broad catch, losing the real failure 
reason. Please restore an explicit type check (and return a clear error) before 
casting.
   ```suggestion
               // Get the mount path from the answer
               if (!(answer instanceof ModifyStoragePoolAnswer)) {
                   throw new CloudRuntimeException(String.format(
                           "Unexpected answer type %s returned for modify 
storage pool command for pool %s on host %d",
                           answer.getClass().getName(), pool, hostId));
               }
   ```



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java:
##########
@@ -411,18 +420,18 @@ 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() && host.getPrivateIpAddress() != null 
|| host.getPrivateIpAddress().trim().isEmpty()) {
+                            // 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;
                         }

Review Comment:
   In NFS3 host identifier validation, the condition `if (ip.isEmpty() && 
host.getPrivateIpAddress() != null || 
host.getPrivateIpAddress().trim().isEmpty())` has incorrect operator precedence 
and can throw a NullPointerException when privateIpAddress is null. It also 
allows `hostIdentifiers.add(ip)` to reuse the previous host's IP when `host` is 
null. Please restructure this block to (a) short-circuit correctly when private 
IP is null/blank, and (b) only add an IP for the current host (skip null hosts 
and reset `ip` per iteration).



##########
plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java:
##########
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cloudstack.storage.feign.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class VolumeConcise {
+    @JsonProperty("uuid")
+    private String uuid;
+    @JsonProperty("name")
+    private String name;
+    public String getUuid() {
+        return uuid;
+    }
+    public void setUuid(String uuid) {
+        this.uuid = uuid;
+    }
+    public String getName() {
+        return name;
+    }
+    public void setName(String name) {}

Review Comment:
   `VolumeConcise.setName` is a no-op, so JSON deserialization will never 
populate `name`. This can lead to null names in `FlexVolSnapshot.volume` and 
any logic relying on it. Implement the setter (or remove it and make the field 
immutable with constructor-based binding).
   ```suggestion
       public void setName(String name) {
           this.name = name;
       }
   ```



##########
plugins/storage/volume/ontap/pom.xml:
##########
@@ -121,12 +122,24 @@
             <version>${mockito.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>net.bytebuddy</groupId>
+            <artifactId>byte-buddy-agent</artifactId>
+            <version>${byte-buddy-agent.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
             <version>${assertj.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.cloudstack</groupId>
+            <artifactId>cloud-engine-storage-snapshot</artifactId>
+            <version>4.23.0.0-SNAPSHOT</version>

Review Comment:
   The dependency on `cloud-engine-storage-snapshot` is pinned to 
`4.23.0.0-SNAPSHOT` while the rest of the module uses `${project.version}`. 
This makes backports/version bumps harder and can break builds if the parent 
version changes. Prefer `${project.version}` (or rely on dependencyManagement) 
for intra-repo artifacts.
   ```suggestion
               <version>${project.version}</version>
   ```



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