content: // 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.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.HashMap;
  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 com.cloud.utils.exception.CloudRuntimeException;
  import com.fasterxml.jackson.core.JsonProcessingException;
  import com.fasterxml.jackson.core.type.TypeReference;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  
  /**
   * Array API
   */
  public class FlashArrayAdapter implements ProviderAdapter {
      protected Logger logger = LogManager.getLogger(getClass());
  
      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 hostname) {
  
          // should not happen but double check for sanity
          if (dataObject.getType() == ProviderAdapterDataObject.Type.SNAPSHOT) {
              throw new RuntimeException("This storage provider does not support direct attachments of snapshots to hosts");
          }
  
          String volumeName = normalizeName(pod, dataObject.getExternalName());
          try {
              FlashArrayList<FlashArrayConnection> list = null;
              FlashArrayHost host = getHost(hostname);
              if (host != null) {
                  list = POST("/connections?host_names=" + host.getName() + "&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) {
                      for (FlashArrayConnection conn : list.getItems()) {
                          if (conn.getHost() != null && conn.getHost().getName() != null &&
                              (conn.getHost().getName().equals(hostname) || conn.getHost().getName().equals(hostname.substring(0, hostname.indexOf('.')))) &&
                              conn.getLun() != null) {
                              return "" + conn.getLun();
                          }
                      }
                      throw new RuntimeException("Volume lun is not found in existing connection");
                  } else {
                      throw new RuntimeException("Volume lun is not found in existing connection");
                  }
              } else {
                  throw e;
              }
          }
      }
  
      @Override
      public void detach(ProviderAdapterContext context, ProviderAdapterDataObject dataObject, String hostname) {
          String volumeName = normalizeName(pod, dataObject.getExternalName());
          // hostname is always provided by cloudstack, but we will detach from hostgroup
          // if this pool is configured to use hostgroup for attachments
          if (hostgroup != null) {
              DELETE("/connections?host_group_names=" + hostgroup + "&volume_names=" + volumeName);
          }
  
          FlashArrayHost host = getHost(hostname);
          if (host != null) {
              DELETE("/connections?host_names=" + host.getName() + "&volume_names=" + volumeName);
          }
      }
  
      @Override
      public void delete(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
          // first make sure we are disconnected
          removeVlunsAll(context, pod, dataObject.getExternalName());
          String fullName = normalizeName(pod, dataObject.getExternalName());
  
          FlashArrayVolume volume = new FlashArrayVolume();
  
          // rename as we delete so it doesn't conflict if the template or volume is ever recreated
          // pure keeps the volume(s) around in a Destroyed bucket for a period of time post delete
          String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date());
          volume.setExternalName(fullName + "-" + timestamp);
  
          try {
              PATCH("/volumes?names=" + fullName, volume, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
              });
  
              // now delete it with new name
              volume.setDestroyed(true);
  
              PATCH("/volumes?names=" + fullName + "-" + timestamp, 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;
              }
  
              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;
              }
  
              return volume;
          } catch (Exception e) {
              // assume any exception is a not found. Flash returns 400's for most errors
              return null;
          }
      }
  
      @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, Long newSize) {
          // Add new parameter as newSize to match method declaration but not used anywhere
          // 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();
  
          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");
          }
  
          FlashArrayHost host = getHost(hostname);
          if (host != null) {
              return true;
          }
  
          return false;
      }
  
      private FlashArrayHost getHost(String hostname) {
          FlashArrayList<FlashArrayHost> list = null;
  
          try {
              list = GET("/hosts?names=" + hostname,
                  new TypeReference<FlashArrayList<FlashArrayHost>>() {
                  });
          } catch (Exception e) {
  
          }
  
          if (list == null) {
              if (hostname.indexOf('.') > 0) {
                  list = GET("/hosts?names=" + hostname.substring(0, (hostname.indexOf('.'))),
                      new TypeReference<FlashArrayList<FlashArrayHost>>() {
                  });
              }
          }
          return (FlashArrayHost) getFlashArrayItem(list);
      }
  
      private String getAccessToken() {
          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() {
          username = connectionDetails.get(ProviderAdapter.API_USERNAME_KEY);
          password = connectionDetails.get(ProviderAdapter.API_PASSWORD_KEY);
          String urlStr = connectionDetails.get(ProviderAdapter.API_URL_KEY);
  
          URL urlFull;
          try {
              urlFull = new URL(urlStr);
          } catch (MalformedURLException e) {
              throw new RuntimeException("Invalid URL format: " + urlStr, e);
          }
          ;
  
          int port = urlFull.getPort();
          if (port <= 0) {
              port = 443;
          }
          this.url = urlFull.getProtocol() + "://" + urlFull.getHost() + ":" + port + urlFull.getPath();
  
          Map<String, String> queryParms = new HashMap<String, String>();
          if (urlFull.getQuery() != null) {
              String[] queryToks = urlFull.getQuery().split("&");
              for (String tok : queryToks) {
                  if (tok.endsWith("=")) {
                      continue;
                  }
                  int i = tok.indexOf("=");
                  if (i > 0) {
                      queryParms.put(tok.substring(0, i), tok.substring(i + 1));
                  }
              }
          }
  
          pod = connectionDetails.get(FlashArrayAdapter.STORAGE_POD);
          if (pod == null) {
              pod = queryParms.get(FlashArrayAdapter.STORAGE_POD);
              if (pod == null) {
                  throw new RuntimeException(
                          FlashArrayAdapter.STORAGE_POD + " paramater/option required to configure this storage pool");
              }
          }
  
          apiLoginVersion = connectionDetails.get(FlashArrayAdapter.API_LOGIN_VERSION);
          if (apiLoginVersion == null) {
              apiLoginVersion = queryParms.get(FlashArrayAdapter.API_LOGIN_VERSION);
              if (apiLoginVersion == null) {
                  apiLoginVersion = API_LOGIN_VERSION_DEFAULT;
              }
          }
  
          apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
          if (apiVersion == null) {
              apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
              if (apiVersion == null) {
                  apiVersion = API_VERSION_DEFAULT;
              }
          }
  
          // retrieve for legacy purposes.  if set, we'll remove any connections to hostgroup we find and use the host
          hostgroup = connectionDetails.get(FlashArrayAdapter.HOSTGROUP);
          if (hostgroup == null) {
              hostgroup = queryParms.get(FlashArrayAdapter.HOSTGROUP);
          }
  
          String connTimeoutStr = connectionDetails.get(FlashArrayAdapter.CONNECT_TIMEOUT_MS);
          if (connTimeoutStr == null) {
              connTimeoutStr = queryParms.get(FlashArrayAdapter.CONNECT_TIMEOUT_MS);
          }
          if (connTimeoutStr == null) {
              connTimeout = CONNECT_TIMEOUT_MS_DEFAULT;
          } else {
              try {
                  connTimeout = Integer.parseInt(connTimeoutStr);
              } catch (NumberFormatException e) {
                  logger.warn("Connection timeout not formatted correctly, using default", e);
                  connTimeout = CONNECT_TIMEOUT_MS_DEFAULT;
              }
          }
  
          String keyTtlString = connectionDetails.get(FlashArrayAdapter.KEY_TTL);
          if (keyTtlString == null) {
              keyTtlString = queryParms.get(FlashArrayAdapter.KEY_TTL);
          }
          if (keyTtlString == null) {
              keyTtl = KEY_TTL_DEFAULT;
          } else {
              try {
                  keyTtl = Integer.parseInt(keyTtlString);
              } catch (NumberFormatException e) {
                  logger.warn("Key TTL not formatted correctly, using default", e);
                  keyTtl = KEY_TTL_DEFAULT;
              }
          }
  
          String copyWaitStr = connectionDetails.get(FlashArrayAdapter.POST_COPY_WAIT_MS);
          if (copyWaitStr == null) {
              copyWaitStr = queryParms.get(FlashArrayAdapter.POST_COPY_WAIT_MS);
          }
          if (copyWaitStr == null) {
              postCopyWait = POST_COPY_WAIT_MS_DEFAULT;
          } else {
              try {
                  postCopyWait = Integer.parseInt(copyWaitStr);
              } catch (NumberFormatException e) {
                  logger.warn("Key TTL not formatted correctly, using default", e);
                  postCopyWait = KEY_TTL_DEFAULT;
              }
          }
  
          String skipTlsValidationStr = connectionDetails.get(ProviderAdapter.API_SKIP_TLS_VALIDATION_KEY);
          if (skipTlsValidationStr == null) {
              skipTlsValidationStr = queryParms.get(ProviderAdapter.API_SKIP_TLS_VALIDATION_KEY);
          }
  
          if (skipTlsValidationStr != null) {
              skipTlsValidation = Boolean.parseBoolean(skipTlsValidationStr);
          } else {
              skipTlsValidation = true;
          }
  
          CloseableHttpResponse response = null;
          try {
              HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
              // request.addHeader("Content-Type", "application/json");
              // request.addHeader("Accept", "application/json");
              ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
              postParms.add(new BasicNameValuePair("username", username));
              postParms.add(new BasicNameValuePair("password", password));
              request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
              CloseableHttpClient client = getClient();
              response = (CloseableHttpResponse) client.execute(request);
  
              int statusCode = response.getStatusLine().getStatusCode();
              FlashArrayApiToken apitoken = null;
              if (statusCode == 200 | statusCode == 201) {
                  apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class);
                  if (apitoken == null) {
                      throw new CloudRuntimeException(
                              "Authentication responded successfully but no api token was returned");
                  }
              } else if (statusCode == 401 || statusCode == 403) {
                  throw new CloudRuntimeException(
                          "Authentication or Authorization to FlashArray [" + url + "] with user [" + username
                                  + "] failed, unable to retrieve session token");
              } else {
                  throw new CloudRuntimeException(
                          "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
                                  + "] - " + response.getStatusLine().getReasonPhrase());
              }
  
              // now we need to get the access token
              request = new HttpPost(url + "/" + apiVersion + "/login");
              request.addHeader("api-token", apitoken.getApiToken());
              response = (CloseableHttpResponse) client.execute(request);
  
              statusCode = response.getStatusLine().getStatusCode();
              if (statusCode == 200 | statusCode == 201) {
                  Header[] headers = response.getHeaders("x-auth-token");
                  if (headers == null || headers.length == 0) {
                      throw new CloudRuntimeException(
                              "Getting access token responded successfully but access token was not available");
                  }
                  accessToken = headers[0].getValue();
              } else if (statusCode == 401 || statusCode == 403) {
                  throw new CloudRuntimeException(
                          "Authentication or Authorization to FlashArray [" + url + "] with user [" + username
                                  + "] failed, unable to retrieve session token");
              } else {
                  throw new CloudRuntimeException(
                          "Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
                                  + "] - " + response.getStatusLine().getReasonPhrase());
              }
  
          } catch (UnsupportedEncodingException e) {
              throw new CloudRuntimeException("Error creating input for login, check username/password encoding");
          } catch (UnsupportedOperationException e) {
              throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e);
          } catch (IOException e) {
              throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e);
          } finally {
              try {
                  if (response != null) {
                      response.close();
                  }
              } catch (IOException e) {
                  logger.debug("Error closing response from login attempt to FlashArray", e);
              }
          }
      }
  
      private void removeVlunsAll(ProviderAdapterContext context, String volumeNamespace, String volumeName) {
          volumeName = normalizeName(volumeNamespace, volumeName);
          FlashArrayList<FlashArrayConnection> list = null;
  
          try {
              list = GET("/connections?volume_names=" + volumeName,
                      new TypeReference<FlashArrayList<FlashArrayConnection>>() {
                      });
          } catch (CloudRuntimeException e) {
              // this means the volume being deleted no longer exists so no connections can be
              // searched
              if (e.toString().contains("Bad Request")) {
                  return;
              }
          }
  
          if (list != null && list.getItems() != null) {
              for (FlashArrayConnection conn : list.getItems()) {
                  if (hostgroup != null && conn.getHostGroup() != null && conn.getHostGroup().getName() != null) {
                      DELETE("/connections?host_group_names=" + conn.getHostGroup().getName() + "&volume_names="
                              + volumeName);
                      break;
                  } else if (conn.getHost() != null && conn.getHost().getName() != null) {
                      DELETE("/connections?host_names=" + conn.getHost().getName() + "&volume_names=" + volumeName);
                  }
              }
          }
      }
  
      private FlashArrayVolume getVolume(String volumeName) {
          FlashArrayList<FlashArrayVolume> list = GET("/volumes?names=" + volumeName,
                  new TypeReference<FlashArrayList<FlashArrayVolume>>() {
                  });
          return (FlashArrayVolume) getFlashArrayItem(list);
      }
  
      private FlashArrayPod getVolumeNamespace(String name) {
          FlashArrayList<FlashArrayPod> list = GET("/pods?names=" + name,
                  new TypeReference<FlashArrayList<FlashArrayPod>>() {
                  });
          return (FlashArrayPod) getFlashArrayItem(list);
      }
  
      private FlashArrayVolume getSnapshot(String snapshotName) {
          FlashArrayList<FlashArrayVolume> list = GET("/volume-snapshots?names=" + snapshotName,
                  new TypeReference<FlashArrayList<FlashArrayVolume>>() {
                  });
          return (FlashArrayVolume) getFlashArrayItem(list);
      }
  
      private Object getFlashArrayItem(FlashArrayList<?> list) {
          if (list.getItems() != null && list.getItems().size() > 0) {
              return list.getItems().get(0);
          } else {
              return null;
          }
      }
  
      private String normalizeName(String volumeNamespace, String volumeName) {
          if (!volumeName.contains("::")) {
              if (volumeNamespace != null) {
                  volumeName = volumeNamespace + "::" + volumeName;
              }
          }
          return volumeName;
      }
  
      @SuppressWarnings("unchecked")
      private <T> T POST(String path, Object input, final TypeReference<T> type) {
          CloseableHttpResponse response = null;
          try {
              this.refreshSession(false);
              HttpPost request = new HttpPost(url + "/" + apiVersion + path);
              request.addHeader("Content-Type", "application/json");
              request.addHeader("Accept", "application/json");
              request.addHeader("X-auth-token", getAccessToken());
              if (input != null) {
                  try {
                      String data = mapper.writeValueAsString(input);
                      request.setEntity(new StringEntity(data));
                  } catch (UnsupportedEncodingException | JsonProcessingException e) {
                      throw new CloudRuntimeException(
                              "Error processing request payload to [" + url + "] for path [" + path + "]", e);
                  }
              }
  
              CloseableHttpClient client = getClient();
              try {
                  response = (CloseableHttpResponse) client
                          .execute(request);
              } catch (IOException e) {
                  throw new CloudRuntimeException("Error sending request to FlashArray [" + url + path + "]", e);
              }
  
              final int statusCode = response.getStatusLine().getStatusCode();
              if (statusCode == 200 || statusCode == 201) {
                  try {
                      if (type != null) {
                          Header header = response.getFirstHeader("Location");
                          if (type.getType().getTypeName().equals(String.class.getName())) {
                              if (header != null) {
                                  return (T) header.getValue();
                              } else {
                                  return null;
                              }
                          } else {
                              return mapper.readValue(response.getEntity().getContent(), type);
                          }
                      }
                      return null;
                  } catch (UnsupportedOperationException | IOException e) {
                      throw new CloudRuntimeException("Error processing response from FlashArray [" + url + path + "]",
                              e);
                  }
              } else if (statusCode == 400) {
                  try {
                      Map<String, Object> payload = mapper.readValue(response.getEntity().getContent(),
                              new TypeReference<Map<String, Object>>() {
                              });
                      throw new CloudRuntimeException("Invalid request error 400: " + payload);
                  } catch (UnsupportedOperationException | IOException e) {
                      throw new CloudRuntimeException(
                              "Error processing bad request response from FlashArray [" + url + path + "]", e);
                  }
              } else if (statusCode == 401 || statusCode == 403) {
                  throw new CloudRuntimeException(
                          "Authentication or Authorization to FlashArray [" + url + "] with user [" + username
                                  + "] failed, unable to retrieve session token");
              } else {
                  try {
                      Map<String, Object> payload = mapper.readValue(response.getEntity().getContent(),
                              new TypeReference<Map<String, Object>>() {
                              });
                      throw new CloudRuntimeException("Invalid request error " + statusCode + ": " + payload);
                  } catch (UnsupportedOperationException | IOException e) {
                      throw new CloudRuntimeException(
                              "Unexpected HTTP response code from FlashArray on POST [" + url + path + "] - ["
                                      + statusCode + "] - " + response.getStatusLine().getReasonPhrase());
                  }
              }
          } finally {
              if (response != null) {
                  try {
                      response.close();
                  } catch (IOException e) {
                      logger.debug("Unexpected failure closing response to FlashArray API", e);
                  }
              }
          }
      }
  
      private <T> T PATCH(String path, Object input, final TypeReference<T> type) {
          CloseableHttpResponse response = null;
          try {
              this.refreshSession(false);
              HttpPatch request = new HttpPatch(url + "/" + apiVersion + path);
              request.addHeader("Content-Type", "application/json");
              request.addHeader("Accept", "application/json");
              request.addHeader("X-auth-token", getAccessToken());
              String data = mapper.writeValueAsString(input);
              request.setEntity(new StringEntity(data));
  
              CloseableHttpClient client = getClient();
              response = (CloseableHttpResponse) client.execute(request);
  
              final int statusCode = response.getStatusLine().getStatusCode();
              if (statusCode == 200 || statusCode == 201) {
                  if (type != null)
                      return mapper.readValue(response.getEntity().getContent(), type);
                  return null;
              } else if (statusCode == 400) {
                  Map<String, Object> payload = mapper.readValue(response.getEntity().getContent(),
                          new TypeReference<Map<String, Object>>() {
                          });
                  throw new CloudRuntimeException("Invalid request error 400: " + payload);
              } else if (statusCode == 401 || statusCode == 403) {
                  throw new CloudRuntimeException(
                          "Authentication or Authorization to FlashArray [" + url + "] with user [" + username
                                  + "] failed, unable to retrieve session token");
              } else {
                  Map<String, Object> payload = mapper.readValue(response.getEntity().getContent(),
                          new TypeReference<Map<String, Object>>() {
                          });
                  throw new CloudRuntimeException(
                          "Invalid request error from FlashArray on PUT [" + url + path + "]" + statusCode + ": "
                                  + response.getStatusLine().getReasonPhrase() + " - " + payload);
              }
          } catch (UnsupportedEncodingException | JsonProcessingException e) {
              throw new CloudRuntimeException(
                      "Error processing request payload to [" + url + "] for path [" + path + "]", e);
          } catch (UnsupportedOperationException e) {
              throw new CloudRuntimeException("Error processing bad request response from FlashArray [" + url + "]",
                      e);
          } catch (IOException e) {
              throw new CloudRuntimeException("Error sending request to FlashArray [" + url + "]", e);
  
          } finally {
              if (response != null) {
                  try {
                      response.close();
                  } catch (IOException e) {
                      logger.debug("Unexpected failure closing response to FlashArray API", e);
                  }
              }
          }
  
      }
  
      private <T> T GET(String path, final TypeReference<T> type) {
          CloseableHttpResponse response = null;
          try {
              this.refreshSession(false);
              HttpGet request = new HttpGet(url + "/" + apiVersion + path);
              request.addHeader("Content-Type", "application/json");
              request.addHeader("Accept", "application/json");
              request.addHeader("X-auth-token", getAccessToken());
  
              CloseableHttpClient client = getClient();
              response = (CloseableHttpResponse) client.execute(request);
              final int statusCode = response.getStatusLine().getStatusCode();
              if (statusCode == 200) {
                  try {
                      return mapper.readValue(response.getEntity().getContent(), type);
                  } catch (UnsupportedOperationException | IOException e) {
                      throw new CloudRuntimeException("Error processing response from FlashArray [" + url + "]", e);
                  }
              } else if (statusCode == 401 || statusCode == 403) {
                  throw new CloudRuntimeException(
                          "Authentication or Authorization to FlashArray [" + url + "] with user [" + username
                                  + "] failed, unable to retrieve session token");
              } else {
                  throw new CloudRuntimeException(
                          "Unexpected HTTP response code from FlashArray on GET [" + request.getURI() + "] - ["
                                  + statusCode + "] - " + response.getStatusLine().getReasonPhrase());
              }
          } catch (IOException e) {
              throw new CloudRuntimeException("Error sending request to FlashArray [" + url + "]", e);
          } catch (UnsupportedOperationException e) {
              throw new CloudRuntimeException("Error processing response from FlashArray [" + url + "]", e);
          } finally {
              if (response != null) {
                  try {
                      response.close();
                  } catch (IOException e) {
                      logger.debug("Unexpected failure closing response to FlashArray API", e);
                  }
              }
          }
      }
  
      private void DELETE(String path) {
          CloseableHttpResponse response = null;
          try {
              this.refreshSession(false);
              HttpDelete request = new HttpDelete(url + "/" + apiVersion + path);
              request.addHeader("Content-Type", "application/json");
              request.addHeader("Accept", "application/json");
              request.addHeader("X-auth-token", getAccessToken());
  
              CloseableHttpClient client = getClient();
              response = (CloseableHttpResponse) client.execute(request);
              final int statusCode = response.getStatusLine().getStatusCode();
              if (statusCode == 200 || statusCode == 404 || statusCode == 400) {
                  // this means the volume was deleted successfully, or doesn't exist (effective
                  // delete), or
                  // the volume name is malformed or too long - meaning it never got created to
                  // begin with (effective delete)
                  return;
              } else if (statusCode == 401 || statusCode == 403) {
                  throw new CloudRuntimeException(
                          "Authentication or Authorization to FlashArray [" + url + "] with user [" + username
                                  + "] failed, unable to retrieve session token");
              } else if (statusCode == 409) {
                  throw new CloudRuntimeException(
                          "The volume cannot be deleted at this time due to existing dependencies.  Validate that all snapshots associated with this volume have been deleted and try again.");
              } else {
                  throw new CloudRuntimeException(
                          "Unexpected HTTP response code from FlashArray on DELETE [" + url + path + "] - ["
                                  + statusCode + "] - " + response.getStatusLine().getReasonPhrase());
              }
          } catch (IOException e) {
              throw new CloudRuntimeException("Error sending request to FlashArray [" + url + "]", e);
          } finally {
              if (response != null) {
                  try {
                      response.close();
                  } catch (IOException e) {
                      logger.debug("Unexpected failure closing response to FlashArray API", e);
                  }
              }
          }
      }
  
      private CloseableHttpClient getClient() {
          if (_client == null) {
              RequestConfig config = RequestConfig.custom()
                      .setConnectTimeout((int) connTimeout)
                      .setConnectionRequestTimeout((int) connTimeout)
                      .setSocketTimeout((int) connTimeout).build();
  
              HostnameVerifier verifier = null;
              SSLContext sslContext = null;
  
              if (this.skipTlsValidation) {
                  try {
                      verifier = NoopHostnameVerifier.INSTANCE;
                      sslContext = new SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build();
                  } catch (KeyManagementException e) {
                      throw new CloudRuntimeException(e);
                  } catch (NoSuchAlgorithmException e) {
                      throw new CloudRuntimeException(e);
                  } catch (KeyStoreException e) {
                      throw new CloudRuntimeException(e);
                  }
              }
  
              _client = HttpClients.custom()
                      .setDefaultRequestConfig(config)
                      .setSSLHostnameVerifier(verifier)
                      .setSSLContext(sslContext)
                      .build();
          }
          return _client;
      }
  
      /**
       * pure array requires volume sizes in multiples of 512...we'll just round up to
       * next 512 boundary
       *
       * @param sizeInBytes
       * @return
       */
      private Long roundUp512Boundary(Long sizeInBytes) {
          Long remainder = sizeInBytes % 512;
          if (remainder > 0) {
              sizeInBytes = sizeInBytes + (512 - remainder);
          }
          return sizeInBytes;
      }
  
      @Override
      public Map<String, String> getConnectionIdMap(ProviderAdapterDataObject dataIn) {
          Map<String, String> map = new HashMap<String, String>();
  
          // flasharray doesn't let you directly map a snapshot to a host, so we'll just return an empty map
          if (dataIn.getType() == ProviderAdapterDataObject.Type.SNAPSHOT) {
              return map;
          }
  
          try {
              FlashArrayList<FlashArrayConnection> list = GET("/connections?volume_names=" + dataIn.getExternalName(),
                      new TypeReference<FlashArrayList<FlashArrayConnection>>() {
                      });
  
              if (list != null && list.getItems() != null) {
                  for (FlashArrayConnection conn : list.getItems()) {
                      if (conn.getHost() != null) {
                          map.put(conn.getHost().getName(), "" + conn.getLun());
                      }
                  }
              }
          } catch (Exception e) {
              // flasharray returns a 400 if the volume doesn't exist, so we'll just return an empty object.
              if (logger.isTraceEnabled()) {
                  logger.trace("Error getting connection map for volume [" + dataIn.getExternalName() + "]: " + e.toString(), e);
              }
          }
          return map;
      }
  
      @Override
      public boolean canDirectAttachSnapshot() {
          return false;
      }
  }
  