/* * Copyright (C) 2011 Citrix Systems, Inc. All rights reserved. * * Licensed 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.cloud.bridge.util.JsonAccessor; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonParser; /** * CloudStackClient implements a simple CloudStack client object, it can be used to execute CloudStack commands * with JSON response * * @author Kelven Yang */ 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.errorcode") + " " + queryAsyncJobResponse.getAsString("queryasyncjobresultresponse.jobresult.errortext")); 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("Unable to find responseObjName:[" + responseObjName + "]. Returning null! Exception: " + e.getMessage()); 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; } 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); } }