Skip to content

Commit 4ac4d9c

Browse files
hsato03Henrique SatoBryanMLima
authored
API to validate Quota activation rule (#9605)
* API to validate Quota activation rule * Apply suggestions from code review Co-authored-by: Bryan Lima <[email protected]> * Use constants --------- Co-authored-by: Henrique Sato <[email protected]> Co-authored-by: Bryan Lima <[email protected]>
1 parent 9b6f9b5 commit 4ac4d9c

File tree

17 files changed

+993
-10
lines changed

17 files changed

+993
-10
lines changed

framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariables.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
package org.apache.cloudstack.quota.activationrule.presetvariables;
1919

20+
import java.util.List;
21+
2022
public class PresetVariables {
2123

2224
@PresetVariableDefinition(description = "Account owner of the resource.")
@@ -37,6 +39,9 @@ public class PresetVariables {
3739
@PresetVariableDefinition(description = "Zone where the resource is.")
3840
private GenericPresetVariable zone;
3941

42+
@PresetVariableDefinition(description = "A list containing the tariffs ordered by the field 'position'.")
43+
private List<Tariff> lastTariffs;
44+
4045
public Account getAccount() {
4146
return account;
4247
}
@@ -84,4 +89,12 @@ public GenericPresetVariable getZone() {
8489
public void setZone(GenericPresetVariable zone) {
8590
this.zone = zone;
8691
}
92+
93+
public List<Tariff> getLastTariffs() {
94+
return lastTariffs;
95+
}
96+
97+
public void setLastTariffs(List<Tariff> lastTariffs) {
98+
this.lastTariffs = lastTariffs;
99+
}
87100
}

framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import java.util.HashMap;
2121
import java.util.Map;
2222

23+
import com.cloud.utils.exception.CloudRuntimeException;
2324
import org.apache.cloudstack.usage.UsageTypes;
2425
import org.apache.cloudstack.usage.UsageUnitTypes;
2526
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
27+
import org.apache.commons.lang3.StringUtils;
2628

2729
public class QuotaTypes extends UsageTypes {
2830
private final Integer quotaType;
@@ -106,6 +108,20 @@ static public QuotaTypes getQuotaType(int quotaType) {
106108
return quotaTypeMap.get(quotaType);
107109
}
108110

111+
static public QuotaTypes getQuotaTypeByName(String name) {
112+
if (StringUtils.isBlank(name)) {
113+
throw new CloudRuntimeException("Could not retrieve Quota type by name because the value passed as parameter is null, empty, or blank.");
114+
}
115+
116+
for (QuotaTypes type : quotaTypeMap.values()) {
117+
if (type.getQuotaName().equals(name)) {
118+
return type;
119+
}
120+
}
121+
122+
throw new CloudRuntimeException(String.format("Could not find Quota type with name [%s].", name));
123+
}
124+
109125
@Override
110126
public String toString() {
111127
return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "quotaType", "quotaName");
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//Licensed to the Apache Software Foundation (ASF) under one
2+
//or more contributor license agreements. See the NOTICE file
3+
//distributed with this work for additional information
4+
//regarding copyright ownership. The ASF licenses this file
5+
//to you under the Apache License, Version 2.0 (the
6+
//"License"); you may not use this file except in compliance
7+
//with the License. You may obtain a copy of the License at
8+
//
9+
//http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
//Unless required by applicable law or agreed to in writing,
12+
//software distributed under the License is distributed on an
13+
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
//KIND, either express or implied. See the License for the
15+
//specific language governing permissions and limitations
16+
//under the License.
17+
package org.apache.cloudstack.api.command;
18+
19+
import com.cloud.exception.InvalidParameterValueException;
20+
import com.cloud.user.Account;
21+
import org.apache.cloudstack.api.APICommand;
22+
import org.apache.cloudstack.api.ApiConstants;
23+
import org.apache.cloudstack.api.BaseCmd;
24+
import org.apache.cloudstack.api.Parameter;
25+
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
26+
import org.apache.cloudstack.api.response.QuotaValidateActivationRuleResponse;
27+
import org.apache.cloudstack.quota.constant.QuotaTypes;
28+
29+
import javax.inject.Inject;
30+
31+
@APICommand(name = "quotaValidateActivationRule", responseObject = QuotaValidateActivationRuleResponse.class, description = "Validates if the given activation rule is valid for the informed usage type.", since = "4.20.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
32+
public class QuotaValidateActivationRuleCmd extends BaseCmd {
33+
34+
@Inject
35+
QuotaResponseBuilder responseBuilder;
36+
37+
@Parameter(name = ApiConstants.ACTIVATION_RULE, type = CommandType.STRING, required = true, description = "Quota tariff's activation rule to validate. The activation rule is valid if it has no syntax errors and all " +
38+
"variables are compatible with the given usage type.", length = 65535)
39+
private String activationRule;
40+
41+
@Parameter(name = ApiConstants.USAGE_TYPE, type = CommandType.INTEGER, required = true, description = "The Quota usage type used to validate the activation rule.")
42+
private Integer quotaType;
43+
44+
@Override
45+
public void execute() {
46+
QuotaValidateActivationRuleResponse response = responseBuilder.validateActivationRule(this);
47+
48+
response.setResponseName(getCommandName());
49+
setResponseObject(response);
50+
}
51+
52+
@Override
53+
public long getEntityOwnerId() {
54+
return Account.ACCOUNT_ID_SYSTEM;
55+
}
56+
57+
public String getActivationRule() {
58+
return activationRule;
59+
}
60+
61+
public QuotaTypes getQuotaType() {
62+
QuotaTypes quotaTypes = QuotaTypes.getQuotaType(quotaType);
63+
64+
if (quotaTypes == null) {
65+
throw new InvalidParameterValueException(String.format("Usage type not found for value [%s].", quotaType));
66+
}
67+
68+
return quotaTypes;
69+
}
70+
}

plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
2727
import org.apache.cloudstack.api.command.QuotaTariffListCmd;
2828
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
29+
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
2930
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
3031
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
3132
import org.apache.cloudstack.quota.vo.QuotaTariffVO;
@@ -88,4 +89,6 @@ public interface QuotaResponseBuilder {
8889
QuotaConfigureEmailResponse createQuotaConfigureEmailResponse(QuotaEmailConfigurationVO quotaEmailConfigurationVO, Double minBalance, long accountId);
8990

9091
List<QuotaConfigureEmailResponse> listEmailConfiguration(long accountId);
92+
93+
QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd);
9194
}

plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
//under the License.
1717
package org.apache.cloudstack.api.response;
1818

19+
import java.io.IOException;
1920
import java.lang.reflect.Field;
2021
import java.lang.reflect.Modifier;
2122
import java.lang.reflect.ParameterizedType;
@@ -34,12 +35,15 @@
3435
import java.util.Iterator;
3536
import java.util.List;
3637
import java.util.ListIterator;
38+
import java.util.Map;
39+
import java.util.Set;
3740
import java.util.function.Consumer;
3841
import java.util.stream.Collectors;
3942

4043
import javax.inject.Inject;
4144

4245
import com.cloud.utils.DateUtil;
46+
import com.cloud.utils.exception.CloudRuntimeException;
4347
import org.apache.cloudstack.api.ApiErrorCode;
4448
import org.apache.cloudstack.api.ServerApiException;
4549
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
@@ -51,8 +55,10 @@
5155
import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
5256
import org.apache.cloudstack.api.command.QuotaTariffListCmd;
5357
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
58+
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
5459
import org.apache.cloudstack.context.CallContext;
5560
import org.apache.cloudstack.discovery.ApiDiscoveryService;
61+
import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper;
5662
import org.apache.cloudstack.quota.QuotaManager;
5763
import org.apache.cloudstack.quota.QuotaManagerImpl;
5864
import org.apache.cloudstack.quota.QuotaService;
@@ -78,6 +84,7 @@
7884
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
7985
import org.apache.cloudstack.quota.vo.QuotaTariffVO;
8086
import org.apache.cloudstack.quota.vo.QuotaUsageVO;
87+
import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter;
8188
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
8289
import org.apache.commons.lang3.StringUtils;
8390
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -133,11 +140,13 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
133140
private QuotaManager _quotaManager;
134141
@Inject
135142
private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
143+
@Inject
144+
private JsInterpreterHelper jsInterpreterHelper;
145+
@Inject
146+
private ApiDiscoveryService apiDiscoveryService;
136147

137148
private final Class<?>[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class};
138149

139-
@Inject
140-
private ApiDiscoveryService apiDiscoveryService;
141150

142151
@Override
143152
public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) {
@@ -789,7 +798,7 @@ protected Class<?> getClassOfField(Field field){
789798
*/
790799
public void filterSupportedTypes(List<Pair<String, String>> variables, QuotaTypes quotaType, PresetVariableDefinition presetVariableDefinitionAnnotation, Class<?> fieldClass,
791800
String presetVariableName) {
792-
if (Arrays.stream(presetVariableDefinitionAnnotation.supportedTypes()).noneMatch(supportedType ->
801+
if (quotaType != null && Arrays.stream(presetVariableDefinitionAnnotation.supportedTypes()).noneMatch(supportedType ->
793802
supportedType == quotaType.getQuotaType() || supportedType == 0)) {
794803
return;
795804
}
@@ -928,4 +937,82 @@ protected QuotaConfigureEmailResponse createQuotaConfigureEmailResponse(QuotaEma
928937

929938
return quotaConfigureEmailResponse;
930939
}
940+
941+
@Override
942+
public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd) {
943+
String message;
944+
String activationRule = cmd.getActivationRule();
945+
QuotaTypes quotaType = cmd.getQuotaType();
946+
String quotaName = quotaType.getQuotaName();
947+
List<Pair<String, String>> usageTypeVariablesAndDescriptions = new ArrayList<>();
948+
949+
addAllPresetVariables(PresetVariables.class, quotaType, usageTypeVariablesAndDescriptions, null);
950+
List<String> usageTypeVariables = usageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList());
951+
952+
try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) {
953+
Map<String, String> newVariables = injectUsageTypeVariables(jsInterpreter, usageTypeVariables);
954+
String scriptToExecute = jsInterpreterHelper.replaceScriptVariables(activationRule, newVariables);
955+
jsInterpreter.executeScript(String.format("new Function(\"%s\")", scriptToExecute.replaceAll("\n", "")));
956+
} catch (IOException | CloudRuntimeException e) {
957+
logger.error("Unable to execute activation rule due to: [{}].", e.getMessage(), e);
958+
message = "Error while executing activation rule. Check if there are no syntax errors and all variables are compatible with the given usage type.";
959+
return createValidateActivationRuleResponse(activationRule, quotaName, false, message);
960+
}
961+
962+
Set<String> scriptVariables = jsInterpreterHelper.getScriptVariables(activationRule);
963+
if (isScriptVariablesValid(scriptVariables, usageTypeVariables)) {
964+
message = "The script has no syntax errors and all variables are compatible with the given usage type.";
965+
return createValidateActivationRuleResponse(activationRule, quotaName, true, message);
966+
}
967+
968+
message = "Found variables that are not compatible with the given usage type.";
969+
return createValidateActivationRuleResponse(activationRule, quotaName, false, message);
970+
}
971+
972+
/**
973+
* Checks whether script variables are compatible with the usage type. First, we remove all script variables that correspond to the script's usage type variables.
974+
* Then, returns true if none of the remaining script variables match any usage types variables, and false otherwise.
975+
*
976+
* @param scriptVariables Script variables.
977+
* @param scriptUsageTypeVariables Script usage type variables.
978+
* @return True if the script variables are valid, false otherwise.
979+
*/
980+
protected boolean isScriptVariablesValid(Set<String> scriptVariables, List<String> scriptUsageTypeVariables) {
981+
List<Pair<String, String>> allUsageTypeVariablesAndDescriptions = new ArrayList<>();
982+
addAllPresetVariables(PresetVariables.class, null, allUsageTypeVariablesAndDescriptions, null);
983+
List<String> allUsageTypesVariables = allUsageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList());
984+
985+
List<String> matchVariables = scriptVariables.stream().filter(scriptUsageTypeVariables::contains).collect(Collectors.toList());
986+
matchVariables.forEach(scriptVariables::remove);
987+
988+
return scriptVariables.stream().noneMatch(allUsageTypesVariables::contains);
989+
}
990+
991+
/**
992+
* Injects variables into JavaScript interpreter. It's necessary to remove all dots from the given variables, as the interpreter
993+
* does not interpret the variables as attributes of objects.
994+
*
995+
* @param jsInterpreter the {@link JsInterpreter} which the variables will be injected.
996+
* @param variables the {@link List} with variables to format and inject the formatted variables into interpreter.
997+
* @return A {@link Map} which has the key as the given variable and the value as the given variable formatted (without dots).
998+
*/
999+
protected Map<String, String> injectUsageTypeVariables(JsInterpreter jsInterpreter, List<String> variables) {
1000+
Map<String, String> formattedVariables = new HashMap<>();
1001+
for (String variable : variables) {
1002+
String formattedVariable = variable.replace(".", "");
1003+
formattedVariables.put(variable, formattedVariable);
1004+
jsInterpreter.injectVariable(formattedVariable, "false");
1005+
}
1006+
1007+
return formattedVariables;
1008+
}
1009+
1010+
public QuotaValidateActivationRuleResponse createValidateActivationRuleResponse(String activationRule, String quotaType, Boolean isValid, String message) {
1011+
QuotaValidateActivationRuleResponse response = new QuotaValidateActivationRuleResponse();
1012+
response.setActivationRule(activationRule);
1013+
response.setQuotaType(quotaType);
1014+
response.setValid(isValid);
1015+
response.setMessage(message);
1016+
return response;
1017+
}
9311018
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//Licensed to the Apache Software Foundation (ASF) under one
2+
//or more contributor license agreements. See the NOTICE file
3+
//distributed with this work for additional information
4+
//regarding copyright ownership. The ASF licenses this file
5+
//to you under the Apache License, Version 2.0 (the
6+
//"License"); you may not use this file except in compliance
7+
//with the License. You may obtain a copy of the License at
8+
//
9+
//http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
//Unless required by applicable law or agreed to in writing,
12+
//software distributed under the License is distributed on an
13+
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
//KIND, either express or implied. See the License for the
15+
//specific language governing permissions and limitations
16+
//under the License.
17+
package org.apache.cloudstack.api.response;
18+
19+
import com.cloud.serializer.Param;
20+
import com.google.gson.annotations.SerializedName;
21+
import org.apache.cloudstack.api.BaseResponse;
22+
23+
public class QuotaValidateActivationRuleResponse extends BaseResponse {
24+
25+
@SerializedName("activationrule")
26+
@Param(description = "The validated activation rule.")
27+
private String activationRule;
28+
29+
@SerializedName("quotatype")
30+
@Param(description = "The Quota usage type used to validate the activation rule.")
31+
private String quotaType;
32+
33+
@SerializedName("isvalid")
34+
@Param(description = "Whether the activation rule is valid.")
35+
private Boolean isValid;
36+
37+
@SerializedName("message")
38+
@Param(description = "The reason whether the activation rule is valid or not.")
39+
private String message;
40+
41+
public QuotaValidateActivationRuleResponse() {
42+
super("validactivationrule");
43+
}
44+
45+
public String getActivationRule() {
46+
return activationRule;
47+
}
48+
49+
public void setActivationRule(String activationRule) {
50+
this.activationRule = activationRule;
51+
}
52+
53+
public Boolean isValid() {
54+
return isValid;
55+
}
56+
57+
public void setValid(Boolean valid) {
58+
isValid = valid;
59+
}
60+
61+
public String getQuotaType() {
62+
return quotaType;
63+
}
64+
65+
public void setQuotaType(String quotaType) {
66+
this.quotaType = quotaType;
67+
}
68+
69+
public String getMessage() {
70+
return message;
71+
}
72+
73+
public void setMessage(String message) {
74+
this.message = message;
75+
}
76+
}

plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.apache.cloudstack.api.command.QuotaTariffListCmd;
4242
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
4343
import org.apache.cloudstack.api.command.QuotaUpdateCmd;
44+
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
4445
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
4546
import org.apache.cloudstack.context.CallContext;
4647
import org.apache.cloudstack.framework.config.ConfigKey;
@@ -121,6 +122,7 @@ public List<Class<?>> getCommands() {
121122
cmdList.add(QuotaConfigureEmailCmd.class);
122123
cmdList.add(QuotaListEmailConfigurationCmd.class);
123124
cmdList.add(QuotaPresetVariablesListCmd.class);
125+
cmdList.add(QuotaValidateActivationRuleCmd.class);
124126
return cmdList;
125127
}
126128

0 commit comments

Comments
 (0)