blob: dbbfcfc592134828e00486efe0291efa2626b640 [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.primera;
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.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
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.ProviderVolume.AddressType;
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.ProviderAdapterDiskOffering.ProvisioningType;
import org.apache.http.Header;
import org.apache.http.client.config.RequestConfig;
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.HttpPost;
import org.apache.http.client.methods.HttpPut;
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.ssl.SSLContextBuilder;
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;
public class PrimeraAdapter implements ProviderAdapter {
protected Logger logger = LogManager.getLogger(getClass());
public static final String HOSTSET = "hostset";
public static final String CPG = "cpg";
public static final String SNAP_CPG = "snapCpg";
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 TASK_WAIT_TIMEOUT_MS = "taskWaitTimeoutMs";
private static final long KEY_TTL_DEFAULT = (1000 * 60 * 14);
private static final long CONNECT_TIMEOUT_MS_DEFAULT = 600000;
private static final long TASK_WAIT_TIMEOUT_MS_DEFAULT = 10 * 60 * 1000;
public static final long BYTES_IN_MiB = 1048576;
static final ObjectMapper mapper = new ObjectMapper();
public String cpg = null;
public String snapCpg = null;
public String hostset = null;
private String username;
private String password;
private String key;
private String url;
private long keyExpiration = -1;
private long keyTtl = KEY_TTL_DEFAULT;
private long connTimeout = CONNECT_TIMEOUT_MS_DEFAULT;
private long taskWaitTimeoutMs = TASK_WAIT_TIMEOUT_MS_DEFAULT;
private CloseableHttpClient _client = null;
private boolean skipTlsValidation;
private Map<String, String> connectionDetails = null;
public PrimeraAdapter(String url, Map<String, String> details) {
this.url = url;
this.connectionDetails = details;
login();
}
@Override
public void refresh(Map<String, String> details) {
this.connectionDetails = details;
this.refreshSession(true);
}
/**
* Validate that the hostgroup and pod from the details data exists. Each
* configuration object/connection needs a distinct set of these 2 things.
*/
@Override
public void validate() {
login();
if (this.getHostset(hostset) == null) {
throw new RuntimeException("Hostgroup [" + hostset + "] not found in FlashArray at [" + url
+ "], please validate configuration");
}
if (this.getCpg(cpg) == null) {
throw new RuntimeException(
"Pod [" + cpg + "] not found in FlashArray at [" + url + "], please validate configuration");
}
}
@Override
public void disconnect() {
return;
}
@Override
public ProviderVolume create(ProviderAdapterContext context, ProviderAdapterDataObject dataIn,
ProviderAdapterDiskOffering diskOffering, long sizeInBytes) {
PrimeraVolumeRequest request = new PrimeraVolumeRequest();
String externalName = ProviderVolumeNamer.generateObjectName(context, dataIn);
request.setName(externalName);
request.setCpg(cpg);
request.setSnapCPG(snapCpg);
if (sizeInBytes < BYTES_IN_MiB) {
request.setSizeMiB(1);
} else {
request.setSizeMiB(sizeInBytes/BYTES_IN_MiB);
}
// determine volume type based on offering
// THIN: tpvv=true, reduce=false
// SPARSE: tpvv=true, reduce=true
// THICK: tpvv=false, tpZeroFill=true (not supported)
if (diskOffering != null) {
if (diskOffering.getType() == ProvisioningType.THIN) {
request.setTpvv(true);
request.setReduce(false);
} else if (diskOffering.getType() == ProvisioningType.SPARSE) {
request.setTpvv(false);
request.setReduce(true);
} else if (diskOffering.getType() == ProvisioningType.FAT) {
throw new RuntimeException("This storage provider does not support FAT provisioned volumes");
}
// sets the amount of space allowed for snapshots as a % of the volumes size
if (diskOffering.getHypervisorSnapshotReserve() != null) {
request.setSsSpcAllocLimitPct(diskOffering.getHypervisorSnapshotReserve());
}
} else {
// default to deduplicated volume
request.setReduce(true);
request.setTpvv(false);
}
request.setComment(ProviderVolumeNamer.generateObjectComment(context, dataIn));
POST("/volumes", request, null);
dataIn.setExternalName(externalName);
ProviderVolume volume = getVolume(context, dataIn);
return volume;
}
@Override
public String attach(ProviderAdapterContext context, ProviderAdapterDataObject dataIn) {
assert dataIn.getExternalName() != null : "External name not provided internally on volume attach";
PrimeraHostset.PrimeraHostsetVLUNRequest request = new PrimeraHostset.PrimeraHostsetVLUNRequest();
request.setHostname("set:" + hostset);
request.setVolumeName(dataIn.getExternalName());
request.setAutoLun(true);
// auto-lun returned here: Location: /api/v1/vluns/test_vv02,252,mysystem,2:2:4
String location = POST("/vluns", request, new TypeReference<String>() {});
if (location == null) {
throw new RuntimeException("Attach volume failed with empty location response to vlun add command on storage provider");
}
String[] toks = location.split(",");
if (toks.length <2) {
throw new RuntimeException("Attach volume failed with invalid location response to vlun add command on storage provider. Provided location: " + location);
}
return toks[1];
}
@Override
public void detach(ProviderAdapterContext context, ProviderAdapterDataObject request) {
// we expect to only be attaching one hostset to the vluns, so on detach we'll
// remove ALL vluns we find.
assert request.getExternalName() != null : "External name not provided internally on volume detach";
removeAllVluns(request.getExternalName());
}
public void removeVlun(String name, Integer lunid, String hostString) {
// hostString can be a hostname OR "set:<hostsetname>". It is stored this way
// in the appliance and returned as the vlun's name/string.
DELETE("/vluns/" + name + "," + lunid + "," + hostString);
}
/**
* Removes all vluns - this should only be done when you are sure the volume is no longer in use
* @param name
*/
public void removeAllVluns(String name) {
PrimeraVlunList list = getVolumeHostsets(name);
if (list != null && list.getMembers() != null) {
for (PrimeraVlun vlun: list.getMembers()) {
removeVlun(vlun.getVolumeName(), vlun.getLun(), vlun.getHostname());
}
}
}
public PrimeraVlunList getVolumeHostsets(String name) {
String query = "%22volumeName%20EQ%20" + name + "%22";
return GET("/vluns?query=" + query, new TypeReference<PrimeraVlunList>() {});
}
@Override
public void delete(ProviderAdapterContext context, ProviderAdapterDataObject request) {
assert request.getExternalName() != null : "External name not provided internally on volume delete";
// first remove vluns (take volumes from vluns) from hostset
removeAllVluns(request.getExternalName());
DELETE("/volumes/" + request.getExternalName());
}
@Override
public ProviderVolume copy(ProviderAdapterContext context, ProviderAdapterDataObject sourceVolumeInfo,
ProviderAdapterDataObject targetVolumeInfo) {
PrimeraVolumeCopyRequest request = new PrimeraVolumeCopyRequest();
PrimeraVolumeCopyRequestParameters parms = new PrimeraVolumeCopyRequestParameters();
assert sourceVolumeInfo.getExternalName() != null: "External provider name not provided on copy request to Primera volume provider";
// if we have no external name, treat it as a new volume
if (targetVolumeInfo.getExternalName() == null) {
targetVolumeInfo.setExternalName(ProviderVolumeNamer.generateObjectName(context, targetVolumeInfo));
}
ProviderVolume sourceVolume = this.getVolume(context, sourceVolumeInfo);
if (sourceVolume == null) {
throw new RuntimeException("Source volume " + sourceVolumeInfo.getExternalUuid() + " with provider name " + sourceVolumeInfo.getExternalName() + " not found on storage provider");
}
ProviderVolume targetVolume = this.getVolume(context, targetVolumeInfo);
if (targetVolume == null) {
this.create(context, targetVolumeInfo, null, sourceVolume.getAllocatedSizeInBytes());
}
parms.setDestVolume(targetVolumeInfo.getExternalName());
parms.setOnline(false);
request.setParameters(parms);
PrimeraTaskReference taskref = POST("/volumes/" + sourceVolumeInfo.getExternalName(), request, new TypeReference<PrimeraTaskReference>() {});
if (taskref == null) {
throw new RuntimeException("Unable to retrieve task used to copy to newly created volume");
}
waitForTaskToComplete(taskref.getTaskid(), "copy volume " + sourceVolumeInfo.getExternalName() + " to " +
targetVolumeInfo.getExternalName(), taskWaitTimeoutMs);
return this.getVolume(context, targetVolumeInfo);
}
private void waitForTaskToComplete(String taskid, String taskDescription, Long timeoutMs) {
// first wait for task to complete
long taskWaitTimeout = System.currentTimeMillis() + timeoutMs;
boolean timedOut = true;
PrimeraTaskStatus status = null;
long starttime = System.currentTimeMillis();
while (System.currentTimeMillis() <= taskWaitTimeout) {
status = this.getTaskStatus(taskid);
if (status != null && status.isFinished()) {
timedOut = false;
if (!status.isSuccess()) {
throw new RuntimeException("Task " + taskDescription + " was cancelled. TaskID: " + status.getId() + "; Final Status: " + status.getStatusName());
}
break;
} else {
if (status != null) {
logger.info("Task " + taskDescription + " is still running. TaskID: " + status.getId() + "; Current Status: " + status.getStatusName());
}
// ugly...to keep from hot-polling API
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
}
}
if (timedOut) {
if (status != null) {
throw new RuntimeException("Task " + taskDescription + " timed out. TaskID: " + status.getId() + ", Last Known Status: " + status.getStatusName());
} else {
throw new RuntimeException("Task " + taskDescription + " timed out and a current status could not be retrieved from storage endpoint");
}
}
logger.info(taskDescription + " completed in " + ((System.currentTimeMillis() - starttime)/1000) + " seconds");
}
private PrimeraTaskStatus getTaskStatus(String taskid) {
return GET("/tasks/" + taskid + "?view=excludeDetail", new TypeReference<PrimeraTaskStatus>() {
});
}
@Override
public ProviderSnapshot snapshot(ProviderAdapterContext context, ProviderAdapterDataObject sourceVolume,
ProviderAdapterDataObject targetSnapshot) {
assert sourceVolume.getExternalName() != null : "External name not set";
PrimeraVolumeSnapshotRequest request = new PrimeraVolumeSnapshotRequest();
PrimeraVolumeSnapshotRequestParameters parms = new PrimeraVolumeSnapshotRequestParameters();
parms.setName(ProviderVolumeNamer.generateObjectName(context, targetSnapshot));
request.setParameters(parms);
POST("/volumes/" + sourceVolume.getExternalName(), request, null);
targetSnapshot.setExternalName(parms.getName());
return getSnapshot(context, targetSnapshot);
}
@Override
public ProviderVolume revert(ProviderAdapterContext context, ProviderAdapterDataObject dataIn) {
assert dataIn.getExternalName() != null: "External name not internally set for provided snapshot when requested storage provider to revert";
// first get original volume
PrimeraVolume snapVol = (PrimeraVolume)getVolume(context, dataIn);
assert snapVol != null: "Storage volume associated with snapshot externally named [" + dataIn.getExternalName() + "] not found";
assert snapVol.getParentId() != null: "Unable to determine parent volume/snapshot for snapshot named [" + dataIn.getExternalName() + "]";
PrimeraVolumeRevertSnapshotRequest request = new PrimeraVolumeRevertSnapshotRequest();
request.setOnline(true);
request.setPriority(2);
PrimeraTaskReference taskref = PUT("/volumes/" + dataIn.getExternalName(), request, new TypeReference<PrimeraTaskReference>() {});
if (taskref == null) {
throw new RuntimeException("Unable to retrieve task used to revert snapshot to base volume");
}
waitForTaskToComplete(taskref.getTaskid(), "revert snapshot " + dataIn.getExternalName(), taskWaitTimeoutMs);
return getVolumeById(context, snapVol.getParentId());
}
/**
* Resize the volume to the new size. For HPE Primera, the API takes the additional space to add to the volume
* so this method will first retrieve the current volume's size and subtract that from the new size provided
* before calling the API.
*
* This method uses option GROW_VOLUME=3 for the API at this URL:
* https://support.hpe.com/hpesc/public/docDisplay?docId=a00118636en_us&page=v25706371.html
*
*/
@Override
public void resize(ProviderAdapterContext context, ProviderAdapterDataObject request, long totalNewSizeInBytes) {
assert request.getExternalName() != null: "External name not internally set for provided volume when requesting resize of volume";
PrimeraVolume existingVolume = (PrimeraVolume) getVolume(context, request);
assert existingVolume != null: "Storage volume resize request not possible as existing volume not found for external provider name: " + request.getExternalName();
long existingSizeInBytes = existingVolume.getSizeMiB() * PrimeraAdapter.BYTES_IN_MiB;
assert existingSizeInBytes < totalNewSizeInBytes: "Existing volume size is larger than requested new size for volume resize request. The Primera storage system does not support truncating/shrinking volumes.";
long addOnSizeInBytes = totalNewSizeInBytes - existingSizeInBytes;
PrimeraVolume volume = new PrimeraVolume();
volume.setSizeMiB((int) (addOnSizeInBytes / PrimeraAdapter.BYTES_IN_MiB));
volume.setAction(3);
PUT("/volumes/" + request.getExternalName(), volume, null);
}
@Override
public ProviderVolume getVolume(ProviderAdapterContext context, ProviderAdapterDataObject request) {
String externalName;
// if the external name isn't provided, look for the derived contextual name. some failure scenarios
// may result in the volume for this context being created but a subsequent failure causing the external
// name to not be persisted for later use. This is true of template-type objects being cached on primary
// storage
if (request.getExternalName() == null) {
externalName = ProviderVolumeNamer.generateObjectName(context, request);
} else {
externalName = request.getExternalName();
}
return GET("/volumes/" + externalName, new TypeReference<PrimeraVolume>() {
});
}
private ProviderVolume getVolumeById(ProviderAdapterContext context, Integer id) {
String query = "%22id%20EQ%20" + id + "%22";
return GET("/volumes?query=" + query, new TypeReference<PrimeraVolume>() {});
}
@Override
public ProviderSnapshot getSnapshot(ProviderAdapterContext context, ProviderAdapterDataObject request) {
assert request.getExternalName() != null: "External name not provided internally when finding snapshot on storage provider";
return GET("/volumes/" + request.getExternalName(), new TypeReference<PrimeraVolume>() {
});
}
@Override
public ProviderVolume getVolumeByAddress(ProviderAdapterContext context, AddressType addressType, String address) {
assert address != null: "External volume address not provided";
assert AddressType.FIBERWWN.equals(addressType): "This volume provider currently does not support address type " + addressType.name();
String query = "%22wwn%20EQ%20" + address + "%22";
return GET("/volumes?query=" + query, new TypeReference<PrimeraVolume>() {});
}
@Override
public ProviderVolumeStorageStats getManagedStorageStats() {
PrimeraCpg cpgobj = getCpg(cpg);
// just in case
if (cpgobj == null || cpgobj.getTotalSpaceMiB() == 0) {
return null;
}
Long capacityBytes = 0L;
if (cpgobj.getsDGrowth() != null) {
capacityBytes = cpgobj.getsDGrowth().getLimitMiB() * PrimeraAdapter.BYTES_IN_MiB;
}
Long usedBytes = 0L;
if (cpgobj.getUsrUsage() != null) {
usedBytes = (cpgobj.getUsrUsage().getRawUsedMiB()) * PrimeraAdapter.BYTES_IN_MiB;
}
ProviderVolumeStorageStats stats = new ProviderVolumeStorageStats();
stats.setActualUsedInBytes(usedBytes);
stats.setCapacityInBytes(capacityBytes);
return stats;
}
@Override
public ProviderVolumeStats getVolumeStats(ProviderAdapterContext context, ProviderAdapterDataObject request) {
PrimeraVolume vol = (PrimeraVolume)getVolume(context, request);
if (vol == null || vol.getSizeMiB() == null || vol.getSizeMiB() == 0) {
return null;
}
Long virtualSizeInBytes = vol.getHostWriteMiB() * PrimeraAdapter.BYTES_IN_MiB;
Long allocatedSizeInBytes = vol.getSizeMiB() * PrimeraAdapter.BYTES_IN_MiB;
Long actualUsedInBytes = vol.getTotalUsedMiB() * PrimeraAdapter.BYTES_IN_MiB;
ProviderVolumeStats stats = new ProviderVolumeStats();
stats.setActualUsedInBytes(actualUsedInBytes);
stats.setAllocatedInBytes(allocatedSizeInBytes);
stats.setVirtualUsedInBytes(virtualSizeInBytes);
return stats;
}
@Override
public boolean canAccessHost(ProviderAdapterContext context, String hostname) {
PrimeraHostset hostset = getHostset(this.hostset);
List<String> members = hostset.getSetmembers();
// 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
int index = member.indexOf(".");
if (index > 0) {
if (member.substring(0, (member.indexOf('.'))).equals(shortname)) {
return true;
}
}
}
return false;
}
private PrimeraCpg getCpg(String name) {
return GET("/cpgs/" + name, new TypeReference<PrimeraCpg>() {
});
}
private PrimeraHostset getHostset(String name) {
return GET("/hostsets/" + name, new TypeReference<PrimeraHostset>() {
});
}
private String getSessionKey() {
refreshSession(false);
return key;
}
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 Primera 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));
}
}
}
cpg = connectionDetails.get(PrimeraAdapter.CPG);
if (cpg == null) {
cpg = queryParms.get(PrimeraAdapter.CPG);
if (cpg == null) {
throw new RuntimeException(
PrimeraAdapter.CPG + " paramater/option required to configure this storage pool");
}
}
snapCpg = connectionDetails.get(PrimeraAdapter.SNAP_CPG);
if (snapCpg == null) {
snapCpg = queryParms.get(PrimeraAdapter.SNAP_CPG);
if (snapCpg == null) {
// default to using same CPG as the volume
snapCpg = cpg;
}
}
hostset = connectionDetails.get(PrimeraAdapter.HOSTSET);
if (hostset == null) {
hostset = queryParms.get(PrimeraAdapter.HOSTSET);
if (hostset == null) {
throw new RuntimeException(
PrimeraAdapter.HOSTSET + " paramater/option required to configure this storage pool");
}
}
String connTimeoutStr = connectionDetails.get(PrimeraAdapter.CONNECT_TIMEOUT_MS);
if (connTimeoutStr == null) {
connTimeoutStr = queryParms.get(PrimeraAdapter.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(PrimeraAdapter.KEY_TTL);
if (keyTtlString == null) {
keyTtlString = queryParms.get(PrimeraAdapter.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 taskWaitTimeoutMsStr = connectionDetails.get(PrimeraAdapter.TASK_WAIT_TIMEOUT_MS);
if (taskWaitTimeoutMsStr == null) {
taskWaitTimeoutMsStr = queryParms.get(PrimeraAdapter.TASK_WAIT_TIMEOUT_MS);
if (taskWaitTimeoutMsStr == null) {
taskWaitTimeoutMs = PrimeraAdapter.TASK_WAIT_TIMEOUT_MS_DEFAULT;
} else {
try {
taskWaitTimeoutMs = Long.parseLong(taskWaitTimeoutMsStr);
} catch (NumberFormatException e) {
logger.warn(PrimeraAdapter.TASK_WAIT_TIMEOUT_MS + " property not set to a proper number, using default value");
}
}
}
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 + "/credentials");
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.setEntity(new StringEntity("{\"user\":\"" + username + "\", \"password\":\"" + password + "\"}"));
CloseableHttpClient client = getClient();
response = (CloseableHttpResponse) client.execute(request);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 | statusCode == 201) {
PrimeraKey keyobj = mapper.readValue(response.getEntity().getContent(), PrimeraKey.class);
key = keyobj.getKey();
} else if (statusCode == 401 || statusCode == 403) {
throw new RuntimeException("Authentication or Authorization to Primera [" + url + "] with user [" + username
+ "] failed, unable to retrieve session token");
} else {
throw new RuntimeException("Unexpected HTTP response code from Primera [" + url + "] - [" + statusCode
+ "] - " + response.getStatusLine().getReasonPhrase());
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error creating input for login, check username/password encoding");
} catch (UnsupportedOperationException e) {
throw new RuntimeException("Error processing login response from Primera [" + url + "]", e);
} catch (IOException e) {
throw new RuntimeException("Error sending login request to Primera [" + url + "]", e);
} finally {
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
logger.debug("Error closing response from login attempt to Primera", 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 RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
}
_client = HttpClients.custom()
.setDefaultRequestConfig(config)
.setSSLHostnameVerifier(verifier)
.setSSLContext(sslContext)
.build();
}
return _client;
}
@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 + path);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.addHeader("X-HP3PAR-WSAPI-SessionKey", getSessionKey());
try {
String data = mapper.writeValueAsString(input);
request.setEntity(new StringEntity(data));
logger.debug("POST data: " + request.getEntity());
} catch (UnsupportedEncodingException | JsonProcessingException e) {
throw new RuntimeException(
"Error processing request payload to [" + url + "] for path [" + path + "]", e);
}
CloseableHttpClient client = getClient();
try {
response = (CloseableHttpResponse) client
.execute(request);
} catch (IOException e) {
throw new RuntimeException("Error sending request to Primera [" + 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 if (type.getType().getTypeName().equals(PrimeraTaskReference.class.getName())) {
T obj = mapper.readValue(response.getEntity().getContent(), type);
PrimeraTaskReference taskref = (PrimeraTaskReference) obj;
taskref.setLocation(header.getValue());
return obj;
} else {
return mapper.readValue(response.getEntity().getContent(), type);
}
}
return null;
} catch (UnsupportedOperationException | IOException e) {
throw new RuntimeException("Error processing response from Primera [" + url + path + "]", e);
}
} else if (statusCode == 400) {
try {
Map<String, Object> payload = mapper.readValue(response.getEntity().getContent(),
new TypeReference<Map<String, Object>>() {
});
throw new RuntimeException("Invalid request error 400: " + payload);
} catch (UnsupportedOperationException | IOException e) {
throw new RuntimeException(
"Error processing bad request response from Primera [" + url + path + "]", e);
}
} else if (statusCode == 401 || statusCode == 403) {
throw new RuntimeException("Authentication or Authorization to Primera [" + 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 RuntimeException("Invalid request error " + statusCode + ": " + payload);
} catch (UnsupportedOperationException | IOException e) {
throw new RuntimeException("Unexpected HTTP response code from Primera 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 Primera API", e);
}
}
}
}
private <T> T PUT(String path, Object input, final TypeReference<T> type) {
CloseableHttpResponse response = null;
try {
this.refreshSession(false);
HttpPut request = new HttpPut(url + path);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.addHeader("X-HP3PAR-WSAPI-SessionKey", getSessionKey());
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 RuntimeException("Invalid request error 400: " + payload);
} else if (statusCode == 401 || statusCode == 403) {
throw new RuntimeException("Authentication or Authorization to Primera [" + 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 RuntimeException("Invalid request error from Primera on PUT [" + url + path + "]" + statusCode + ": "
+ response.getStatusLine().getReasonPhrase() + " - " + payload);
}
} catch (UnsupportedEncodingException | JsonProcessingException e) {
throw new RuntimeException(
"Error processing request payload to [" + url + "] for path [" + path + "]", e);
} catch (UnsupportedOperationException e) {
throw new RuntimeException("Error processing bad request response from Primera [" + url + "]",
e);
} catch (IOException e) {
throw new RuntimeException("Error sending request to Primera [" + url + "]", e);
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
logger.debug("Unexpected failure closing response to Primera API", e);
}
}
}
}
private <T> T GET(String path, final TypeReference<T> type) {
CloseableHttpResponse response = null;
try {
this.refreshSession(false);
HttpGet request = new HttpGet(url + path);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.addHeader("X-HP3PAR-WSAPI-SessionKey", getSessionKey());
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 RuntimeException("Error processing response from Primera [" + url + "]", e);
}
} else if (statusCode == 401 || statusCode == 403) {
throw new RuntimeException("Authentication or Authorization to Primera [" + url + "] with user [" + username
+ "] failed, unable to retrieve session token");
} else if (statusCode == 404) {
return null;
} else {
throw new RuntimeException("Unexpected HTTP response code from Primera on GET [" + url + path + "] - ["
+ statusCode + "] - " + response.getStatusLine().getReasonPhrase());
}
} catch (IOException e) {
throw new RuntimeException("Error sending request to Primera [" + url + "]", e);
} catch (UnsupportedOperationException e) {
throw new RuntimeException("Error processing response from Primera [" + url + "]", e);
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
logger.debug("Unexpected failure closing response to Primera API", e);
}
}
}
}
private void DELETE(String path) {
CloseableHttpResponse response = null;
try {
this.refreshSession(false);
HttpDelete request = new HttpDelete(url + path);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.addHeader("X-HP3PAR-WSAPI-SessionKey", getSessionKey());
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 RuntimeException("Authentication or Authorization to Primera [" + url + "] with user [" + username
+ "] failed, unable to retrieve session token");
} else if (statusCode == 409) {
throw new RuntimeException("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 RuntimeException("Unexpected HTTP response code from Primera on DELETE [" + url + path + "] - ["
+ statusCode + "] - " + response.getStatusLine().getReasonPhrase());
}
} catch (IOException e) {
throw new RuntimeException("Error sending request to Primera [" + url + "]", e);
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
logger.debug("Unexpected failure closing response to Primera API", e);
}
}
}
}
}