blob: 34179680607ec9eedb7ac2a28c8f831f3cc8a1bf [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.storage.datastore.adapter.flasharray;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import org.apache.http.Header;
import org.apache.http.NameValuePair;
import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapter;
import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapterContext;
import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapterDataObject;
import org.apache.cloudstack.storage.datastore.adapter.ProviderAdapterDiskOffering;
import org.apache.cloudstack.storage.datastore.adapter.ProviderSnapshot;
import org.apache.cloudstack.storage.datastore.adapter.ProviderVolume;
import org.apache.cloudstack.storage.datastore.adapter.ProviderVolumeNamer;
import org.apache.cloudstack.storage.datastore.adapter.ProviderVolumeStats;
import org.apache.cloudstack.storage.datastore.adapter.ProviderVolumeStorageStats;
import org.apache.cloudstack.storage.datastore.adapter.ProviderVolume.AddressType;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import 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 volumeName = normalizeName(pod, dataObject.getExternalName());
try {
FlashArrayList<FlashArrayConnection> list = POST("/connections?host_group_names=" + hostgroup + "&volume_names=" + volumeName, null, new TypeReference<FlashArrayList<FlashArrayConnection>> () { });
if (list == null || list.getItems() == null || list.getItems().size() == 0) {
throw new RuntimeException("Volume attach did not return lun information");
}
FlashArrayConnection connection = (FlashArrayConnection)this.getFlashArrayItem(list);
if (connection.getLun() == null) {
throw new RuntimeException("Volume attach missing lun field");
}
return ""+connection.getLun();
} catch (Throwable e) {
// the volume is already attached. happens in some scenarios where orchestration creates the volume before copying to it
if (e.toString().contains("Connection already exists")) {
FlashArrayList<FlashArrayConnection> list = GET("/connections?volume_names=" + volumeName,
new TypeReference<FlashArrayList<FlashArrayConnection>>() {
});
if (list != null && list.getItems() != null) {
return ""+list.getItems().get(0).getLun();
} else {
throw new RuntimeException("Volume lun is not found in existing connection");
}
} else {
throw e;
}
}
}
@Override
public void detach(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
String volumeName = normalizeName(pod, dataObject.getExternalName());
DELETE("/connections?host_group_names=" + hostgroup + "&volume_names=" + volumeName);
}
@Override
public void delete(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
// public void deleteVolume(String volumeNamespace, String volumeName) {
// first make sure we are disconnected
removeVlunsAll(context, pod, dataObject.getExternalName());
String fullName = normalizeName(pod, dataObject.getExternalName());
FlashArrayVolume volume = new FlashArrayVolume();
volume.setDestroyed(true);
try {
PATCH("/volumes?names=" + fullName, volume, new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
} catch (CloudRuntimeException e) {
if (e.toString().contains("Volume does not exist")) {
return;
} else {
throw e;
}
}
}
@Override
public ProviderVolume getVolume(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
String externalName = dataObject.getExternalName();
// if its not set, look for the generated name for some edge cases
if (externalName == null) {
externalName = pod + "::" + ProviderVolumeNamer.generateObjectName(context, dataObject);
}
FlashArrayVolume volume = null;
try {
volume = getVolume(externalName);
// if we didn't get an address back its likely an empty object
if (volume != null && volume.getAddress() == null) {
return null;
} else if (volume == null) {
return null;
}
populateConnectionId(volume);
return volume;
} catch (Exception e) {
// assume any exception is a not found. Flash returns 400's for most errors
return null;
}
}
@Override
public ProviderVolume getVolumeByAddress(ProviderAdapterContext context, AddressType addressType, String address) {
// public FlashArrayVolume getVolumeByWwn(String wwn) {
if (address == null ||addressType == null) {
throw new RuntimeException("Invalid search criteria provided for getVolumeByAddress");
}
// only support WWN type addresses at this time.
if (!ProviderVolume.AddressType.FIBERWWN.equals(addressType)) {
throw new RuntimeException(
"Invalid volume address type [" + addressType + "] requested for volume search");
}
// convert WWN to serial to search on. strip out WWN type # + Flash OUI value
String serial = address.substring(FlashArrayVolume.PURE_OUI.length() + 1).toUpperCase();
String query = "serial='" + serial + "'";
FlashArrayVolume volume = null;
try {
FlashArrayList<FlashArrayVolume> list = GET("/volumes?filter=" + query,
new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
// if we didn't get an address back its likely an empty object
if (list == null || list.getItems() == null || list.getItems().size() == 0) {
return null;
}
volume = (FlashArrayVolume)this.getFlashArrayItem(list);
if (volume != null && volume.getAddress() == null) {
return null;
}
populateConnectionId(volume);
return volume;
} catch (Exception e) {
// assume any exception is a not found. Flash returns 400's for most errors
return null;
}
}
private void populateConnectionId(FlashArrayVolume volume) {
// we need to see if there is a connection (lun) associated with this volume.
// note we assume 1 lun for the hostgroup associated with this object
FlashArrayList<FlashArrayConnection> list = null;
try {
list = GET("/connections?volume_names=" + volume.getExternalName(),
new TypeReference<FlashArrayList<FlashArrayConnection>>() {
});
} catch (CloudRuntimeException e) {
// this means there is no attachment associated with this volume on the array
if (e.toString().contains("Bad Request")) {
return;
}
}
if (list != null && list.getItems() != null) {
for (FlashArrayConnection conn: list.getItems()) {
if (conn.getHostGroup() != null && conn.getHostGroup().getName().equals(this.hostgroup)) {
volume.setExternalConnectionId(""+conn.getLun());
break;
}
}
}
}
@Override
public void resize(ProviderAdapterContext context, ProviderAdapterDataObject dataObject, long newSizeInBytes) {
// public void resizeVolume(String volumeNamespace, String volumeName, long
// newSizeInBytes) {
FlashArrayVolume volume = new FlashArrayVolume();
volume.setAllocatedSizeBytes(roundUp512Boundary(newSizeInBytes));
PATCH("/volumes?names=" + dataObject.getExternalName(), volume, null);
}
/**
* Take a snapshot and return a Volume representing that snapshot
*
* @param volumeName
* @param snapshotName
* @return
*/
@Override
public ProviderSnapshot snapshot(ProviderAdapterContext context, ProviderAdapterDataObject sourceDataObject, ProviderAdapterDataObject targetDataObject) {
// public FlashArrayVolume snapshotVolume(String volumeNamespace, String
// volumeName, String snapshotName) {
FlashArrayList<FlashArrayVolume> list = POST(
"/volume-snapshots?source_names=" + sourceDataObject.getExternalName(), null,
new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
return (FlashArrayVolume) getFlashArrayItem(list);
}
/**
* Replaces the base volume with the given snapshot. Note this can only be done
* when the snapshot and volume
* are
*
* @param name
* @return
*/
@Override
public ProviderVolume revert(ProviderAdapterContext context, ProviderAdapterDataObject snapshotDataObject) {
// public void promoteSnapshot(String namespace, String snapshotName) {
if (snapshotDataObject == null || snapshotDataObject.getExternalName() == null) {
throw new RuntimeException("Snapshot revert not possible as an external snapshot name was not provided");
}
FlashArrayVolume snapshot = this.getSnapshot(snapshotDataObject.getExternalName());
if (snapshot.getSource() == null) {
throw new CloudRuntimeException("Snapshot source was not available from the storage array");
}
String origVolumeName = snapshot.getSource().getName();
// now "create" a new volume with the snapshot volume as its source (basically a
// Flash array copy)
// and overwrite to true (volume already exists, we are recreating it)
FlashArrayVolume input = new FlashArrayVolume();
input.setExternalName(origVolumeName);
input.setAllocatedSizeBytes(roundUp512Boundary(snapshot.getAllocatedSizeInBytes()));
input.setSource(new FlashArrayVolumeSource(snapshot.getExternalName()));
POST("/volumes?names=" + origVolumeName + "&overwrite=true", input, null);
return this.getVolume(origVolumeName);
}
@Override
public ProviderSnapshot getSnapshot(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
FlashArrayList<FlashArrayVolume> list = GET(
"/volume-snapshots?names=" + dataObject.getExternalName(),
new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
return (FlashArrayVolume) getFlashArrayItem(list);
}
@Override
public ProviderVolume copy(ProviderAdapterContext context, ProviderAdapterDataObject sourceDataObject, ProviderAdapterDataObject destDataObject) {
// private ManagedVolume copy(ManagedVolume sourceVolume, String destNamespace,
// String destName) {
if (sourceDataObject == null || sourceDataObject.getExternalName() == null
||sourceDataObject.getType() == null) {
throw new RuntimeException("Provided volume has no external source information");
}
if (destDataObject == null) {
throw new RuntimeException("Provided volume target information was not provided");
}
if (destDataObject.getExternalName() == null) {
// this means its a new volume? so our external name will be the Cloudstack UUID
destDataObject
.setExternalName(ProviderVolumeNamer.generateObjectName(context, destDataObject));
}
FlashArrayVolume currentVol;
if (sourceDataObject.getType().equals(ProviderAdapterDataObject.Type.SNAPSHOT)) {
currentVol = getSnapshot(sourceDataObject.getExternalName());
} else {
currentVol = (FlashArrayVolume) this
.getFlashArrayItem(GET("/volumes?names=" + sourceDataObject.getExternalName(),
new TypeReference<FlashArrayList<FlashArrayVolume>>() {
}));
}
if (currentVol == null) {
throw new RuntimeException("Unable to find current volume to copy from");
}
// now "create" a new volume with the snapshot volume as its source (basically a
// Flash array copy)
// and overwrite to true (volume already exists, we are recreating it)
FlashArrayVolume payload = new FlashArrayVolume();
payload.setExternalName(normalizeName(pod, destDataObject.getExternalName()));
payload.setPodName(pod);
payload.setAllocatedSizeBytes(roundUp512Boundary(currentVol.getAllocatedSizeInBytes()));
payload.setSource(new FlashArrayVolumeSource(sourceDataObject.getExternalName()));
FlashArrayList<FlashArrayVolume> list = POST(
"/volumes?names=" + payload.getExternalName() + "&overwrite=true", payload,
new TypeReference<FlashArrayList<FlashArrayVolume>>() {
});
FlashArrayVolume outVolume = (FlashArrayVolume) getFlashArrayItem(list);
pause(postCopyWait);
return outVolume;
}
private void pause(long period) {
try {
Thread.sleep(period);
} catch (InterruptedException e) {
}
}
public boolean supportsSnapshotConnection() {
return false;
}
@Override
public void refresh(Map<String, String> details) {
this.connectionDetails = details;
this.refreshSession(true);
}
@Override
public void validate() {
login();
// check if hostgroup and pod from details really exist - we will
// require a distinct configuration object/connection object for each type
if (this.getHostgroup(hostgroup) == null) {
throw new RuntimeException("Hostgroup [" + hostgroup + "] not found in FlashArray at [" + url
+ "], please validate configuration");
}
if (this.getVolumeNamespace(pod) == null) {
throw new RuntimeException(
"Pod [" + pod + "] not found in FlashArray at [" + url + "], please validate configuration");
}
}
@Override
public void disconnect() {
return;
}
@Override
public ProviderVolumeStorageStats getManagedStorageStats() {
FlashArrayPod pod = getVolumeNamespace(this.pod);
// just in case
if (pod == null || pod.getFootprint() == 0) {
return null;
}
Long capacityBytes = pod.getQuotaLimit();
Long usedBytes = pod.getQuotaLimit() - (pod.getQuotaLimit() - pod.getFootprint());
ProviderVolumeStorageStats stats = new ProviderVolumeStorageStats();
stats.setCapacityInBytes(capacityBytes);
stats.setActualUsedInBytes(usedBytes);
return stats;
}
@Override
public ProviderVolumeStats getVolumeStats(ProviderAdapterContext context, ProviderAdapterDataObject dataObject) {
ProviderVolume vol = getVolume(dataObject.getExternalName());
Long usedBytes = vol.getUsedBytes();
Long allocatedSizeInBytes = vol.getAllocatedSizeInBytes();
if (usedBytes == null || allocatedSizeInBytes == null) {
return null;
}
ProviderVolumeStats stats = new ProviderVolumeStats();
stats.setAllocatedInBytes(allocatedSizeInBytes);
stats.setActualUsedInBytes(usedBytes);
return stats;
}
@Override
public boolean canAccessHost(ProviderAdapterContext context, String hostname) {
if (hostname == null) {
throw new RuntimeException("Unable to validate host access because a hostname was not provided");
}
List<String> members = getHostgroupMembers(hostgroup);
// check for fqdn and shortname combinations. this assumes there is at least a shortname match in both the storage array and cloudstack
// hostname configuration
String shortname;
if (hostname.indexOf('.') > 0) {
shortname = hostname.substring(0, (hostname.indexOf('.')));
} else {
shortname = hostname;
}
for (String member : members) {
// exact match (short or long names)
if (member.equals(hostname)) {
return true;
}
// primera has short name and cloudstack had long name
if (member.equals(shortname)) {
return true;
}
// member has long name but cloudstack had shortname
if (member.indexOf('.') > 0) {
if (member.substring(0, (member.indexOf('.'))).equals(shortname)) {
return true;
}
}
}
return false;
}
private String getAccessToken() {
refreshSession(false);
return accessToken;
}
private synchronized void refreshSession(boolean force) {
try {
if (force || keyExpiration < System.currentTimeMillis()) {
// close client to force connection reset on appliance -- not doing this can
// result in NotAuthorized error...guessing
_client.close();
;
_client = null;
login();
keyExpiration = System.currentTimeMillis() + keyTtl;
}
} catch (Exception e) {
// retry frequently but not every request to avoid DDOS on storage API
logger.warn("Failed to refresh FlashArray API key for " + username + "@" + url + ", will retry in 5 seconds",
e);
keyExpiration = System.currentTimeMillis() + (5 * 1000);
}
}
private void validateLoginInfo(String urlStr) {
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");
}
}
hostgroup = connectionDetails.get(FlashArrayAdapter.HOSTGROUP);
if (hostgroup == null) {
hostgroup = queryParms.get(FlashArrayAdapter.HOSTGROUP);
if (hostgroup == 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;
}
}
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;
}
}
/**
* 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);
validateLoginInfo(urlStr);
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()) {
DELETE("/connections?host_group_names=" + conn.getHostGroup().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 FlashArrayHostgroup getHostgroup(String name) {
FlashArrayList<FlashArrayHostgroup> list = GET("/host-groups?name=" + name,
new TypeReference<FlashArrayList<FlashArrayHostgroup>>() {
});
return (FlashArrayHostgroup) getFlashArrayItem(list);
}
private List<String> getHostgroupMembers(String groupname) {
FlashArrayGroupMemberReferenceList list = GET("/hosts/host-groups?group_names=" + groupname,
new TypeReference<FlashArrayGroupMemberReferenceList>() {
});
if (list == null || list.getItems().size() == 0) {
return null;
}
List<String> hostnames = new ArrayList<String>();
for (FlashArrayGroupMemberReference ref : list.getItems()) {
hostnames.add(ref.getMember().getName());
}
return hostnames;
}
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;
}
}