| /* | |
| * 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); | |
| } | |
| } |