rg9975 commented on code in PR #7889:
URL: https://github.com/apache/cloudstack/pull/7889#discussion_r1353027107


##########
plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java:
##########
@@ -0,0 +1,1084 @@
+// 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.datastore.adapter.flasharray;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+
+import org.apache.http.Header;
+import org.apache.http.NameValuePair;
+import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapter;
+import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapterContext;
+import 
org.apache.cloudstack.storage.datastore.adapter.ProviderAdapterDataObject;
+import 
org.apache.cloudstack.storage.datastore.adapter.ProviderAdapterDiskOffering;
+import org.apache.cloudstack.storage.datastore.adapter.ProviderSnapshot;
+import org.apache.cloudstack.storage.datastore.adapter.ProviderVolume;
+import org.apache.cloudstack.storage.datastore.adapter.ProviderVolumeNamer;
+import org.apache.cloudstack.storage.datastore.adapter.ProviderVolumeStats;
+import 
org.apache.cloudstack.storage.datastore.adapter.ProviderVolumeStorageStats;
+import 
org.apache.cloudstack.storage.datastore.adapter.ProviderVolume.AddressType;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.apache.log4j.Logger;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Array API
+ */
+public class FlashArrayAdapter implements ProviderAdapter {
+    static final Logger logger = Logger.getLogger(FlashArrayAdapter.class);
+
+    public static final String HOSTGROUP = "hostgroup";
+    public static final String STORAGE_POD = "pod";
+    public static final String KEY_TTL = "keyttl";
+    public static final String CONNECT_TIMEOUT_MS = "connectTimeoutMs";
+    public static final String POST_COPY_WAIT_MS = "postCopyWaitMs";
+    public static final String API_LOGIN_VERSION = "apiLoginVersion";
+    public static final String API_VERSION = "apiVersion";
+
+    private static final long KEY_TTL_DEFAULT = (1000 * 60 * 14);
+    private static final long CONNECT_TIMEOUT_MS_DEFAULT = 600000;
+    private static final long POST_COPY_WAIT_MS_DEFAULT = 5000;
+    private static final String API_LOGIN_VERSION_DEFAULT = "1.19";
+    private static final String API_VERSION_DEFAULT = "2.23";
+
+    static final ObjectMapper mapper = new ObjectMapper();
+    public String pod = null;
+    public String hostgroup = null;
+    private String username;
+    private String password;
+    private String accessToken;
+    private String url;
+    private long keyExpiration = -1;
+    private long keyTtl = KEY_TTL_DEFAULT;
+    private long connTimeout = CONNECT_TIMEOUT_MS_DEFAULT;
+    private long postCopyWait = POST_COPY_WAIT_MS_DEFAULT;
+    private CloseableHttpClient _client = null;
+    private boolean skipTlsValidation;
+    private String apiLoginVersion = API_LOGIN_VERSION_DEFAULT;
+    private String apiVersion = API_VERSION_DEFAULT;
+
+    private Map<String, String> connectionDetails = null;
+
+    protected FlashArrayAdapter(String url, Map<String, String> details) {
+        this.url = url;
+        this.connectionDetails = details;
+        login();
+    }
+
+    @Override
+    public ProviderVolume create(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject, ProviderAdapterDiskOffering offering, 
long size) {
+        FlashArrayVolume request = new FlashArrayVolume();
+        request.setExternalName(
+                pod + "::" + ProviderVolumeNamer.generateObjectName(context, 
dataObject));
+        request.setPodName(pod);
+        request.setAllocatedSizeBytes(roundUp512Boundary(size));
+        FlashArrayList<FlashArrayVolume> list = POST("/volumes?names=" + 
request.getExternalName() + "&overwrite=false",
+                request, new TypeReference<FlashArrayList<FlashArrayVolume>>() 
{
+                });
+
+        return (ProviderVolume) getFlashArrayItem(list);
+    }
+
+    /**
+     * Volumes must be added to a host set to be visable to the hosts.
+     * the Hostset should contain all the hosts that are membrers of the zone 
or
+     * cluster (depending on Cloudstack Storage Pool configuration)
+     */
+    @Override
+    public String attach(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject) {
+        String volumeName = normalizeName(pod, dataObject.getExternalName());
+        try {
+            FlashArrayList<FlashArrayConnection> list = 
POST("/connections?host_group_names=" + hostgroup + "&volume_names=" + 
volumeName, null, new TypeReference<FlashArrayList<FlashArrayConnection>> () { 
});
+
+            if (list == null || list.getItems() == null || 
list.getItems().size() == 0) {
+                throw new RuntimeException("Volume attach did not return lun 
information");
+            }
+
+            FlashArrayConnection connection = 
(FlashArrayConnection)this.getFlashArrayItem(list);
+            if (connection.getLun() == null) {
+                throw new RuntimeException("Volume attach missing lun field");
+            }
+
+            return ""+connection.getLun();
+
+        } catch (Throwable e) {
+            // the volume is already attached.  happens in some scenarios 
where orchestration creates the volume before copying to it
+            if (e.toString().contains("Connection already exists")) {
+                FlashArrayList<FlashArrayConnection> list = 
GET("/connections?volume_names=" + volumeName,
+                    new TypeReference<FlashArrayList<FlashArrayConnection>>() {
+                    });
+                if (list != null && list.getItems() != null) {
+                    return ""+list.getItems().get(0).getLun();
+                } else {
+                    throw new RuntimeException("Volume lun is not found in 
existing connection");
+                }
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public void detach(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject) {
+        String volumeName = normalizeName(pod, dataObject.getExternalName());
+        DELETE("/connections?host_group_names=" + hostgroup + "&volume_names=" 
+ volumeName);
+    }
+
+    @Override
+    public void delete(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject) {
+        // public void deleteVolume(String volumeNamespace, String volumeName) 
{
+        // first make sure we are disconnected
+        removeVlunsAll(context, pod, dataObject.getExternalName());
+        String fullName = normalizeName(pod, dataObject.getExternalName());
+
+        FlashArrayVolume volume = new FlashArrayVolume();
+        volume.setDestroyed(true);
+        try {
+            PATCH("/volumes?names=" + fullName, volume, new 
TypeReference<FlashArrayList<FlashArrayVolume>>() {
+            });
+        } catch (CloudRuntimeException e) {
+            if (e.toString().contains("Volume does not exist")) {
+                return;
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public ProviderVolume getVolume(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject) {
+        String externalName = dataObject.getExternalName();
+        // if its not set, look for the generated name for some edge cases
+        if (externalName == null) {
+            externalName = pod + "::" + 
ProviderVolumeNamer.generateObjectName(context, dataObject);
+        }
+        FlashArrayVolume volume = null;
+        try {
+            volume = getVolume(externalName);
+            // if we didn't get an address back its likely an empty object
+            if (volume != null && volume.getAddress() == null) {
+                return null;
+            } else if (volume == null) {
+                return null;
+            }
+
+            populateConnectionId(volume);
+
+            return volume;
+        } catch (Exception e) {
+            // assume any exception is a not found. Flash returns 400's for 
most errors
+            return null;
+        }
+    }
+
+    @Override
+    public ProviderVolume getVolumeByAddress(ProviderAdapterContext context, 
AddressType addressType, String address) {
+        // public FlashArrayVolume getVolumeByWwn(String wwn) {
+        if (address == null ||addressType == null) {
+            throw new RuntimeException("Invalid search criteria provided for 
getVolumeByAddress");
+        }
+
+        // only support WWN type addresses at this time.
+        if (!ProviderVolume.AddressType.FIBERWWN.equals(addressType)) {
+            throw new RuntimeException(
+                    "Invalid volume address type [" + addressType + "] 
requested for volume search");
+        }
+
+        // convert WWN to serial to search on. strip out WWN type # + Flash 
OUI value
+        String serial = address.substring(FlashArrayVolume.PURE_OUI.length() + 
1).toUpperCase();
+        String query = "serial='" + serial + "'";
+
+        FlashArrayVolume volume = null;
+        try {
+            FlashArrayList<FlashArrayVolume> list = GET("/volumes?filter=" + 
query,
+                new TypeReference<FlashArrayList<FlashArrayVolume>>() {
+                });
+
+            // if we didn't get an address back its likely an empty object
+            if (list == null || list.getItems() == null || 
list.getItems().size() == 0) {
+                return null;
+            }
+
+            volume = (FlashArrayVolume)this.getFlashArrayItem(list);
+            if (volume != null && volume.getAddress() == null) {
+                return null;
+            }
+
+            populateConnectionId(volume);
+
+            return volume;
+        } catch (Exception e) {
+            // assume any exception is a not found. Flash returns 400's for 
most errors
+            return null;
+        }
+    }
+
+    private void populateConnectionId(FlashArrayVolume volume) {
+        // we need to see if there is a connection (lun) associated with this 
volume.
+        // note we assume 1 lun for the hostgroup associated with this object
+        FlashArrayList<FlashArrayConnection> list = null;
+        try {
+            list = GET("/connections?volume_names=" + volume.getExternalName(),
+                    new TypeReference<FlashArrayList<FlashArrayConnection>>() {
+                    });
+        } catch (CloudRuntimeException e) {
+            // this means there is no attachment associated with this volume 
on the array
+            if (e.toString().contains("Bad Request")) {
+                return;
+            }
+        }
+
+        if (list != null && list.getItems() != null) {
+            for (FlashArrayConnection conn: list.getItems()) {
+                if (conn.getHostGroup() != null && 
conn.getHostGroup().getName().equals(this.hostgroup)) {
+                    volume.setExternalConnectionId(""+conn.getLun());
+                    break;
+                }
+            }
+
+        }
+    }
+
+    @Override
+    public void resize(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject, long newSizeInBytes) {
+        // public void resizeVolume(String volumeNamespace, String volumeName, 
long
+        // newSizeInBytes) {
+        FlashArrayVolume volume = new FlashArrayVolume();
+        volume.setAllocatedSizeBytes(roundUp512Boundary(newSizeInBytes));
+        PATCH("/volumes?names=" + dataObject.getExternalName(), volume, null);
+    }
+
+    /**
+     * Take a snapshot and return a Volume representing that snapshot
+     *
+     * @param volumeName
+     * @param snapshotName
+     * @return
+     */
+    @Override
+    public ProviderSnapshot snapshot(ProviderAdapterContext context, 
ProviderAdapterDataObject sourceDataObject, ProviderAdapterDataObject 
targetDataObject) {
+        // public FlashArrayVolume snapshotVolume(String volumeNamespace, 
String
+        // volumeName, String snapshotName) {
+        FlashArrayList<FlashArrayVolume> list = POST(
+                "/volume-snapshots?source_names=" + 
sourceDataObject.getExternalName(), null,
+                new TypeReference<FlashArrayList<FlashArrayVolume>>() {
+                });
+
+        return (FlashArrayVolume) getFlashArrayItem(list);
+    }
+
+    /**
+     * Replaces the base volume with the given snapshot. Note this can only be 
done
+     * when the snapshot and volume
+     * are
+     *
+     * @param name
+     * @return
+     */
+    @Override
+    public ProviderVolume revert(ProviderAdapterContext context, 
ProviderAdapterDataObject snapshotDataObject) {
+        // public void promoteSnapshot(String namespace, String snapshotName) {
+        if (snapshotDataObject == null || snapshotDataObject.getExternalName() 
== null) {
+            throw new RuntimeException("Snapshot revert not possible as an 
external snapshot name was not provided");
+        }
+
+        FlashArrayVolume snapshot = 
this.getSnapshot(snapshotDataObject.getExternalName());
+        if (snapshot.getSource() == null) {
+            throw new CloudRuntimeException("Snapshot source was not available 
from the storage array");
+        }
+
+        String origVolumeName = snapshot.getSource().getName();
+
+        // now "create" a new volume with the snapshot volume as its source 
(basically a
+        // Flash array copy)
+        // and overwrite to true (volume already exists, we are recreating it)
+        FlashArrayVolume input = new FlashArrayVolume();
+        input.setExternalName(origVolumeName);
+        
input.setAllocatedSizeBytes(roundUp512Boundary(snapshot.getAllocatedSizeInBytes()));
+        input.setSource(new 
FlashArrayVolumeSource(snapshot.getExternalName()));
+        POST("/volumes?names=" + origVolumeName + "&overwrite=true", input, 
null);
+
+        return this.getVolume(origVolumeName);
+    }
+
+    @Override
+    public ProviderSnapshot getSnapshot(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject) {
+        FlashArrayList<FlashArrayVolume> list = GET(
+                "/volume-snapshots?names=" + dataObject.getExternalName(),
+                new TypeReference<FlashArrayList<FlashArrayVolume>>() {
+                });
+        return (FlashArrayVolume) getFlashArrayItem(list);
+    }
+
+    @Override
+    public ProviderVolume copy(ProviderAdapterContext context, 
ProviderAdapterDataObject sourceDataObject, ProviderAdapterDataObject 
destDataObject) {
+        // private ManagedVolume copy(ManagedVolume sourceVolume, String 
destNamespace,
+        // String destName) {
+        if (sourceDataObject == null || sourceDataObject.getExternalName() == 
null
+                ||sourceDataObject.getType() == null) {
+            throw new RuntimeException("Provided volume has no external source 
information");
+        }
+
+        if (destDataObject == null) {
+            throw new RuntimeException("Provided volume target information was 
not provided");
+        }
+
+        if (destDataObject.getExternalName() == null) {
+            // this means its a new volume? so our external name will be the 
Cloudstack UUID
+            destDataObject
+                    
.setExternalName(ProviderVolumeNamer.generateObjectName(context, 
destDataObject));
+        }
+
+        FlashArrayVolume currentVol;
+        if 
(sourceDataObject.getType().equals(ProviderAdapterDataObject.Type.SNAPSHOT)) {
+            currentVol = getSnapshot(sourceDataObject.getExternalName());
+        } else {
+            currentVol = (FlashArrayVolume) this
+                    .getFlashArrayItem(GET("/volumes?names=" + 
sourceDataObject.getExternalName(),
+                            new 
TypeReference<FlashArrayList<FlashArrayVolume>>() {
+                            }));
+        }
+
+        if (currentVol == null) {
+            throw new RuntimeException("Unable to find current volume to copy 
from");
+        }
+
+        // now "create" a new volume with the snapshot volume as its source 
(basically a
+        // Flash array copy)
+        // and overwrite to true (volume already exists, we are recreating it)
+        FlashArrayVolume payload = new FlashArrayVolume();
+        payload.setExternalName(normalizeName(pod, 
destDataObject.getExternalName()));
+        payload.setPodName(pod);
+        
payload.setAllocatedSizeBytes(roundUp512Boundary(currentVol.getAllocatedSizeInBytes()));
+        payload.setSource(new 
FlashArrayVolumeSource(sourceDataObject.getExternalName()));
+        FlashArrayList<FlashArrayVolume> list = POST(
+                "/volumes?names=" + payload.getExternalName() + 
"&overwrite=true", payload,
+                new TypeReference<FlashArrayList<FlashArrayVolume>>() {
+                });
+        FlashArrayVolume outVolume = (FlashArrayVolume) 
getFlashArrayItem(list);
+        pause(postCopyWait);
+        return outVolume;
+    }
+
+    private void pause(long period) {
+        try {
+            Thread.sleep(period);
+        } catch (InterruptedException e) {
+
+        }
+    }
+
+    public boolean supportsSnapshotConnection() {
+        return false;
+    }
+
+    @Override
+    public void refresh(Map<String, String> details) {
+        this.connectionDetails = details;
+        this.refreshSession(true);
+    }
+
+    @Override
+    public void validate() {
+        login();
+        // check if hostgroup and pod from details really exist - we will
+        // require a distinct configuration object/connection object for each 
type
+        if (this.getHostgroup(hostgroup) == null) {
+            throw new RuntimeException("Hostgroup [" + hostgroup + "] not 
found in FlashArray at [" + url
+                    + "], please validate configuration");
+        }
+
+        if (this.getVolumeNamespace(pod) == null) {
+            throw new RuntimeException(
+                    "Pod [" + pod + "] not found in FlashArray at [" + url + 
"], please validate configuration");
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        return;
+    }
+
+    @Override
+    public ProviderVolumeStorageStats getManagedStorageStats() {
+        FlashArrayPod pod = getVolumeNamespace(this.pod);
+        // just in case
+        if (pod == null || pod.getFootprint() == 0) {
+            return null;
+        }
+        Long capacityBytes = pod.getQuotaLimit();
+        Long usedBytes = pod.getQuotaLimit() - (pod.getQuotaLimit() - 
pod.getFootprint());
+        ProviderVolumeStorageStats stats = new ProviderVolumeStorageStats();
+        stats.setCapacityInBytes(capacityBytes);
+        stats.setActualUsedInBytes(usedBytes);
+        return stats;
+    }
+
+    @Override
+    public ProviderVolumeStats getVolumeStats(ProviderAdapterContext context, 
ProviderAdapterDataObject dataObject) {
+        ProviderVolume vol = getVolume(dataObject.getExternalName());
+        Long usedBytes = vol.getUsedBytes();
+        Long allocatedSizeInBytes = vol.getAllocatedSizeInBytes();
+        if (usedBytes == null || allocatedSizeInBytes == null) {
+            return null;
+        }
+        ProviderVolumeStats stats = new ProviderVolumeStats();
+        stats.setAllocatedInBytes(allocatedSizeInBytes);
+        stats.setActualUsedInBytes(usedBytes);
+        return stats;
+    }
+
+    @Override
+    public boolean canAccessHost(ProviderAdapterContext context, String 
hostname) {
+        if (hostname == null) {
+            throw new RuntimeException("Unable to validate host access because 
a hostname was not provided");
+        }
+
+        List<String> members = getHostgroupMembers(hostgroup);
+
+        // check for fqdn and shortname combinations.  this assumes there is 
at least a shortname match in both the storage array and cloudstack
+        // hostname configuration
+        String shortname;
+        if (hostname.indexOf('.') > 0) {
+            shortname = hostname.substring(0, (hostname.indexOf('.')));
+        } else {
+            shortname = hostname;
+        }
+
+        for (String member : members) {
+            // exact match (short or long names)
+            if (member.equals(hostname)) {
+                return true;
+            }
+
+            // primera has short name and cloudstack had long name
+            if (member.equals(shortname)) {
+                return true;
+            }
+
+            // member has long name but cloudstack had shortname
+            if (member.indexOf('.') > 0) {
+                if (member.substring(0, 
(member.indexOf('.'))).equals(shortname)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private String getAccessToken() {
+        refreshSession(false);
+        return accessToken;
+    }
+
+    private synchronized void refreshSession(boolean force) {
+        try {
+            if (force || keyExpiration < System.currentTimeMillis()) {
+                // close client to force connection reset on appliance -- not 
doing this can
+                // result in NotAuthorized error...guessing
+                _client.close();
+                ;
+                _client = null;
+                login();
+                keyExpiration = System.currentTimeMillis() + keyTtl;
+            }
+        } catch (Exception e) {
+            // retry frequently but not every request to avoid DDOS on storage 
API
+            logger.warn("Failed to refresh FlashArray API key for " + username 
+ "@" + url + ", will retry in 5 seconds",
+                    e);
+            keyExpiration = System.currentTimeMillis() + (5 * 1000);
+        }
+    }
+
+    /**
+     * Login to the array and get an access token
+     */
+    private void login() {

Review Comment:
   Updated in upcoming commit.



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