Fix some bugs and add java integration test for api rate limit plugin.

This commit is contained in:
Min Chen 2013-01-17 15:13:51 -08:00
parent c1a540c6bb
commit 86ada92ffa
8 changed files with 623 additions and 152 deletions

View File

@ -185,7 +185,7 @@ under the License.
<pluggableservice name="ApiDiscoveryService" key="org.apache.cloudstack.discovery.ApiDiscoveryService" class="org.apache.cloudstack.discovery.ApiDiscoveryServiceImpl"/>
<pluggableservice name="VirtualRouterElementService" key="com.cloud.network.element.VirtualRouterElementService" class="com.cloud.network.element.VirtualRouterElement"/>
<pluggableservice name="NiciraNvpElementService" key="com.cloud.network.element.NiciraNvpElementService" class="com.cloud.network.element.NiciraNvpElement"/>
<pluggableservice name="ApiRateLimitService" key="org.apache.cloudstack.api.ratelimit.ApiRateLimitService" class="org.apache.cloudstack.ratelimit.ApiRateLimitServiceImpl"/>
<pluggableservice name="ApiRateLimitService" key="org.apache.cloudstack.ratelimit.ApiRateLimitService" class="org.apache.cloudstack.ratelimit.ApiRateLimitServiceImpl"/>
<dao name="OvsTunnelInterfaceDao" class="com.cloud.network.ovs.dao.OvsTunnelInterfaceDaoImpl" singleton="false"/>
<dao name="OvsTunnelAccountDao" class="com.cloud.network.ovs.dao.OvsTunnelNetworkDaoImpl" singleton="false"/>
<dao name="NiciraNvpDao" class="com.cloud.network.dao.NiciraNvpDaoImpl" singleton="false"/>

View File

