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.util;
  
  import com.cloud.host.Host;
  import com.cloud.host.HostVO;
  import com.cloud.utils.exception.CloudRuntimeException;
  import com.google.common.base.Preconditions;
  import com.google.gson.Gson;
  import com.google.gson.GsonBuilder;
  import com.google.gson.reflect.TypeToken;
  import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO;
  import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.http.HttpEntity;
  import org.apache.http.HttpHost;
  import org.apache.http.HttpRequest;
  import org.apache.http.HttpResponse;
  import org.apache.http.HttpStatus;
  import org.apache.http.StatusLine;
  import org.apache.http.client.methods.HttpDelete;
  import org.apache.http.client.methods.HttpGet;
  import org.apache.http.client.methods.HttpPost;
  import org.apache.http.client.methods.HttpPut;
  import org.apache.http.entity.StringEntity;
  import org.apache.http.impl.client.CloseableHttpClient;
  import org.apache.http.impl.client.HttpClientBuilder;
  import org.apache.http.util.EntityUtils;
  import org.apache.logging.log4j.Logger;
  import org.apache.logging.log4j.LogManager;
  
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.lang.reflect.Type;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.StringTokenizer;
  import java.util.UUID;
  
  public class DateraUtil {
  
      protected static Logger LOGGER = LogManager.getLogger(DateraUtil.class);
      private static final String API_VERSION = "v2";
  
      public static final String PROVIDER_NAME = "Datera";
      public static final String DRIVER_VERSION = "4.13.0-v2.0.0";
  
      private static final String HEADER_AUTH_TOKEN = "auth-token";
      private static final String HEADER_CONTENT_TYPE = "content-type";
      private static final String HEADER_VALUE_JSON = "application/json";
  
      public static final String MANAGEMENT_VIP = "mVip";
      public static final String STORAGE_VIP = "sVip";
  
      public static final String MANAGEMENT_PORT = "mPort";
      public static final String STORAGE_PORT = "sPort";
      public static final String DEFAULT_IP_POOL = "default";
  
      private static final int DEFAULT_MANAGEMENT_PORT = 7717;
      private static final int DEFAULT_STORAGE_PORT = 3260;
      private static final int DEFAULT_NUM_REPLICAS = 3;
  
      private static final long ONEGIB_BYTES = 1073741824;
  
      private static final String DEFAULT_VOL_PLACEMENT = "hybrid";
  
      public static final String CLUSTER_ADMIN_USERNAME = "clusterAdminUsername";
      public static final String CLUSTER_ADMIN_PASSWORD = "clusterAdminPassword";
  
      public static final String CLUSTER_DEFAULT_MIN_IOPS = "clusterDefaultMinIops";
      public static final String CLUSTER_DEFAULT_MAX_IOPS = "clusterDefaultMaxIops";
      public static final String NUM_REPLICAS = "numReplicas";
      public static final String VOL_PLACEMENT = "volPlacement";
  
      public static final String STORAGE_POOL_ID = "DateraStoragePoolId";
      public static final String VOLUME_SIZE = "DateraVolumeSize";
      public static final String VOLUME_ID = "DateraVolumeId";
      public static final String SNAPSHOT_ID = "DateraSnapshotId";
      public static final String TEMP_VOLUME_ID = "tempVolumeId";
      public static final String IP_POOL = "ipPool";
  
      public static final int MAX_IOPS = 10000; // max IOPS that can be assigned to a volume
  
      public static final String INITIATOR_GROUP_PREFIX = "CS-InitiatorGroup";
      public static final String INITIATOR_PREFIX = "CS-Initiator";
      public static final String APPINSTANCE_PREFIX = "CS";
      public static final int APPINSTANCE_MAX_LENTH = 64;
  
      public static final int MIN_NUM_REPLICAS = 1;
      public static final int MAX_NUM_REPLICAS = 5;
  
      public static final int POLL_TIMEOUT_MS = 3000;
      public static final String STATE_AVAILABLE = "available";
      public static final int DEFAULT_RETRIES = 10;
  
      private static Gson gson = new GsonBuilder().create();
  
      private int managementPort;
      private String managementIp;
      private String username;
      private String password;
  
      private static final String SCHEME_HTTP = "http";
      private static final int UUID_LENGTH = 8;
  
      public DateraUtil(String managementIp, int managementPort, String username, String password) {
          this.managementPort = managementPort;
          this.managementIp = managementIp;
          this.username = username;
          this.password = password;
      }
  
      public static String login(DateraObject.DateraConnection conn)
              throws UnsupportedEncodingException, DateraObject.DateraError {
  
          DateraObject.DateraLogin loginParams = new DateraObject.DateraLogin(conn.getUsername(), conn.getPassword());
          HttpPut loginReq = new HttpPut(generateApiUrl("login"));
  
          StringEntity jsonParams = new StringEntity(gson.toJson(loginParams));
          loginReq.setEntity(jsonParams);
  
          String response = executeHttp(conn, loginReq);
          DateraObject.DateraLoginResponse loginResponse = gson.fromJson(response,
                  DateraObject.DateraLoginResponse.class);
  
          return loginResponse.getKey();
  
      }
  
      public static Map<String, DateraObject.AppInstance> getAppInstances(DateraObject.DateraConnection conn)
              throws DateraObject.DateraError {
  
          HttpGet getAppInstancesReq = new HttpGet(generateApiUrl("app_instances"));
          String response = null;
  
          response = executeApiRequest(conn, getAppInstancesReq);
  
          Type responseType = new TypeToken<Map<String, DateraObject.AppInstance>>() {
          }.getType();
  
          return gson.fromJson(response, responseType);
      }
  
      public static DateraObject.AppInstance getAppInstance(DateraObject.DateraConnection conn, String name)
              throws DateraObject.DateraError {
  
          HttpGet url = new HttpGet(generateApiUrl("app_instances", name));
  
          String response = null;
          try {
              response = executeApiRequest(conn, url);
              return gson.fromJson(response, DateraObject.AppInstance.class);
          } catch (DateraObject.DateraError dateraError) {
              if (DateraObject.DateraErrorTypes.NotFoundError.equals(dateraError)) {
                  return null;
              } else {
                  throw dateraError;
              }
          }
      }
  
      public static DateraObject.PerformancePolicy getAppInstancePerformancePolicy(DateraObject.DateraConnection conn,
              String appInstanceName) throws DateraObject.DateraError {
  
          HttpGet url = new HttpGet(generateApiUrl("app_instances", appInstanceName, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "volumes", DateraObject.DEFAULT_VOLUME_NAME, "performance_policy"));
  
          try {
              String response = executeApiRequest(conn, url);
              return gson.fromJson(response, DateraObject.PerformancePolicy.class);
          } catch (DateraObject.DateraError dateraError) {
              if (DateraObject.DateraErrorTypes.NotFoundError.equals(dateraError)) {
                  return null;
              } else {
                  throw dateraError;
              }
          }
  
      }
  
      public static DateraObject.PerformancePolicy createAppInstancePerformancePolicy(DateraObject.DateraConnection conn,
              String appInstanceName, int totalIops) throws UnsupportedEncodingException, DateraObject.DateraError {
  
          HttpPost url = new HttpPost(generateApiUrl("app_instances", appInstanceName, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "volumes", DateraObject.DEFAULT_VOLUME_NAME, "performance_policy"));
  
          DateraObject.PerformancePolicy performancePolicy = new DateraObject.PerformancePolicy(totalIops);
  
          url.setEntity(new StringEntity(gson.toJson(performancePolicy)));
  
          String response = executeApiRequest(conn, url);
  
          return gson.fromJson(response, DateraObject.PerformancePolicy.class);
      }
  
      public static void updateAppInstanceIops(DateraObject.DateraConnection conn, String appInstance, int totalIops)
              throws UnsupportedEncodingException, DateraObject.DateraError {
  
          if (getAppInstancePerformancePolicy(conn, appInstance) == null) {
              createAppInstancePerformancePolicy(conn, appInstance, totalIops);
          } else {
  
              HttpPut url = new HttpPut(
                      generateApiUrl("app_instances", appInstance, "storage_instances", DateraObject.DEFAULT_STORAGE_NAME,
                              "volumes", DateraObject.DEFAULT_VOLUME_NAME, "performance_policy"));
  
              DateraObject.PerformancePolicy performancePolicy = new DateraObject.PerformancePolicy(totalIops);
  
              url.setEntity(new StringEntity(gson.toJson(performancePolicy)));
              executeApiRequest(conn, url);
          }
      }
  
      public static void updateAppInstanceSize(DateraObject.DateraConnection conn, String appInstanceName, int newSize)
              throws UnsupportedEncodingException, DateraObject.DateraError {
  
          HttpPut url = new HttpPut(generateApiUrl("app_instances", appInstanceName, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "volumes", DateraObject.DEFAULT_VOLUME_NAME));
  
          DateraObject.Volume volume = new DateraObject.Volume(newSize);
          url.setEntity(new StringEntity(gson.toJson(volume)));
          executeApiRequest(conn, url);
  
      }
  
      public static void updateAppInstancePlacement(DateraObject.DateraConnection conn, String appInstanceName,
              String newPlacementMode) throws UnsupportedEncodingException, DateraObject.DateraError {
          HttpPut url = new HttpPut(generateApiUrl("app_instances", appInstanceName, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "volumes", DateraObject.DEFAULT_VOLUME_NAME));
  
          DateraObject.Volume volume = new DateraObject.Volume(newPlacementMode);
          url.setEntity(new StringEntity(gson.toJson(volume)));
          executeApiRequest(conn, url);
  
      }
  
      private static DateraObject.AppInstance createAppInstance(DateraObject.DateraConnection conn, String name,
              StringEntity appInstanceEntity) throws DateraObject.DateraError {
  
          HttpPost createAppInstance = new HttpPost(generateApiUrl("app_instances"));
          HttpGet getAppInstance = new HttpGet(generateApiUrl("app_instances", name));
          createAppInstance.setEntity(appInstanceEntity);
          String response = null;
  
          executeApiRequest(conn, createAppInstance);
  
          // create is async, do a get to fetch the IQN
          executeApiRequest(conn, getAppInstance);
  
          return pollAppInstanceAvailable(conn, name);
      }
  
      public static DateraObject.AppInstance createAppInstance(DateraObject.DateraConnection conn, String name, int size,
              int totalIops, int replicaCount) throws UnsupportedEncodingException, DateraObject.DateraError {
  
          DateraObject.AppInstance appInstance = new DateraObject.AppInstance(name, size, totalIops, replicaCount,
                  DEFAULT_VOL_PLACEMENT, DEFAULT_IP_POOL);
          StringEntity appInstanceEntity = new StringEntity(gson.toJson(appInstance));
  
          return createAppInstance(conn, name, appInstanceEntity);
      }
  
      public static DateraObject.AppInstance createAppInstance(DateraObject.DateraConnection conn, String name, int size,
              int totalIops, int replicaCount, String placementMode, String ipPool)
              throws UnsupportedEncodingException, DateraObject.DateraError {
  
          DateraObject.AppInstance appInstance = new DateraObject.AppInstance(name, size, totalIops, replicaCount,
                  placementMode, ipPool);
          StringEntity appInstanceEntity = new StringEntity(gson.toJson(appInstance));
  
          return createAppInstance(conn, name, appInstanceEntity);
      }
  
      public static DateraObject.AppInstance cloneAppInstanceFromVolume(DateraObject.DateraConnection conn, String name,
              String srcCloneName) throws UnsupportedEncodingException, DateraObject.DateraError {
          return null;
      }
  
      public static DateraObject.AppInstance cloneAppInstanceFromVolume(DateraObject.DateraConnection conn, String name,
              String srcCloneName, String ipPool) throws UnsupportedEncodingException, DateraObject.DateraError {
          LOGGER.debug("cloneAppInstanceFromVolume() called");
          DateraObject.AppInstance srcAppInstance = getAppInstance(conn, srcCloneName);
  
          if (srcAppInstance == null) {
              throw new DateraObject.DateraError("NotFoundError", 404, null,
                      "Unable to find the base app instance to clone from");
          }
  
          String srcClonePath = srcAppInstance.getVolumePath();
  
          DateraObject.AppInstance appInstanceObj = new DateraObject.AppInstance(name, srcClonePath);
  
          StringEntity appInstanceEntity = new StringEntity(gson.toJson(appInstanceObj));
          DateraObject.AppInstance appInstance = createAppInstance(conn, name, appInstanceEntity);
  
          // Update ipPool
          updateAppInstanceIpPool(conn, name, ipPool);
  
          // bring it online
          updateAppInstanceAdminState(conn, name, DateraObject.AppState.ONLINE);
  
          return getAppInstance(conn, name);
      }
  
      public static DateraObject.AppInstance pollAppInstanceAvailable(DateraObject.DateraConnection conn,
              String appInstanceName) throws DateraObject.DateraError {
  
          int retries = DateraUtil.DEFAULT_RETRIES;
          DateraObject.AppInstance appInstance = null;
          do {
              appInstance = getAppInstance(conn, appInstanceName);
              try {
                  Thread.sleep(DateraUtil.POLL_TIMEOUT_MS);
              } catch (InterruptedException e) {
                  return null;
              }
              retries--;
          } while ((appInstance != null && !Objects.equals(appInstance.getVolumeOpState(), DateraUtil.STATE_AVAILABLE))
                  && retries > 0);
          return appInstance;
      }
  
      public static DateraObject.Initiator createInitiator(DateraObject.DateraConnection conn, String name, String iqn)
              throws DateraObject.DateraError, UnsupportedEncodingException {
  
          HttpPost req = new HttpPost(generateApiUrl("initiators"));
  
          DateraObject.Initiator initiator = new DateraObject.Initiator(name, iqn);
          StringEntity httpEntity = new StringEntity(gson.toJson(initiator));
          req.setEntity(httpEntity);
  
          return gson.fromJson(executeApiRequest(conn, req), DateraObject.Initiator.class);
      }
  
      public static DateraObject.Initiator getInitiator(DateraObject.DateraConnection conn, String iqn)
              throws DateraObject.DateraError {
  
          try {
              HttpGet getReq = new HttpGet(generateApiUrl("initiators", iqn));
              String response = executeApiRequest(conn, getReq);
              return gson.fromJson(response, DateraObject.Initiator.class);
          } catch (DateraObject.DateraError dateraError) {
              if (DateraObject.DateraErrorTypes.NotFoundError.equals(dateraError)) {
                  return null;
              } else {
                  throw dateraError;
              }
          }
      }
  
      public static void deleteInitiator(DateraObject.DateraConnection conn, String iqn) throws DateraObject.DateraError {
  
          HttpDelete req = new HttpDelete(generateApiUrl("initiators", iqn));
          executeApiRequest(conn, req);
      }
  
      public static DateraObject.InitiatorGroup createInitiatorGroup(DateraObject.DateraConnection conn, String name)
              throws UnsupportedEncodingException, DateraObject.DateraError {
  
          HttpPost createReq = new HttpPost(generateApiUrl("initiator_groups"));
  
          DateraObject.InitiatorGroup group = new DateraObject.InitiatorGroup(name, Collections.<String>emptyList());
  
          StringEntity httpEntity = new StringEntity(gson.toJson(group));
          createReq.setEntity(httpEntity);
  
          String response = executeApiRequest(conn, createReq);
          return gson.fromJson(response, DateraObject.InitiatorGroup.class);
      }
  
      public static void deleteInitatorGroup(DateraObject.DateraConnection conn, String name)
              throws DateraObject.DateraError {
          HttpDelete delReq = new HttpDelete(generateApiUrl("initiator_groups", name));
          executeApiRequest(conn, delReq);
      }
  
      public static DateraObject.InitiatorGroup getInitiatorGroup(DateraObject.DateraConnection conn, String name)
              throws DateraObject.DateraError {
          try {
              HttpGet getReq = new HttpGet(generateApiUrl("initiator_groups", name));
              String response = executeApiRequest(conn, getReq);
              return gson.fromJson(response, DateraObject.InitiatorGroup.class);
  
          } catch (DateraObject.DateraError dateraError) {
              if (DateraObject.DateraErrorTypes.NotFoundError.equals(dateraError)) {
                  return null;
              } else {
                  throw dateraError;
              }
          }
      }
  
      public static void updateInitiatorGroup(DateraObject.DateraConnection conn, String initiatorPath, String groupName,
              DateraObject.DateraOperation op) throws DateraObject.DateraError, UnsupportedEncodingException {
  
          DateraObject.InitiatorGroup initiatorGroup = getInitiatorGroup(conn, groupName);
  
          if (initiatorGroup == null) {
              throw new CloudRuntimeException("Unable to find initiator group by name " + groupName);
          }
  
          HttpPut addReq = new HttpPut(generateApiUrl("initiator_groups", groupName, "members"));
  
          DateraObject.Initiator initiator = new DateraObject.Initiator(initiatorPath, op);
  
          addReq.setEntity(new StringEntity(gson.toJson(initiator)));
          executeApiRequest(conn, addReq);
      }
  
      public static void addInitiatorToGroup(DateraObject.DateraConnection conn, String initiatorPath, String groupName)
              throws UnsupportedEncodingException, DateraObject.DateraError {
          updateInitiatorGroup(conn, initiatorPath, groupName, DateraObject.DateraOperation.ADD);
      }
  
      public static void removeInitiatorFromGroup(DateraObject.DateraConnection conn, String initiatorPath,
              String groupName) throws DateraObject.DateraError, UnsupportedEncodingException {
          updateInitiatorGroup(conn, initiatorPath, groupName, DateraObject.DateraOperation.REMOVE);
      }
  
      public static Map<String, DateraObject.InitiatorGroup> getAppInstanceInitiatorGroups(
              DateraObject.DateraConnection conn, String appInstance) throws DateraObject.DateraError {
          HttpGet req = new HttpGet(generateApiUrl("app_instances", appInstance, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "acl_policy", "initiator_groups"));
  
          String response = executeApiRequest(conn, req);
  
          if (response == null) {
              return null;
          }
  
          Type responseType = new TypeToken<Map<String, DateraObject.InitiatorGroup>>() {
          }.getType();
  
          return gson.fromJson(response, responseType);
      }
  
      public static void assignGroupToAppInstance(DateraObject.DateraConnection conn, String group, String appInstance)
              throws DateraObject.DateraError, UnsupportedEncodingException {
  
          DateraObject.InitiatorGroup initiatorGroup = getInitiatorGroup(conn, group);
  
          if (initiatorGroup == null) {
              throw new CloudRuntimeException("Initator group " + group + " not found ");
          }
  
          Map<String, DateraObject.InitiatorGroup> initiatorGroups = getAppInstanceInitiatorGroups(conn, appInstance);
  
          if (initiatorGroups == null) {
              throw new CloudRuntimeException("Initator group not found for appInstnace " + appInstance);
          }
  
          for (DateraObject.InitiatorGroup ig : initiatorGroups.values()) {
              if (ig.getName().equals(group)) {
                  // already assigned
                  return;
              }
          }
  
          HttpPut url = new HttpPut(generateApiUrl("app_instances", appInstance, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "acl_policy", "initiator_groups"));
  
          url.setEntity(new StringEntity(gson
                  .toJson(new DateraObject.InitiatorGroup(initiatorGroup.getPath(), DateraObject.DateraOperation.ADD))));
  
          executeApiRequest(conn, url);
      }
  
      public static void removeGroupFromAppInstance(DateraObject.DateraConnection conn, String group, String appInstance)
              throws DateraObject.DateraError, UnsupportedEncodingException {
  
          DateraObject.InitiatorGroup initiatorGroup = getInitiatorGroup(conn, group);
  
          if (initiatorGroup == null) {
              throw new CloudRuntimeException("Initator groups not found for appInstnace " + appInstance);
          }
  
          Map<String, DateraObject.InitiatorGroup> initiatorGroups = getAppInstanceInitiatorGroups(conn, appInstance);
  
          if (initiatorGroups == null) {
              throw new CloudRuntimeException("Initator group not found for appInstnace " + appInstance);
          }
  
          boolean groupAssigned = false;
  
          for (DateraObject.InitiatorGroup ig : initiatorGroups.values()) {
              if (ig.getName().equals(group)) {
                  groupAssigned = true;
                  break;
              }
          }
  
          if (!groupAssigned) {
              return; // already removed
          }
  
          HttpPut url = new HttpPut(generateApiUrl("app_instances", appInstance, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "acl_policy", "initiator_groups"));
  
          url.setEntity(new StringEntity(gson.toJson(
                  new DateraObject.InitiatorGroup(initiatorGroup.getPath(), DateraObject.DateraOperation.REMOVE))));
  
          executeApiRequest(conn, url);
      }
  
      public static void updateAppInstanceAdminState(DateraObject.DateraConnection conn, String appInstanceName,
              DateraObject.AppState appState) throws UnsupportedEncodingException, DateraObject.DateraError {
  
          DateraObject.AppInstance appInstance = new DateraObject.AppInstance(appState);
          HttpPut updateAppInstanceReq = new HttpPut(generateApiUrl("app_instances", appInstanceName));
  
          updateAppInstanceReq.setEntity(new StringEntity(gson.toJson(appInstance)));
          executeApiRequest(conn, updateAppInstanceReq);
      }
  
      public static void updateAppInstanceIpPool(DateraObject.DateraConnection conn, String appInstanceName,
              String ipPool) throws UnsupportedEncodingException, DateraObject.DateraError {
  
          HttpPut url = new HttpPut(generateApiUrl("app_instances", appInstanceName, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME));
  
          url.setEntity(new StringEntity(gson.toJson(new DateraObject.AccessNetworkIpPool(ipPool))));
  
          executeApiRequest(conn, url);
      }
  
      public static void deleteAppInstance(DateraObject.DateraConnection conn, String name)
              throws UnsupportedEncodingException, DateraObject.DateraError {
  
          HttpDelete deleteAppInstanceReq = new HttpDelete(generateApiUrl("app_instances", name));
          updateAppInstanceAdminState(conn, name, DateraObject.AppState.OFFLINE);
          executeApiRequest(conn, deleteAppInstanceReq);
      }
  
      public static DateraObject.AppInstance cloneAppInstanceFromSnapshot(DateraObject.DateraConnection conn,
              String newAppInstanceName, String snapshotName)
              throws DateraObject.DateraError, UnsupportedEncodingException {
  
          return cloneAppInstanceFromSnapshot(conn, newAppInstanceName, snapshotName, DEFAULT_IP_POOL);
      }
  
      public static DateraObject.AppInstance cloneAppInstanceFromSnapshot(DateraObject.DateraConnection conn,
              String newAppInstanceName, String snapshotName, String ipPool)
              throws DateraObject.DateraError, UnsupportedEncodingException {
  
          // split the snapshot name to appInstanceName and the snapshot timestamp
          String[] tokens = snapshotName.split(":");
          Preconditions.checkArgument(tokens.length == 2);
  
          // A snapshot is stored in Cloudstack as <AppInstanceName>:<SnapshotTime>
          String appInstanceName = tokens[0];
          String snapshotTime = tokens[1];
  
          // get the snapshot from Datera
          HttpGet getSnasphotReq = new HttpGet(
                  generateApiUrl("app_instances", appInstanceName, "storage_instances", DateraObject.DEFAULT_STORAGE_NAME,
                          "volumes", DateraObject.DEFAULT_VOLUME_NAME, "snapshots", snapshotTime));
  
          String resp = executeApiRequest(conn, getSnasphotReq);
  
          DateraObject.VolumeSnapshot snapshot = gson.fromJson(resp, DateraObject.VolumeSnapshot.class);
  
          String snapshotPath = snapshot.getPath();
  
          DateraObject.AppInstance appInstanceObj = new DateraObject.AppInstance(newAppInstanceName, snapshotPath);
  
          StringEntity appInstanceEntity = new StringEntity(gson.toJson(appInstanceObj));
  
          DateraObject.AppInstance appInstance = createAppInstance(conn, newAppInstanceName, appInstanceEntity);
  
          // Update ipPool
          updateAppInstanceIpPool(conn, newAppInstanceName, ipPool);
  
          // bring it online
          updateAppInstanceAdminState(conn, newAppInstanceName, DateraObject.AppState.ONLINE);
  
          return getAppInstance(conn, newAppInstanceName);
      }
  
      public static void deleteVolumeSnapshot(DateraObject.DateraConnection conn, String snapshotName)
              throws DateraObject.DateraError {
  
          // split the snapshot name to appInstanceName and the snapshot timestamp
          String[] tokens = snapshotName.split(":");
          Preconditions.checkArgument(tokens.length == 2);
  
          // A snapshot is stored in Cloudstack as <AppInstanceName>:<SnapshotTime>
          String appInstanceName = tokens[0];
          String snapshotTime = tokens[1];
  
          HttpDelete deleteSnapshotReq = new HttpDelete(
                  generateApiUrl("app_instances", appInstanceName, "storage_instances", DateraObject.DEFAULT_STORAGE_NAME,
                          "volumes", DateraObject.DEFAULT_VOLUME_NAME, "snapshots", snapshotTime));
  
          executeApiRequest(conn, deleteSnapshotReq);
      }
  
      public static DateraObject.VolumeSnapshot getVolumeSnapshot(DateraObject.DateraConnection conn,
              String appInstanceName, String snapshotTime) throws DateraObject.DateraError {
  
          HttpGet getSnapshotReq = new HttpGet(
                  generateApiUrl("app_instances", appInstanceName, "storage_instances", DateraObject.DEFAULT_STORAGE_NAME,
                          "volumes", DateraObject.DEFAULT_VOLUME_NAME, "snapshots", snapshotTime));
  
          String resp = executeApiRequest(conn, getSnapshotReq);
          return gson.fromJson(resp, DateraObject.VolumeSnapshot.class);
      }
  
      public static DateraObject.VolumeSnapshot takeVolumeSnapshot(DateraObject.DateraConnection conn,
              String baseAppInstanceName) throws UnsupportedEncodingException, DateraObject.DateraError {
  
          HttpPost takeSnasphotReq = new HttpPost(
                  generateApiUrl("app_instances", baseAppInstanceName, "storage_instances",
                          DateraObject.DEFAULT_STORAGE_NAME, "volumes", DateraObject.DEFAULT_VOLUME_NAME, "snapshots"));
  
          String snapshotUuid = UUID.randomUUID().toString();
          DateraObject.VolumeSnapshot volumeSnapshot = new DateraObject.VolumeSnapshot(snapshotUuid);
          takeSnasphotReq.setEntity(new StringEntity(gson.toJson(volumeSnapshot)));
          String snapshotResponse = executeApiRequest(conn, takeSnasphotReq);
          volumeSnapshot = gson.fromJson(snapshotResponse, DateraObject.VolumeSnapshot.class);
          String snapshotTime = volumeSnapshot.getTimestamp();
  
          int retries = DateraUtil.DEFAULT_RETRIES;
  
          do {
              try {
                  Thread.sleep(DateraUtil.POLL_TIMEOUT_MS);
              } catch (InterruptedException e) {
                  return null;
              }
              volumeSnapshot = getVolumeSnapshot(conn, baseAppInstanceName, snapshotTime);
          } while ((!Objects.equals(volumeSnapshot.getOpState(), DateraUtil.STATE_AVAILABLE)) && --retries > 0);
  
          return volumeSnapshot;
      }
  
      public static DateraObject.AppInstance restoreVolumeSnapshot(DateraObject.DateraConnection conn,
              String snapshotName) throws DateraObject.DateraError {
  
          // split the snapshot name to appInstanceName and the snapshot timestamp
          String[] tokens = snapshotName.split(":");
          Preconditions.checkArgument(tokens.length == 2);
  
          // A snapshot is stored in Cloudstack as <AppInstanceName>:<SnapshotTime>
          String appInstanceName = tokens[0];
          String snapshotTime = tokens[1];
  
          HttpPut restoreSnapshotReq = new HttpPut(generateApiUrl("app_instances", appInstanceName, "storage_instances",
                  DateraObject.DEFAULT_STORAGE_NAME, "volumes", DateraObject.DEFAULT_VOLUME_NAME));
  
          try {
              // bring appInstance offline
              updateAppInstanceAdminState(conn, appInstanceName, DateraObject.AppState.OFFLINE);
  
              DateraObject.VolumeSnapshotRestore volumeSnapshotRestore = new DateraObject.VolumeSnapshotRestore(
                      snapshotTime);
  
              StringEntity jsonParams = new StringEntity(gson.toJson(volumeSnapshotRestore));
              restoreSnapshotReq.setEntity(jsonParams);
              executeApiRequest(conn, restoreSnapshotReq);
              // bring appInstance online
              updateAppInstanceAdminState(conn, appInstanceName, DateraObject.AppState.ONLINE);
  
          } catch (UnsupportedEncodingException e) {
              throw new CloudRuntimeException("Failed to restore volume snapshot" + e.getMessage());
          }
          return getAppInstance(conn, appInstanceName);
  
      }
  
      private static String executeApiRequest(DateraObject.DateraConnection conn, HttpRequest apiReq)
              throws DateraObject.DateraError {
  
          // Get the token first
          String authToken = null;
          try {
              authToken = login(conn);
          } catch (UnsupportedEncodingException e) {
              throw new CloudRuntimeException("Unable to login to Datera " + e.getMessage());
          }
  
          if (authToken == null) {
              throw new CloudRuntimeException("Unable to login to Datera: error getting auth token ");
          }
  
          apiReq.addHeader(HEADER_AUTH_TOKEN, authToken);
  
          return executeHttp(conn, apiReq);
      }
  
      private static String executeHttp(DateraObject.DateraConnection conn, HttpRequest request)
              throws DateraObject.DateraError {
          CloseableHttpClient httpclient = HttpClientBuilder.create().build();
          String response = null;
  
          if (null == httpclient) {
              throw new CloudRuntimeException("Unable to create httpClient for request");
          }
  
          try {
  
              request.setHeader(HEADER_CONTENT_TYPE, HEADER_VALUE_JSON);
  
              HttpHost target = new HttpHost(conn.getManagementIp(), conn.getManagementPort(), SCHEME_HTTP);
  
              HttpResponse httpResponse = httpclient.execute(target, request);
  
              HttpEntity entity = httpResponse.getEntity();
              StatusLine status = httpResponse.getStatusLine();
              response = EntityUtils.toString(entity);
  
              assert response != null;
  
              if (status.getStatusCode() != HttpStatus.SC_OK) {
                  // check if this is an error
                  DateraObject.DateraError error = gson.fromJson(response, DateraObject.DateraError.class);
                  if (error != null && error.isError()) {
                      throw error;
                  } else {
                      throw new CloudRuntimeException("Error while trying to get HTTP object from Datera");
                  }
  
              }
  
          } catch (IOException e) {
              throw new CloudRuntimeException("Error while sending request to Datera. Error " + e.getMessage());
          }
  
          return response;
      }
  
      protected static String generateApiUrl(String... args) {
          ArrayList<String> urlList = new ArrayList<String>(Arrays.asList(args));
  
          urlList.add(0, API_VERSION);
          urlList.add(0, "");
  
          return StringUtils.join(urlList, "/");
      }
  
      public static String getManagementVip(String url) {
          return getVip(DateraUtil.MANAGEMENT_VIP, url);
      }
  
      public static String getStorageVip(String url) {
          return getVip(DateraUtil.STORAGE_VIP, url);
      }
  
      public static int getManagementPort(String url) {
          return getPort(DateraUtil.MANAGEMENT_VIP, url, DEFAULT_MANAGEMENT_PORT);
      }
  
      public static int getStoragePort(String url) {
          return getPort(DateraUtil.STORAGE_VIP, url, DEFAULT_STORAGE_PORT);
      }
  
      public static int getNumReplicas(String url) {
          try {
              String value = getValue(DateraUtil.NUM_REPLICAS, url, false);
              return Integer.parseInt(value);
          } catch (NumberFormatException ex) {
              return DEFAULT_NUM_REPLICAS;
          }
  
      }
  
      public static String getVolPlacement(String url) {
          String volPlacement = getValue(DateraUtil.VOL_PLACEMENT, url, false);
          if (volPlacement == null) {
              return DEFAULT_VOL_PLACEMENT;
          } else {
              return volPlacement;
          }
      }
  
      public static String getIpPool(String url) {
          String ipPool = getValue(DateraUtil.IP_POOL, url, false);
          if (ipPool == null) {
              return DEFAULT_IP_POOL;
          } else {
              return ipPool;
          }
      }
  
      private static String getVip(String keyToMatch, String url) {
          String delimiter = ":";
  
          String storageVip = getValue(keyToMatch, url);
  
          int index = storageVip.indexOf(delimiter);
  
          if (index != -1) {
              return storageVip.substring(0, index);
          }
  
          return storageVip;
      }
  
      private static int getPort(String keyToMatch, String url, int defaultPortNumber) {
          String delimiter = ":";
  
          String storageVip = getValue(keyToMatch, url);
  
          int index = storageVip.indexOf(delimiter);
  
          int portNumber = defaultPortNumber;
  
          if (index != -1) {
              String port = storageVip.substring(index + delimiter.length());
  
              try {
                  portNumber = Integer.parseInt(port);
              } catch (NumberFormatException ex) {
                  throw new IllegalArgumentException("Invalid URL format (port is not an integer)");
              }
          }
  
          return portNumber;
      }
  
      public static String getValue(String keyToMatch, String url) {
          return getValue(keyToMatch, url, true);
      }
  
      public static String getValue(String keyToMatch, String url, boolean throwExceptionIfNotFound) {
          String delimiter1 = ";";
          String delimiter2 = "=";
  
          StringTokenizer st = new StringTokenizer(url, delimiter1);
  
          while (st.hasMoreElements()) {
              String token = st.nextElement().toString();
  
              int index = token.indexOf(delimiter2);
  
              if (index == -1) {
                  throw new CloudRuntimeException("Invalid URL format");
              }
  
              String key = token.substring(0, index);
  
              if (key.equalsIgnoreCase(keyToMatch)) {
                  return token.substring(index + delimiter2.length());
              }
          }
  
          if (throwExceptionIfNotFound) {
              throw new CloudRuntimeException("Key not found in URL");
          }
  
          return null;
      }
  
      public static String getModifiedUrl(String originalUrl) {
          StringBuilder sb = new StringBuilder();
  
          String delimiter = ";";
  
          StringTokenizer st = new StringTokenizer(originalUrl, delimiter);
  
          while (st.hasMoreElements()) {
              String token = st.nextElement().toString().toUpperCase();
  
              if (token.startsWith(DateraUtil.MANAGEMENT_VIP.toUpperCase())
                      || token.startsWith(DateraUtil.STORAGE_VIP.toUpperCase())) {
                  sb.append(token).append(delimiter);
              }
          }
  
          String modifiedUrl = sb.toString();
          int lastIndexOf = modifiedUrl.lastIndexOf(delimiter);
  
          if (lastIndexOf == (modifiedUrl.length() - delimiter.length())) {
              return modifiedUrl.substring(0, lastIndexOf);
          }
  
          return modifiedUrl;
      }
  
      public static DateraObject.DateraConnection getDateraConnection(long storagePoolId,
              StoragePoolDetailsDao storagePoolDetailsDao) {
          StoragePoolDetailVO storagePoolDetail = storagePoolDetailsDao.findDetail(storagePoolId,
                  DateraUtil.MANAGEMENT_VIP);
  
          String mVip = storagePoolDetail.getValue();
  
          storagePoolDetail = storagePoolDetailsDao.findDetail(storagePoolId, DateraUtil.MANAGEMENT_PORT);
  
          int mPort = Integer.parseInt(storagePoolDetail.getValue());
  
          storagePoolDetail = storagePoolDetailsDao.findDetail(storagePoolId, DateraUtil.CLUSTER_ADMIN_USERNAME);
  
          String clusterAdminUsername = storagePoolDetail.getValue();
  
          storagePoolDetail = storagePoolDetailsDao.findDetail(storagePoolId, DateraUtil.CLUSTER_ADMIN_PASSWORD);
  
          String clusterAdminPassword = storagePoolDetail.getValue();
  
          return new DateraObject.DateraConnection(mVip, mPort, clusterAdminUsername, clusterAdminPassword);
      }
  
      public static boolean hostsSupport_iScsi(List<HostVO> hosts) {
          if (hosts == null || hosts.size() == 0) {
              return false;
          }
  
          for (Host host : hosts) {
              if (!hostSupport_iScsi(host)) {
                  return false;
              }
          }
  
          return true;
      }
  
      public static boolean hostSupport_iScsi(Host host) {
          if (host == null || host.getStorageUrl() == null || host.getStorageUrl().trim().length() == 0
                  || !host.getStorageUrl().startsWith("iqn")) {
              return false;
          }
          return true;
      }
  
      public static String getInitiatorGroupKey(long storagePoolId) {
          return "DateraInitiatorGroup-" + storagePoolId;
      }
  
      /**
       * Checks whether a host initiator is present in an initiator group
       *
       * @param initiator      Host initiator to check
       * @param initiatorGroup the initiator group
       * @return true if host initiator is in the group, false otherwise
       */
      public static boolean isInitiatorPresentInGroup(DateraObject.Initiator initiator,
              DateraObject.InitiatorGroup initiatorGroup) {
  
          for (String memberPath : initiatorGroup.getMembers()) {
              if (memberPath.equals(initiator.getPath())) {
                  return true;
              }
          }
  
          return false;
      }
  
      public static int bytesToGib(long volumeSizeBytes) {
          return (int) Math.ceil(volumeSizeBytes / (double) ONEGIB_BYTES);
      }
  
      public static long gibToBytes(int volumeSizeGb) {
          return volumeSizeGb * ONEGIB_BYTES;
      }
  
      /**
       * IQN path is stored in the DB by cloudstack it is of the form /<IQN>/0
       *
       * @param iqn: IQN of the LUN
       * @return IQN path as defined above
       */
      public static String generateIqnPath(String iqn) {
          if (iqn != null) {
              return "/" + iqn + "/0";
          }
          return null;
      }
  
      /**
       * Does the opposite of generateIqnPath
       *
       * @param iqnPath
       * @return timmed IQN path
       */
  
      public static String extractIqn(String iqnPath) {
  
          if (iqnPath == null) {
              return null;
          }
  
          if (iqnPath.endsWith("/")) {
              iqnPath = iqnPath.substring(0, iqnPath.length() - 1);
          }
  
          final String tokens[] = iqnPath.split("/");
          if (tokens.length != 3) {
              final String msg = "Wrong iscsi path " + iqnPath + " it should be /targetIQN/LUN";
              LOGGER.warn(msg);
              return null;
          }
  
          return tokens[1].trim();
      }
  
      /**
       * Generate random uuid
       *
       * @param seed
       * @param length ( default to 8 )
       * @return String uuid
       */
      public static String generateUUID(String seed) {
          int length = UUID_LENGTH;
          // creating UUID
          UUID uid = UUID.fromString(seed);
          String uuid = String.valueOf(uid.randomUUID()).substring(0, length);
  
          return uuid;
      }
  
  }
  