mirror of https://github.com/apache/cloudstack.git
Merge branch 'api_limit'
This commit is contained in:
commit
5a865462e9
|
|
@ -0,0 +1,43 @@
|
|||
// 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.exception;
|
||||
|
||||
import com.cloud.utils.SerialVersionUID;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown if number of requests is over api rate limit set.
|
||||
* @author minc
|
||||
*
|
||||
*/
|
||||
public class RequestLimitException extends CloudRuntimeException {
|
||||
|
||||
private static final long serialVersionUID = SerialVersionUID.AccountLimitException;
|
||||
|
||||
protected RequestLimitException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public RequestLimitException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public RequestLimitException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package org.apache.cloudstack.acl;
|
||||
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.exception.RequestLimitException;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.utils.component.Adapter;
|
||||
|
||||
|
|
@ -26,5 +27,5 @@ public interface APIChecker extends Adapter {
|
|||
// If true, apiChecker has checked the operation
|
||||
// If false, apiChecker is unable to handle the operation or not implemented
|
||||
// On exception, checkAccess failed don't allow
|
||||
boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException;
|
||||
boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException, RequestLimitException;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
// 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.acl;
|
||||
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.utils.component.Adapter;
|
||||
|
||||
/**
|
||||
* APILimitChecker checks if we should block an API request based on pre-set account based api limit.
|
||||
*/
|
||||
public interface APILimitChecker extends Adapter {
|
||||
// Interface for checking if the account is over its api limit
|
||||
void checkLimit(Account account) throws ServerApiException;
|
||||
}
|
||||
|
|
@ -30,6 +30,11 @@
|
|||
<artifactId>cloud-plugin-acl-static-role-based</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-api-limit-account-based</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-api-discovery</artifactId>
|
||||
|
|
|
|||
|
|
@ -509,3 +509,8 @@ configureSimulator=1
|
|||
#### api discovery commands
|
||||
|
||||
listApis=15
|
||||
|
||||
#### API Rate Limit service command
|
||||
|
||||
getApiLimit=15
|
||||
resetApiLimit=1
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ under the License.
|
|||
<param name="premium">true</param>
|
||||
</dao>
|
||||
<adapters key="org.apache.cloudstack.acl.APIChecker">
|
||||
<adapter name="AccountBasedAPIRateLimit" class="org.apache.cloudstack.ratelimit.ApiRateLimitServiceImpl" singleton="true">
|
||||
<param name="api.throttling.interval">1</param>
|
||||
<param name="api.throttling.max">25</param>
|
||||
<param name="api.throttling.cachesize">50000</param>
|
||||
</adapter>
|
||||
<adapter name="StaticRoleBasedAPIAccessChecker" class="org.apache.cloudstack.acl.StaticRoleBasedAPIAccessChecker"/>
|
||||
</adapters>
|
||||
<adapters key="com.cloud.agent.manager.allocator.HostAllocator">
|
||||
|
|
@ -233,6 +238,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.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"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cloud-plugin-api-limit-account-based</artifactId>
|
||||
<name>Apache CloudStack Plugin - API Rate Limit</name>
|
||||
<parent>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloudstack-plugins</artifactId>
|
||||
<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>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// 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.api.command.admin.ratelimit;
|
||||
|
||||
import org.apache.cloudstack.api.ACL;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.PlugService;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.AccountResponse;
|
||||
import org.apache.cloudstack.api.response.ApiLimitResponse;
|
||||
import org.apache.cloudstack.api.response.SuccessResponse;
|
||||
import org.apache.cloudstack.ratelimit.ApiRateLimitService;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.UserContext;
|
||||
|
||||
@APICommand(name = "resetApiLimit", responseObject=ApiLimitResponse.class, description="Reset api count")
|
||||
public class ResetApiLimitCmd extends BaseCmd {
|
||||
private static final Logger s_logger = Logger.getLogger(ResetApiLimitCmd.class.getName());
|
||||
|
||||
private static final String s_name = "resetapilimitresponse";
|
||||
|
||||
@PlugService
|
||||
ApiRateLimitService _apiLimitService;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@ACL
|
||||
@Parameter(name=ApiConstants.ACCOUNT, type=CommandType.UUID, entityType=AccountResponse.class,
|
||||
description="the ID of the acount whose limit to be reset")
|
||||
private Long accountId;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
|
||||
public Long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
|
||||
public void setAccountId(Long accountId) {
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
Account account = UserContext.current().getCaller();
|
||||
if (account != null) {
|
||||
return account.getId();
|
||||
}
|
||||
|
||||
return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(){
|
||||
boolean result = _apiLimitService.resetApiLimit(this.accountId);
|
||||
if (result) {
|
||||
SuccessResponse response = new SuccessResponse(getCommandName());
|
||||
this.setResponseObject(response);
|
||||
} else {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reset api limit counter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// 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.api.command.user.ratelimit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cloudstack.api.ACL;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.BaseListCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.PlugService;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.BaseCmd.CommandType;
|
||||
import org.apache.cloudstack.api.command.admin.ratelimit.ResetApiLimitCmd;
|
||||
import org.apache.cloudstack.api.response.AccountResponse;
|
||||
import org.apache.cloudstack.api.response.ApiLimitResponse;
|
||||
import org.apache.cloudstack.api.response.PhysicalNetworkResponse;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.ratelimit.ApiRateLimitService;
|
||||
import com.cloud.exception.ConcurrentOperationException;
|
||||
import com.cloud.exception.InsufficientCapacityException;
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.exception.ResourceUnavailableException;
|
||||
import com.cloud.user.Account;
|
||||
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 BaseCmd {
|
||||
private static final Logger s_logger = Logger.getLogger(GetApiLimitCmd.class.getName());
|
||||
|
||||
private static final String s_name = "getapilimitresponse";
|
||||
|
||||
@PlugService
|
||||
ApiRateLimitService _apiLimitService;
|
||||
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
Account account = UserContext.current().getCaller();
|
||||
if (account != null) {
|
||||
return account.getId();
|
||||
}
|
||||
|
||||
return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(){
|
||||
Account caller = UserContext.current().getCaller();
|
||||
ApiLimitResponse response = _apiLimitService.searchApiLimit(caller);
|
||||
response.setResponseName(getCommandName());
|
||||
response.setObjectName("apilimit");
|
||||
this.setResponseObject(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// 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.api.response;
|
||||
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
|
||||
|
||||
public class ApiLimitResponse extends BaseResponse {
|
||||
@SerializedName(ApiConstants.ACCOUNT_ID) @Param(description="the account uuid of the api remaining count")
|
||||
private String accountId;
|
||||
|
||||
@SerializedName(ApiConstants.ACCOUNT) @Param(description="the account name of the api remaining count")
|
||||
private String accountName;
|
||||
|
||||
@SerializedName("apiIssued") @Param(description="number of api already issued")
|
||||
private int apiIssued;
|
||||
|
||||
@SerializedName("apiAllowed") @Param(description="currently allowed number of apis")
|
||||
private int apiAllowed;
|
||||
|
||||
@SerializedName("expireAfter") @Param(description="seconds left to reset counters")
|
||||
private long expireAfter;
|
||||
|
||||
public void setAccountId(String accountId) {
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
public void setAccountName(String accountName) {
|
||||
this.accountName = accountName;
|
||||
}
|
||||
|
||||
public void setApiIssued(int apiIssued) {
|
||||
this.apiIssued = apiIssued;
|
||||
}
|
||||
|
||||
public void setApiAllowed(int apiAllowed) {
|
||||
this.apiAllowed = apiAllowed;
|
||||
}
|
||||
|
||||
public void setExpireAfter(long duration) {
|
||||
this.expireAfter = duration;
|
||||
}
|
||||
|
||||
public String getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
public int getApiIssued() {
|
||||
return apiIssued;
|
||||
}
|
||||
|
||||
public int getApiAllowed() {
|
||||
return apiAllowed;
|
||||
}
|
||||
|
||||
public long getExpireAfter() {
|
||||
return expireAfter;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
import org.apache.cloudstack.api.response.ApiLimitResponse;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.utils.component.PluggableService;
|
||||
|
||||
/**
|
||||
* Provide API rate limit service
|
||||
* @author minc
|
||||
*
|
||||
*/
|
||||
public interface ApiRateLimitService extends PluggableService{
|
||||
|
||||
public ApiLimitResponse searchApiLimit(Account caller);
|
||||
|
||||
public boolean resetApiLimit(Long accountId);
|
||||
|
||||
public void setTimeToLive(int timeToLive);
|
||||
|
||||
public void setMaxAllowed(int max);
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.ejb.Local;
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
import net.sf.ehcache.Cache;
|
||||
import net.sf.ehcache.CacheManager;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import org.apache.cloudstack.acl.APIChecker;
|
||||
import org.apache.cloudstack.api.command.admin.ratelimit.ResetApiLimitCmd;
|
||||
import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd;
|
||||
import org.apache.cloudstack.api.response.ApiLimitResponse;
|
||||
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.exception.RequestLimitException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.utils.component.AdapterBase;
|
||||
import com.cloud.utils.component.Inject;
|
||||
|
||||
@Local(value = APIChecker.class)
|
||||
public class ApiRateLimitServiceImpl extends AdapterBase implements APIChecker, ApiRateLimitService {
|
||||
private static final Logger s_logger = Logger.getLogger(ApiRateLimitServiceImpl.class);
|
||||
|
||||
/**
|
||||
* Fixed time duration where api rate limit is set, in seconds
|
||||
*/
|
||||
private int timeToLive = 1;
|
||||
|
||||
/**
|
||||
* Max number of api requests during timeToLive duration.
|
||||
*/
|
||||
private int maxAllowed = 30;
|
||||
|
||||
private LimitStore _store = null;
|
||||
|
||||
@Inject
|
||||
AccountService _accountService;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
|
||||
super.configure(name, params);
|
||||
|
||||
if (_store == null) {
|
||||
// not configured yet, note that since this class is both adapter
|
||||
// and pluggableService, so this method
|
||||
// may be invoked twice in ComponentLocator.
|
||||
// get global configured duration and max values
|
||||
Object duration = params.get("api.throttling.interval");
|
||||
if (duration != null) {
|
||||
timeToLive = Integer.parseInt((String) duration);
|
||||
}
|
||||
Object maxReqs = params.get("api.throttling.max");
|
||||
if (maxReqs != null) {
|
||||
maxAllowed = Integer.parseInt((String) maxReqs);
|
||||
}
|
||||
// create limit store
|
||||
EhcacheLimitStore cacheStore = new EhcacheLimitStore();
|
||||
int maxElements = 10000;
|
||||
Object cachesize = params.get("api.throttling.cachesize");
|
||||
if ( cachesize != null ){
|
||||
maxElements = Integer.parseInt((String)cachesize);
|
||||
}
|
||||
CacheManager cm = CacheManager.create();
|
||||
Cache cache = new Cache("api-limit-cache", maxElements, false, false, timeToLive, timeToLive);
|
||||
cm.addCache(cache);
|
||||
s_logger.info("Limit Cache created with timeToLive=" + timeToLive + ", maxAllowed=" + maxAllowed + ", maxElements=" + maxElements );
|
||||
cacheStore.setCache(cache);
|
||||
_store = cacheStore;
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public ApiLimitResponse searchApiLimit(Account caller) {
|
||||
ApiLimitResponse response = new ApiLimitResponse();
|
||||
response.setAccountId(caller.getUuid());
|
||||
response.setAccountName(caller.getAccountName());
|
||||
StoreEntry entry = _store.get(caller.getId());
|
||||
if (entry == null) {
|
||||
|
||||
/* Populate the entry, thus unlocking any underlying mutex */
|
||||
entry = _store.create(caller.getId(), timeToLive);
|
||||
response.setApiIssued(0);
|
||||
response.setApiAllowed(maxAllowed);
|
||||
response.setExpireAfter(timeToLive);
|
||||
}
|
||||
else{
|
||||
response.setApiIssued(entry.getCounter());
|
||||
response.setApiAllowed(maxAllowed - entry.getCounter());
|
||||
response.setExpireAfter(entry.getExpireDuration());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean resetApiLimit(Long accountId) {
|
||||
if ( accountId != null ){
|
||||
_store.create(accountId, timeToLive);
|
||||
}
|
||||
else{
|
||||
_store.resetCounters();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException, RequestLimitException {
|
||||
Long accountId = user.getAccountId();
|
||||
Account account = _accountService.getAccount(accountId);
|
||||
if ( _accountService.isRootAdmin(account.getType())){
|
||||
// no API throttling on root admin
|
||||
return true;
|
||||
}
|
||||
StoreEntry entry = _store.get(accountId);
|
||||
|
||||
if (entry == null) {
|
||||
|
||||
/* Populate the entry, thus unlocking any underlying mutex */
|
||||
entry = _store.create(accountId, timeToLive);
|
||||
}
|
||||
|
||||
/* Increment the client count and see whether we have hit the maximum allowed clients yet. */
|
||||
int current = entry.incrementAndGet();
|
||||
|
||||
if (current <= maxAllowed) {
|
||||
s_logger.trace("account (" + account.getAccountId() + "," + account.getAccountName() + ") has current count = " + current);
|
||||
return true;
|
||||
} else {
|
||||
long expireAfter = entry.getExpireDuration();
|
||||
// for this exception, we can just show the same message to user and admin users.
|
||||
String msg = "The given user has reached his/her account api limit, please retry after " + expireAfter + " ms.";
|
||||
s_logger.warn(msg);
|
||||
throw new RequestLimitException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getCommands() {
|
||||
List<Class<?>> cmdList = new ArrayList<Class<?>>();
|
||||
cmdList.add(ResetApiLimitCmd.class);
|
||||
cmdList.add(GetApiLimitCmd.class);
|
||||
return cmdList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setTimeToLive(int timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void setMaxAllowed(int max) {
|
||||
this.maxAllowed = max;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
import net.sf.ehcache.Ehcache;
|
||||
import net.sf.ehcache.Element;
|
||||
import net.sf.ehcache.constructs.blocking.BlockingCache;
|
||||
import net.sf.ehcache.constructs.blocking.LockTimeoutException;
|
||||
|
||||
/**
|
||||
* A Limit store implementation using Ehcache.
|
||||
* @author minc
|
||||
*
|
||||
*/
|
||||
public class EhcacheLimitStore implements LimitStore {
|
||||
|
||||
|
||||
private BlockingCache cache;
|
||||
|
||||
|
||||
public void setCache(Ehcache cache) {
|
||||
BlockingCache ref;
|
||||
|
||||
if (!(cache instanceof BlockingCache)) {
|
||||
ref = new BlockingCache(cache);
|
||||
cache.getCacheManager().replaceCacheWithDecoratedCache(cache, new BlockingCache(cache));
|
||||
} else {
|
||||
ref = (BlockingCache) cache;
|
||||
}
|
||||
|
||||
this.cache = ref;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public StoreEntry create(Long key, int timeToLive) {
|
||||
StoreEntryImpl result = new StoreEntryImpl(timeToLive);
|
||||
Element element = new Element(key, result);
|
||||
element.setTimeToLive(timeToLive);
|
||||
cache.put(element);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreEntry get(Long key) {
|
||||
|
||||
Element entry = null;
|
||||
|
||||
try {
|
||||
|
||||
/* This may block. */
|
||||
entry = cache.get(key);
|
||||
} catch (LockTimeoutException e) {
|
||||
throw new RuntimeException();
|
||||
} catch (RuntimeException e) {
|
||||
|
||||
/* Release the lock that may have been acquired. */
|
||||
cache.put(new Element(key, null));
|
||||
}
|
||||
|
||||
StoreEntry result = null;
|
||||
|
||||
if (entry != null) {
|
||||
|
||||
/*
|
||||
* We don't need to check isExpired() on the result, since ehcache takes care of expiring entries for us.
|
||||
* c.f. the get(Key) implementation in this class.
|
||||
*/
|
||||
result = (StoreEntry) entry.getObjectValue();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void resetCounters() {
|
||||
cache.removeAll();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
import com.cloud.user.Account;
|
||||
|
||||
/**
|
||||
* Interface to define how an api limit store should work.
|
||||
* @author minc
|
||||
*
|
||||
*/
|
||||
public interface LimitStore {
|
||||
|
||||
/**
|
||||
* Returns a store entry for the given account. A value of null means that there is no
|
||||
* such entry and the calling client must call create to avoid
|
||||
* other clients potentially being blocked without any hope of progressing. A non-null
|
||||
* entry means that it has not expired and can be used to determine whether the current client should be allowed to
|
||||
* proceed with the rate-limited action or not.
|
||||
*
|
||||
*/
|
||||
StoreEntry get(Long account);
|
||||
|
||||
/**
|
||||
* Creates a new store entry
|
||||
*
|
||||
* @param account
|
||||
* the user account, key to the store
|
||||
* @param timeToLiveInSecs
|
||||
* the positive time-to-live in seconds
|
||||
* @return a non-null entry
|
||||
*/
|
||||
StoreEntry create(Long account, int timeToLiveInSecs);
|
||||
|
||||
void resetCounters();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
/**
|
||||
* Interface for each entry in LimitStore.
|
||||
* @author minc
|
||||
*
|
||||
*/
|
||||
public interface StoreEntry {
|
||||
|
||||
int getCounter();
|
||||
|
||||
int incrementAndGet();
|
||||
|
||||
boolean isExpired();
|
||||
|
||||
long getExpireDuration(); /* seconds to reset counter */
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Implementation of limit store entry.
|
||||
* @author minc
|
||||
*
|
||||
*/
|
||||
public class StoreEntryImpl implements StoreEntry {
|
||||
|
||||
private final long expiry;
|
||||
|
||||
private final AtomicInteger counter;
|
||||
|
||||
StoreEntryImpl(int timeToLive) {
|
||||
this.expiry = System.currentTimeMillis() + timeToLive * 1000;
|
||||
this.counter = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() > expiry;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public long getExpireDuration() {
|
||||
if ( isExpired() )
|
||||
return 0; // already expired
|
||||
else {
|
||||
return expiry - System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int incrementAndGet() {
|
||||
return this.counter.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCounter(){
|
||||
return this.counter.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
// 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.ratelimit;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
import org.apache.cloudstack.api.response.ApiLimitResponse;
|
||||
import org.apache.cloudstack.ratelimit.ApiRateLimitServiceImpl;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.cloud.exception.RequestLimitException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.AccountVO;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserVO;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class ApiRateLimitTest {
|
||||
|
||||
static ApiRateLimitServiceImpl _limitService = new ApiRateLimitServiceImpl();
|
||||
static AccountService _accountService = mock(AccountService.class);
|
||||
private static long acctIdSeq = 5L;
|
||||
private static Account testAccount;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp() throws ConfigurationException {
|
||||
|
||||
_limitService.configure("ApiRateLimitTest", Collections.<String, Object> emptyMap());
|
||||
|
||||
_limitService._accountService = _accountService;
|
||||
|
||||
// Standard responses
|
||||
AccountVO acct = new AccountVO(acctIdSeq);
|
||||
acct.setType(Account.ACCOUNT_TYPE_NORMAL);
|
||||
acct.setAccountName("demo");
|
||||
testAccount = acct;
|
||||
|
||||
when(_accountService.getAccount(5L)).thenReturn(testAccount);
|
||||
when(_accountService.isRootAdmin(Account.ACCOUNT_TYPE_NORMAL)).thenReturn(false);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void testSetUp() {
|
||||
// reset counter for each test
|
||||
_limitService.resetApiLimit(null);
|
||||
}
|
||||
|
||||
private User createFakeUser(){
|
||||
UserVO user = new UserVO();
|
||||
user.setAccountId(acctIdSeq);
|
||||
return user;
|
||||
}
|
||||
|
||||
private boolean isUnderLimit(User key){
|
||||
try{
|
||||
_limitService.checkAccess(key, null);
|
||||
return true;
|
||||
}
|
||||
catch (RequestLimitException ex){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sequentialApiAccess() {
|
||||
int allowedRequests = 1;
|
||||
_limitService.setMaxAllowed(allowedRequests);
|
||||
_limitService.setTimeToLive(1);
|
||||
|
||||
User key = createFakeUser();
|
||||
assertTrue("Allow for the first request", isUnderLimit(key));
|
||||
|
||||
assertFalse("Second request should be blocked, since we assume that the two api "
|
||||
+ " accesses take less than a second to perform", isUnderLimit(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canDoReasonableNumberOfApiAccessPerSecond() throws Exception {
|
||||
int allowedRequests = 200;
|
||||
_limitService.setMaxAllowed(allowedRequests);
|
||||
_limitService.setTimeToLive(1);
|
||||
|
||||
User key = createFakeUser();
|
||||
|
||||
for (int i = 0; i < allowedRequests; i++) {
|
||||
assertTrue("We should allow " + allowedRequests + " requests per second, but failed at request " + i, isUnderLimit(key));
|
||||
}
|
||||
|
||||
|
||||
assertFalse("We should block >" + allowedRequests + " requests per second", isUnderLimit(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleClientsCanAccessWithoutBlocking() throws Exception {
|
||||
int allowedRequests = 200;
|
||||
_limitService.setMaxAllowed(allowedRequests);
|
||||
_limitService.setTimeToLive(1);
|
||||
|
||||
|
||||
final User key = createFakeUser();
|
||||
|
||||
int clientCount = allowedRequests;
|
||||
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();
|
||||
|
||||
isUsable[j] = isUnderLimit(key);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
endGate.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
|
||||
|
||||
for (Runnable runnable : clients) {
|
||||
executor.execute(runnable);
|
||||
}
|
||||
|
||||
startGate.countDown();
|
||||
|
||||
endGate.await();
|
||||
|
||||
for (boolean b : isUsable) {
|
||||
assertTrue("Concurrent client request should be allowed within limit", b);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expiryOfCounterIsSupported() throws Exception {
|
||||
int allowedRequests = 1;
|
||||
_limitService.setMaxAllowed(allowedRequests);
|
||||
_limitService.setTimeToLive(1);
|
||||
|
||||
User key = this.createFakeUser();
|
||||
|
||||
assertTrue("The first request should be allowed", isUnderLimit(key));
|
||||
|
||||
// Allow the token to expire
|
||||
Thread.sleep(1001);
|
||||
|
||||
assertTrue("Another request after interval should be allowed as well", isUnderLimit(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyResetCounters() throws Exception {
|
||||
int allowedRequests = 1;
|
||||
_limitService.setMaxAllowed(allowedRequests);
|
||||
_limitService.setTimeToLive(1);
|
||||
|
||||
User key = this.createFakeUser();
|
||||
|
||||
assertTrue("The first request should be allowed", isUnderLimit(key));
|
||||
|
||||
assertFalse("Another request should be blocked", isUnderLimit(key));
|
||||
|
||||
_limitService.resetApiLimit(key.getAccountId());
|
||||
|
||||
assertTrue("Another request should be allowed after reset counter", isUnderLimit(key));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void verifySearchCounter() throws Exception {
|
||||
int allowedRequests = 10;
|
||||
_limitService.setMaxAllowed(allowedRequests);
|
||||
_limitService.setTimeToLive(1);
|
||||
|
||||
User key = this.createFakeUser();
|
||||
|
||||
for ( int i = 0; i < 5; i++ ){
|
||||
assertTrue("Issued 5 requests", isUnderLimit(key));
|
||||
}
|
||||
|
||||
ApiLimitResponse response = _limitService.searchApiLimit(testAccount);
|
||||
assertEquals("apiIssued is incorrect", 5, response.getApiIssued());
|
||||
assertEquals("apiAllowed is incorrect", 5, response.getApiAllowed());
|
||||
assertTrue("expiredAfter is incorrect", response.getExpireAfter() < 1000);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
<testSourceDirectory>test</testSourceDirectory>
|
||||
</build>
|
||||
<modules>
|
||||
<module>api/rate-limit</module>
|
||||
<module>api/discovery</module>
|
||||
<module>acl/static-role-based</module>
|
||||
<module>deployment-planners/user-concentrated-pod</module>
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.cloud.utils.ReflectUtil;
|
||||
import org.apache.cloudstack.acl.APILimitChecker;
|
||||
import org.apache.cloudstack.acl.APIChecker;
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.*;
|
||||
|
|
@ -118,6 +119,7 @@ import com.cloud.exception.CloudAuthenticationException;
|
|||
import com.cloud.exception.InsufficientCapacityException;
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.exception.RequestLimitException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.exception.ResourceUnavailableException;
|
||||
import com.cloud.server.ManagementServer;
|
||||
|
|
@ -150,6 +152,8 @@ public class ApiServer implements HttpRequestHandler {
|
|||
@Inject private DomainManager _domainMgr = null;
|
||||
@Inject private AsyncJobManager _asyncMgr = null;
|
||||
|
||||
@Inject(adapter = APILimitChecker.class)
|
||||
protected Adapters<APILimitChecker> _apiLimitCheckers;
|
||||
@Inject(adapter = APIChecker.class)
|
||||
protected Adapters<APIChecker> _apiAccessCheckers;
|
||||
|
||||
|
|
@ -382,6 +386,7 @@ public class ApiServer implements HttpRequestHandler {
|
|||
if (UserContext.current().getCaller().getType() != Account.ACCOUNT_TYPE_ADMIN){
|
||||
// hide internal details to non-admin user for security reason
|
||||
errorMsg = BaseCmd.USER_ERROR_MESSAGE;
|
||||
|
||||
}
|
||||
throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, errorMsg, ex);
|
||||
}
|
||||
|
|
@ -585,6 +590,7 @@ public class ApiServer implements HttpRequestHandler {
|
|||
// if userId not null, that mean that user is logged in
|
||||
if (userId != null) {
|
||||
User user = ApiDBUtils.findUserById(userId);
|
||||
|
||||
try{
|
||||
checkCommandAvailable(user, commandName);
|
||||
}
|
||||
|
|
@ -592,6 +598,10 @@ public class ApiServer implements HttpRequestHandler {
|
|||
s_logger.debug("The given command:" + commandName + " does not exist or it is not available for user with id:" + userId);
|
||||
throw new ServerApiException(ApiErrorCode.UNSUPPORTED_ACTION_ERROR, "The given command does not exist or it is not available for user");
|
||||
}
|
||||
catch (RequestLimitException ex){
|
||||
s_logger.debug(ex.getMessage());
|
||||
throw new ServerApiException(ApiErrorCode.API_LIMIT_EXCEED, ex.getMessage());
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// check against every available command to see if the command exists or not
|
||||
|
|
@ -821,6 +831,7 @@ public class ApiServer implements HttpRequestHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void checkCommandAvailable(User user, String commandName) throws PermissionDeniedException {
|
||||
if (user == null) {
|
||||
throw new PermissionDeniedException("User is null for role based API access check for command" + commandName);
|
||||
|
|
|
|||
|
|
@ -304,13 +304,11 @@ public class ApiServlet extends HttpServlet {
|
|||
* key mechanism updateUserContext(params, session != null ? session.getId() : null);
|
||||
*/
|
||||
|
||||
auditTrailSb.insert(0,
|
||||
"(userId=" + UserContext.current().getCallerUserId() + " accountId=" + UserContext.current().getCaller().getId() + " sessionId=" + (session != null ? session.getId() : null)
|
||||
+ ")");
|
||||
auditTrailSb.insert(0, "(userId=" + UserContext.current().getCallerUserId() + " accountId="
|
||||
+ UserContext.current().getCaller().getId() + " sessionId=" + (session != null ? session.getId() : null) + ")");
|
||||
|
||||
String response = _apiServer.handleRequest(params, false, responseType, auditTrailSb);
|
||||
writeResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType);
|
||||
|
||||
} else {
|
||||
if (session != null) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -360,7 +360,6 @@ public enum Config {
|
|||
ConcurrentSnapshotsThresholdPerHost("Advanced", ManagementServer.class, Long.class, "concurrent.snapshots.threshold.perhost",
|
||||
null, "Limits number of snapshots that can be handled by the host concurrently; default is NULL - unlimited", null);
|
||||
|
||||
|
||||
private final String _category;
|
||||
private final Class<?> _componentClass;
|
||||
private final Class<?> _type;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,18 @@
|
|||
// under the License.
|
||||
package com.cloud.api;
|
||||
|
||||
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.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
|
||||
/**
|
||||
* Test fixture to do performance test for list command
|
||||
|
|
@ -163,6 +170,4 @@ public class ListPerfTest extends APITest {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public class CSExceptionErrorCode {
|
|||
ExceptionErrorCodeMap.put("com.cloud.exception.UnsupportedServiceException", 4390);
|
||||
ExceptionErrorCodeMap.put("com.cloud.exception.VirtualMachineMigrationException", 4395);
|
||||
ExceptionErrorCodeMap.put("com.cloud.async.AsyncCommandQueued", 4540);
|
||||
ExceptionErrorCodeMap.put("com.cloud.exception.RequestLimitException", 4545);
|
||||
|
||||
// Have a special error code for ServerApiException when it is
|
||||
// thrown in a standalone manner when failing to detect any of the above
|
||||
|
|
|
|||
Loading…
Reference in New Issue