@ -26,4 +26,26 @@
<version>4.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<build>
<defaultGoal>install</defaultGoal>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<testResources>
<testResource>
<directory>test/resources</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Xmx1024m</argLine>
<excludes>
<exclude>org/apache/cloudstack/ratelimit/integration/*</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -46,7 +46,7 @@ import com.cloud.user.UserContext;
import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "getApiLimit", responseObject=ApiLimitResponse.class, description="Get API limit count for the caller")
public class GetApiLimitCmd extends BaseListCmd {
public class GetApiLimitCmd extends BaseCmd {
private static final Logger s_logger = Logger.getLogger(GetApiLimitCmd.class.getName());
private static final String s_name = "getapilimitresponse";
@ -81,6 +81,7 @@ public class GetApiLimitCmd extends BaseListCmd {
Account caller = UserContext.current().getCaller();
ApiLimitResponse response = _apiLimitService.searchApiLimit(caller);
response.setResponseName(getCommandName());
response.setObjectName("apilimit");
this.setResponseObject(response);
}
}

View File

@ -0,0 +1,211 @@
// 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
// 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.ratelimit.integration;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import org.apache.cloudstack.api.response.SuccessResponse;
import com.cloud.api.ApiGsonHelper;
import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.Gson;
/**
* Base class for API Test
*
* @author Min Chen
*
*/
public abstract class APITest {
protected String rootUrl = "http://localhost:8080/client/api";
protected String sessionKey = null;
protected String cookieToSent = null;
/**
* Sending an api request through Http GET
* @param command command name
* @param params command query parameters in a HashMap
* @return http request response string
*/
protected String sendRequest(String command, HashMap<String, String> params){
try {
// Construct query string
StringBuilder sBuilder = new StringBuilder();
sBuilder.append("command=");
sBuilder.append(command);
if ( params != null && params.size() > 0){
Iterator<String> keys = params.keySet().iterator();
while (keys.hasNext()){
String key = keys.next();
sBuilder.append("&");
sBuilder.append(key);
sBuilder.append("=");
sBuilder.append(URLEncoder.encode(params.get(key), "UTF-8"));
}
}
// Construct request url
String reqUrl = rootUrl + "?" + sBuilder.toString();
// Send Http GET request
URL url = new URL(reqUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
if ( !command.equals("login") && cookieToSent != null){
// add the cookie to a request
conn.setRequestProperty("Cookie", cookieToSent);
}
conn.connect();
if ( command.equals("login")){
// if it is login call, store cookie
String headerName=null;
for (int i=1; (headerName = conn.getHeaderFieldKey(i))!=null; i++) {
if (headerName.equals("Set-Cookie")) {
String cookie = conn.getHeaderField(i);
cookie = cookie.substring(0, cookie.indexOf(";"));
String cookieName = cookie.substring(0, cookie.indexOf("="));
String cookieValue = cookie.substring(cookie.indexOf("=") + 1, cookie.length());
cookieToSent = cookieName + "=" + cookieValue;
}
}
}
// Get the response
StringBuilder response = new StringBuilder();
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
try {
while ((line = rd.readLine()) != null) {
response.append(line);
}
} catch (EOFException ex) {
// ignore this exception
System.out.println("EOF exception due to java bug");
}
rd.close();
return response.toString();
} catch (Exception e) {
throw new CloudRuntimeException("Problem with sending api request", e);
}
}
protected String createMD5String(String password) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new CloudRuntimeException("Error", e);
}
md5.reset();
BigInteger pwInt = new BigInteger(1, md5.digest(password.getBytes()));
// make sure our MD5 hash value is 32 digits long...
StringBuffer sb = new StringBuffer();
String pwStr = pwInt.toString(16);
int padding = 32 - pwStr.length();
for (int i = 0; i < padding; i++) {
sb.append('0');
}
sb.append(pwStr);
return sb.toString();
}
protected Object fromSerializedString(String result, Class<?> repCls) {
try {
if (result != null && !result.isEmpty()) {
// get real content
int start;
int end;
if (repCls == LoginResponse.class || repCls == SuccessResponse.class) {
start = result.indexOf('{', result.indexOf('{') + 1); // find
// the
// second
// {
end = result.lastIndexOf('}', result.lastIndexOf('}') - 1); // find
// the
// second
// }
// backwards
} else {
// get real content
start = result.indexOf('{', result.indexOf('{', result.indexOf('{') + 1) + 1); // find
// the
// third
// {
end = result.lastIndexOf('}', result.lastIndexOf('}', result.lastIndexOf('}') - 1) - 1); // find
// the
// third
// }
// backwards
}
if (start < 0 || end < 0) {
throw new CloudRuntimeException("Response format is wrong: " + result);
}
String content = result.substring(start, end + 1);
Gson gson = ApiGsonHelper.getBuilder().create();
return gson.fromJson(content, repCls);
}
return null;
} catch (RuntimeException e) {
throw new CloudRuntimeException("Caught runtime exception when doing GSON deserialization on: " + result, e);
}
}
/**
* Login call
* @param username user name
* @param password password (plain password, we will do MD5 hash here for you)
* @return login response string
*/
protected void login(String username, String password)
{
//String md5Psw = createMD5String(password);
// send login request
HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("username", username);
params.put("password", password);
String result = this.sendRequest("login", params);
LoginResponse loginResp = (LoginResponse)fromSerializedString(result, LoginResponse.class);
sessionKey = loginResp.getSessionkey();
}
}

View File

