// 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 com.cloud.stack;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import org.apache.log4j.Logger;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.cloud.bridge.util.JsonAccessor;
/**
* CloudStackClient implements a simple CloudStack client object, it can be used to execute CloudStack commands
* with JSON response
*
*/
public class CloudStackClient {
protected final static Logger logger = Logger.getLogger(CloudStackClient.class);
private String _serviceUrl;
private long _pollIntervalMs = 2000; // 1 second polling interval
private long _pollTimeoutMs = 600000; // 10 minutes polling timeout
public CloudStackClient(String serviceRootUrl) {
assert (serviceRootUrl != null);
if (!serviceRootUrl.endsWith("/"))
_serviceUrl = serviceRootUrl + "/api?";
else
_serviceUrl = serviceRootUrl + "api?";
}
public CloudStackClient(String cloudStackServiceHost, int port, boolean bSslEnabled) {
StringBuffer sb = new StringBuffer();
if (!bSslEnabled) {
sb.append("http://" + cloudStackServiceHost);
if (port != 80)
sb.append(":").append(port);
} else {
sb.append("https://" + cloudStackServiceHost);
if (port != 443)
sb.append(":").append(port);
}
//
// If the CloudStack root context path has been from /client to some other name
// use the first constructor instead
//
sb.append("/client/api");
sb.append("?");
_serviceUrl = sb.toString();
}
public CloudStackClient setPollInterval(long intervalMs) {
_pollIntervalMs = intervalMs;
return this;
}
public CloudStackClient setPollTimeout(long pollTimeoutMs) {
_pollTimeoutMs = pollTimeoutMs;
return this;
}
public <T> T call(CloudStackCommand cmd, String apiKey, String secretKey, boolean followToAsyncResult, String responseName, String responseObjName,
Class<T> responseClz) throws Exception {
assert (responseName != null);
JsonAccessor json = execute(cmd, apiKey, secretKey);
if (followToAsyncResult && json.tryEval(responseName + ".jobid") != null) {
long startMs = System.currentTimeMillis();
while (System.currentTimeMillis() - startMs < _pollTimeoutMs) {
CloudStackCommand queryJobCmd = new CloudStackCommand("queryAsyncJobResult");
queryJobCmd.setParam("jobId", json.getAsString(responseName + ".jobid"));
JsonAccessor queryAsyncJobResponse = execute(queryJobCmd, apiKey, secretKey);
if (queryAsyncJobResponse.tryEval("queryasyncjobresultresponse") != null) {
int jobStatus = queryAsyncJobResponse.getAsInt("queryasyncjobresultresponse.jobstatus");
switch (jobStatus) {
case 2:
throw new Exception(queryAsyncJobResponse.getAsString("queryasyncjobresultresponse.jobresult.errortext") + " Error Code - " +
queryAsyncJobResponse.getAsString("queryasyncjobresultresponse.jobresult.errorcode"));
case 0:
try {
Thread.sleep(_pollIntervalMs);
} catch (Exception e) {
}
break;
case 1:
if (responseObjName != null)
return (T)(new Gson()).fromJson(queryAsyncJobResponse.eval("queryasyncjobresultresponse.jobresult." + responseObjName), responseClz);
else
return (T)(new Gson()).fromJson(queryAsyncJobResponse.eval("queryasyncjobresultresponse.jobresult"), responseClz);
default:
assert (false);
throw new Exception("Operation failed - invalid job status response");
}
} else {
throw new Exception("Operation failed - invalid JSON response");
}
}
throw new Exception("Operation failed - async-job query timed out");
} else {
if (responseObjName != null)
return (T)(new Gson()).fromJson(json.eval(responseName + "." + responseObjName), responseClz);
else
return (T)(new Gson()).fromJson(json.eval(responseName), responseClz);
}
}
// collectionType example : new TypeToken<List<String>>() {}.getType();
public <T> List<T> listCall(CloudStackCommand cmd, String apiKey, String secretKey, String responseName, String responseObjName, Type collectionType)
throws Exception {
assert (responseName != null);
JsonAccessor json = execute(cmd, apiKey, secretKey);
if (responseObjName != null)
try {
return (new Gson()).fromJson(json.eval(responseName + "." + responseObjName), collectionType);
} catch (Exception e) {
// this happens because responseObjName won't exist if there are no objects in the list.
logger.debug("CloudSatck API response doesn't contain responseObjName:" + responseObjName + " because response is empty");
return null;
}
return (new Gson()).fromJson(json.eval(responseName), collectionType);
}
public JsonAccessor execute(CloudStackCommand cmd, String apiKey, String secretKey) throws Exception {
JsonParser parser = new JsonParser();
URL url = new URL(_serviceUrl + cmd.signCommand(apiKey, secretKey));
if (logger.isDebugEnabled())
logger.debug("Cloud API call + [" + url.toString() + "]");
URLConnection connect = url.openConnection();
int statusCode;
statusCode = ((HttpURLConnection)connect).getResponseCode();
if (statusCode >= 400) {
logger.error("Cloud API call + [" + url.toString() + "] failed with status code: " + statusCode);
String errorMessage = ((HttpURLConnection)connect).getResponseMessage();
if (errorMessage == null) {
errorMessage = connect.getHeaderField("X-Description");
}
if (errorMessage == null) {
errorMessage = "CloudStack API call HTTP response error, HTTP status code: " + statusCode;
}
errorMessage = errorMessage.concat(" Error Code - " + Integer.toString(statusCode));
throw new IOException(errorMessage);
}
InputStream inputStream = connect.getInputStream();
JsonElement jsonElement = parser.parse(new InputStreamReader(inputStream));
if (jsonElement == null) {
logger.error("Cloud API call + [" + url.toString() + "] failed: unable to parse expected JSON response");
throw new IOException("CloudStack API call error : invalid JSON response");
}
if (logger.isDebugEnabled())
logger.debug("Cloud API call + [" + url.toString() + "] returned: " + jsonElement.toString());
return new JsonAccessor(jsonElement);
}
}