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]