@ -0,0 +1,142 @@
// 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
// 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.ratelimit.integration;
import org.apache.cloudstack.api.BaseResponse;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
/**
* Login Response object
*
* @author Min Chen
*
*/
public class LoginResponse extends BaseResponse {
@SerializedName("timeout")
@Param(description = "session timeout period")
private String timeout;
@SerializedName("sessionkey")
@Param(description = "login session key")
private String sessionkey;
@SerializedName("username")
@Param(description = "login username")
private String username;
@SerializedName("userid")
@Param(description = "login user internal uuid")
private String userid;
@SerializedName("firstname")
@Param(description = "login user firstname")
private String firstname;
@SerializedName("lastname")
@Param(description = "login user lastname")
private String lastname;
@SerializedName("account")
@Param(description = "login user account type")
private String account;
@SerializedName("domainid")
@Param(description = "login user domain id")
private String domainid;
@SerializedName("type")
@Param(description = "login user type")
private int type;
public String getTimeout() {
return timeout;
}
public void setTimeout(String timeout) {
this.timeout = timeout;
}
public String getSessionkey() {
return sessionkey;
}
public void setSessionkey(String sessionkey) {
this.sessionkey = sessionkey;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getDomainid() {
return domainid;
}
public void setDomainid(String domainid) {
this.domainid = domainid;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}

View File

@ -0,0 +1,214 @@
// 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
// 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.ratelimit.integration;
import static org.junit.Assert.*;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.junit.Before;
import org.junit.Test;
import com.cloud.utils.exception.CloudRuntimeException;
/**
* Test fixture to do integration rate limit test.
* Currently we commented out this test suite since it requires a real MS and Db running.
*
* @author Min Chen
*
*/
public class RateLimitIntegrationTest extends APITest {
private static int apiMax = 25; // assuming ApiRateLimitService set api.throttling.max = 25
@Before
public void setup(){
// always reset count for each testcase
login("admin", "password");
// issue reset api limit calls
final HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("sessionkey", sessionKey);
String resetResult = sendRequest("resetApiLimit", params);
assertNotNull("Reset count failed!", fromSerializedString(resetResult, SuccessResponse.class));
}
@Test
public void testNoApiLimitOnRootAdmin() throws Exception {
// issue list Accounts calls
final HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("listAll", "true");
params.put("sessionkey", sessionKey);
// assuming ApiRateLimitService set api.throttling.max = 25
int clientCount = 26;
Runnable[] clients = new Runnable[clientCount];
final boolean[] isUsable = new boolean[clientCount];
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(clientCount);
for (int i = 0; i < isUsable.length; ++i) {
final int j = i;
clients[j] = new Runnable() {
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
startGate.await();
sendRequest("listAccounts", params);
isUsable[j] = true;
} catch (CloudRuntimeException e){
isUsable[j] = false;
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
}
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
for (Runnable runnable : clients) {
executor.execute(runnable);
}
startGate.countDown();
endGate.await();
int rejectCount = 0;
for ( int i = 0; i < isUsable.length; ++i){
if ( !isUsable[i])
rejectCount++;
}
assertEquals("No request should be rejected!", 0, rejectCount);
}
@Test
public void testApiLimitOnUser() throws Exception {
// log in using normal user
login("demo", "password");
// issue list Accounts calls
final HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("listAll", "true");
params.put("sessionkey", sessionKey);
int clientCount = apiMax + 1;
Runnable[] clients = new Runnable[clientCount];
final boolean[] isUsable = new boolean[clientCount];
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(clientCount);
for (int i = 0; i < isUsable.length; ++i) {
final int j = i;
clients[j] = new Runnable() {
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
startGate.await();
sendRequest("listAccounts", params);
isUsable[j] = true;
} catch (CloudRuntimeException e){
isUsable[j] = false;
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
}
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
for (Runnable runnable : clients) {
executor.execute(runnable);
}
startGate.countDown();
endGate.await();
int rejectCount = 0;
for ( int i = 0; i < isUsable.length; ++i){
if ( !isUsable[i])
rejectCount++;
}
assertEquals("Only one request should be rejected!", 1, rejectCount);
}
@Test
public void testGetApiLimitOnUser() throws Exception {
// log in using normal user
login("demo", "password");
// issue an api call
HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("listAll", "true");
params.put("sessionkey", sessionKey);
sendRequest("listAccounts", params);
// issue get api limit calls
final HashMap<String, String> params2 = new HashMap<String, String>();
params2.put("response", "json");
params2.put("sessionkey", sessionKey);
String getResult = sendRequest("getApiLimit", params2);
ApiLimitResponse getLimitResp = (ApiLimitResponse)fromSerializedString(getResult, ApiLimitResponse.class);
assertEquals("Issued api count is incorrect!", 2, getLimitResp.getApiIssued() ); // should be 2 apis issues plus this getlimit api
assertEquals("Allowed api count is incorrect!", apiMax -2, getLimitResp.getApiAllowed());
}
}

View File

@ -19,17 +19,17 @@ package com.cloud.api;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import org.apache.cloudstack.api.response.SuccessResponse;
import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.Gson;
@ -147,17 +147,38 @@ public abstract class APITest {
protected Object fromSerializedString(String result, Class<?> repCls) {
try {
if (result != null && !result.isEmpty()) {
// get real content
int start = result.indexOf('{', result.indexOf('{') + 1); // find the second {
if ( start < 0 ){
int start;
int end;
if (repCls == LoginResponse.class || repCls == SuccessResponse.class) {
start = result.indexOf('{', result.indexOf('{') + 1); // find
// the
// second
// {
end = result.lastIndexOf('}', result.lastIndexOf('}') - 1); // find
// the
// second
// }
// backwards
} else {
// get real content
start = result.indexOf('{', result.indexOf('{', result.indexOf('{') + 1) + 1); // find
// the
// third
// {
end = result.lastIndexOf('}', result.lastIndexOf('}', result.lastIndexOf('}') - 1) - 1); // find
// the
// third
// }
// backwards
}
if (start < 0 || end < 0) {
throw new CloudRuntimeException("Response format is wrong: " + result);
}
int end = result.lastIndexOf('}', result.lastIndexOf('}')-1); // find the second } backwards
if ( end < 0 ){
throw new CloudRuntimeException("Response format is wrong: " + result);
}
String content = result.substring(start, end+1);
String content = result.substring(start, end + 1);
Gson gson = ApiGsonHelper.getBuilder().create();
return gson.fromJson(content, repCls);
}

View File

@ -170,144 +170,4 @@ public class ListPerfTest extends APITest {
}
@Test
public void testNoApiLimitOnRootAdmin() throws Exception {
// issue list Accounts calls
final HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("listAll", "true");
params.put("sessionkey", sessionKey);
// assuming ApiRateLimitService set api.throttling.max = 25
int clientCount = 26;
Runnable[] clients = new Runnable[clientCount];
final boolean[] isUsable = new boolean[clientCount];
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(clientCount);
for (int i = 0; i < isUsable.length; ++i) {
final int j = i;
clients[j] = new Runnable() {
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
startGate.await();
sendRequest("listAccounts", params);
isUsable[j] = true;
} catch (CloudRuntimeException e){
isUsable[j] = false;
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
}
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
for (Runnable runnable : clients) {
executor.execute(runnable);
}
startGate.countDown();
endGate.await();
int rejectCount = 0;
for ( int i = 0; i < isUsable.length; ++i){
if ( !isUsable[i])
rejectCount++;
}
assertEquals("No request should be rejected!", 0, rejectCount);
}
@Test
public void testApiLimitOnUser() throws Exception {
// log in using normal user
login("demo", "password");
// issue list Accounts calls
final HashMap<String, String> params = new HashMap<String, String>();
params.put("response", "json");
params.put("listAll", "true");
params.put("sessionkey", sessionKey);
// assuming ApiRateLimitService set api.throttling.max = 25
int clientCount = 26;
Runnable[] clients = new Runnable[clientCount];
final boolean[] isUsable = new boolean[clientCount];
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(clientCount);
for (int i = 0; i < isUsable.length; ++i) {
final int j = i;
clients[j] = new Runnable() {
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
startGate.await();
sendRequest("listAccounts", params);
isUsable[j] = true;
} catch (CloudRuntimeException e){
isUsable[j] = false;
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
}
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
for (Runnable runnable : clients) {
executor.execute(runnable);
}
startGate.countDown();
endGate.await();
int rejectCount = 0;
for ( int i = 0; i < isUsable.length; ++i){
if ( !isUsable[i])
rejectCount++;
}
assertEquals("Only one request should be rejected!", 1, rejectCount);
// issue get api limit calls
final HashMap<String, String> params2 = new HashMap<String, String>();
params2.put("response", "json");
params2.put("sessionkey", sessionKey);
String getResult = sendRequest("getApiLimit", params2);
//ApiLimitResponse loginResp = (ApiLimitResponse)fromSerializedString(getResult, ApiLimitResponse.class);
}
}