API to validate Quota activation rule (#9605)

* API to validate Quota activation rule

* Apply suggestions from code review

Co-authored-by: Bryan Lima <42067040+BryanMLima@users.noreply.github.com>

* Use constants

---------

Co-authored-by: Henrique Sato <henrique.sato@scclouds.com.br>
Co-authored-by: Bryan Lima <42067040+BryanMLima@users.noreply.github.com>
This commit is contained in:
Henrique Sato 2024-12-03 14:41:25 -03:00 committed by GitHub
parent 9b6f9b5f7d
commit 4ac4d9cf29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 993 additions and 10 deletions

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.quota.activationrule.presetvariables; package org.apache.cloudstack.quota.activationrule.presetvariables;
import java.util.List;
public class PresetVariables { public class PresetVariables {
@PresetVariableDefinition(description = "Account owner of the resource.") @PresetVariableDefinition(description = "Account owner of the resource.")
@ -37,6 +39,9 @@ public class PresetVariables {
@PresetVariableDefinition(description = "Zone where the resource is.") @PresetVariableDefinition(description = "Zone where the resource is.")
private GenericPresetVariable zone; private GenericPresetVariable zone;
@PresetVariableDefinition(description = "A list containing the tariffs ordered by the field 'position'.")
private List<Tariff> lastTariffs;
public Account getAccount() { public Account getAccount() {
return account; return account;
} }
@ -84,4 +89,12 @@ public class PresetVariables {
public void setZone(GenericPresetVariable zone) { public void setZone(GenericPresetVariable zone) {
this.zone = zone; this.zone = zone;
} }
public List<Tariff> getLastTariffs() {
return lastTariffs;
}
public void setLastTariffs(List<Tariff> lastTariffs) {
this.lastTariffs = lastTariffs;
}
} }

View File

@ -20,9 +20,11 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.usage.UsageTypes;
import org.apache.cloudstack.usage.UsageUnitTypes; import org.apache.cloudstack.usage.UsageUnitTypes;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.commons.lang3.StringUtils;
public class QuotaTypes extends UsageTypes { public class QuotaTypes extends UsageTypes {
private final Integer quotaType; private final Integer quotaType;
@ -106,6 +108,20 @@ public class QuotaTypes extends UsageTypes {
return quotaTypeMap.get(quotaType); return quotaTypeMap.get(quotaType);
} }
static public QuotaTypes getQuotaTypeByName(String name) {
if (StringUtils.isBlank(name)) {
throw new CloudRuntimeException("Could not retrieve Quota type by name because the value passed as parameter is null, empty, or blank.");
}
for (QuotaTypes type : quotaTypeMap.values()) {
if (type.getQuotaName().equals(name)) {
return type;
}
}
throw new CloudRuntimeException(String.format("Could not find Quota type with name [%s].", name));
}
@Override @Override
public String toString() { public String toString() {
return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "quotaType", "quotaName"); return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "quotaType", "quotaName");

View File

@ -0,0 +1,70 @@
//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;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.Account;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.api.response.QuotaValidateActivationRuleResponse;
import org.apache.cloudstack.quota.constant.QuotaTypes;
import javax.inject.Inject;
@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)
public class QuotaValidateActivationRuleCmd extends BaseCmd {
@Inject
QuotaResponseBuilder responseBuilder;
@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 " +
"variables are compatible with the given usage type.", length = 65535)
private String activationRule;
@Parameter(name = ApiConstants.USAGE_TYPE, type = CommandType.INTEGER, required = true, description = "The Quota usage type used to validate the activation rule.")
private Integer quotaType;
@Override
public void execute() {
QuotaValidateActivationRuleResponse response = responseBuilder.validateActivationRule(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
public String getActivationRule() {
return activationRule;
}
public QuotaTypes getQuotaType() {
QuotaTypes quotaTypes = QuotaTypes.getQuotaType(quotaType);
if (quotaTypes == null) {
throw new InvalidParameterValueException(String.format("Usage type not found for value [%s].", quotaType));
}
return quotaTypes;
}
}

View File

@ -26,6 +26,7 @@ import org.apache.cloudstack.api.command.QuotaStatementCmd;
import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd;
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
@ -88,4 +89,6 @@ public interface QuotaResponseBuilder {
QuotaConfigureEmailResponse createQuotaConfigureEmailResponse(QuotaEmailConfigurationVO quotaEmailConfigurationVO, Double minBalance, long accountId); QuotaConfigureEmailResponse createQuotaConfigureEmailResponse(QuotaEmailConfigurationVO quotaEmailConfigurationVO, Double minBalance, long accountId);
List<QuotaConfigureEmailResponse> listEmailConfiguration(long accountId); List<QuotaConfigureEmailResponse> listEmailConfiguration(long accountId);
QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd);
} }

View File

@ -16,6 +16,7 @@
//under the License. //under the License.
package org.apache.cloudstack.api.response; package org.apache.cloudstack.api.response;
import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
@ -34,12 +35,15 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import com.cloud.utils.DateUtil; import com.cloud.utils.DateUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaBalanceCmd;
@ -51,8 +55,10 @@ import org.apache.cloudstack.api.command.QuotaStatementCmd;
import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd;
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.cloudstack.discovery.ApiDiscoveryService;
import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper;
import org.apache.cloudstack.quota.QuotaManager; import org.apache.cloudstack.quota.QuotaManager;
import org.apache.cloudstack.quota.QuotaManagerImpl; import org.apache.cloudstack.quota.QuotaManagerImpl;
import org.apache.cloudstack.quota.QuotaService; import org.apache.cloudstack.quota.QuotaService;
@ -78,6 +84,7 @@ import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO;
import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.commons.lang3.reflect.FieldUtils;
@ -133,11 +140,13 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
private QuotaManager _quotaManager; private QuotaManager _quotaManager;
@Inject @Inject
private QuotaEmailConfigurationDao quotaEmailConfigurationDao; private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
@Inject
private JsInterpreterHelper jsInterpreterHelper;
@Inject
private ApiDiscoveryService apiDiscoveryService;
private final Class<?>[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; private final Class<?>[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class};
@Inject
private ApiDiscoveryService apiDiscoveryService;
@Override @Override
public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) { public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) {
@ -789,7 +798,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
*/ */
public void filterSupportedTypes(List<Pair<String, String>> variables, QuotaTypes quotaType, PresetVariableDefinition presetVariableDefinitionAnnotation, Class<?> fieldClass, public void filterSupportedTypes(List<Pair<String, String>> variables, QuotaTypes quotaType, PresetVariableDefinition presetVariableDefinitionAnnotation, Class<?> fieldClass,
String presetVariableName) { String presetVariableName) {
if (Arrays.stream(presetVariableDefinitionAnnotation.supportedTypes()).noneMatch(supportedType -> if (quotaType != null && Arrays.stream(presetVariableDefinitionAnnotation.supportedTypes()).noneMatch(supportedType ->
supportedType == quotaType.getQuotaType() || supportedType == 0)) { supportedType == quotaType.getQuotaType() || supportedType == 0)) {
return; return;
} }
@ -928,4 +937,82 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
return quotaConfigureEmailResponse; return quotaConfigureEmailResponse;
} }
@Override
public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd) {
String message;
String activationRule = cmd.getActivationRule();
QuotaTypes quotaType = cmd.getQuotaType();
String quotaName = quotaType.getQuotaName();
List<Pair<String, String>> usageTypeVariablesAndDescriptions = new ArrayList<>();
addAllPresetVariables(PresetVariables.class, quotaType, usageTypeVariablesAndDescriptions, null);
List<String> usageTypeVariables = usageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList());
try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) {
Map<String, String> newVariables = injectUsageTypeVariables(jsInterpreter, usageTypeVariables);
String scriptToExecute = jsInterpreterHelper.replaceScriptVariables(activationRule, newVariables);
jsInterpreter.executeScript(String.format("new Function(\"%s\")", scriptToExecute.replaceAll("\n", "")));
} catch (IOException | CloudRuntimeException e) {
logger.error("Unable to execute activation rule due to: [{}].", e.getMessage(), e);
message = "Error while executing activation rule. Check if there are no syntax errors and all variables are compatible with the given usage type.";
return createValidateActivationRuleResponse(activationRule, quotaName, false, message);
}
Set<String> scriptVariables = jsInterpreterHelper.getScriptVariables(activationRule);
if (isScriptVariablesValid(scriptVariables, usageTypeVariables)) {
message = "The script has no syntax errors and all variables are compatible with the given usage type.";
return createValidateActivationRuleResponse(activationRule, quotaName, true, message);
}
message = "Found variables that are not compatible with the given usage type.";
return createValidateActivationRuleResponse(activationRule, quotaName, false, message);
}
/**
* 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.
* Then, returns true if none of the remaining script variables match any usage types variables, and false otherwise.
*
* @param scriptVariables Script variables.
* @param scriptUsageTypeVariables Script usage type variables.
* @return True if the script variables are valid, false otherwise.
*/
protected boolean isScriptVariablesValid(Set<String> scriptVariables, List<String> scriptUsageTypeVariables) {
List<Pair<String, String>> allUsageTypeVariablesAndDescriptions = new ArrayList<>();
addAllPresetVariables(PresetVariables.class, null, allUsageTypeVariablesAndDescriptions, null);
List<String> allUsageTypesVariables = allUsageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList());
List<String> matchVariables = scriptVariables.stream().filter(scriptUsageTypeVariables::contains).collect(Collectors.toList());
matchVariables.forEach(scriptVariables::remove);
return scriptVariables.stream().noneMatch(allUsageTypesVariables::contains);
}
/**
* Injects variables into JavaScript interpreter. It's necessary to remove all dots from the given variables, as the interpreter
* does not interpret the variables as attributes of objects.
*
* @param jsInterpreter the {@link JsInterpreter} which the variables will be injected.
* @param variables the {@link List} with variables to format and inject the formatted variables into interpreter.
* @return A {@link Map} which has the key as the given variable and the value as the given variable formatted (without dots).
*/
protected Map<String, String> injectUsageTypeVariables(JsInterpreter jsInterpreter, List<String> variables) {
Map<String, String> formattedVariables = new HashMap<>();
for (String variable : variables) {
String formattedVariable = variable.replace(".", "");
formattedVariables.put(variable, formattedVariable);
jsInterpreter.injectVariable(formattedVariable, "false");
}
return formattedVariables;
}
public QuotaValidateActivationRuleResponse createValidateActivationRuleResponse(String activationRule, String quotaType, Boolean isValid, String message) {
QuotaValidateActivationRuleResponse response = new QuotaValidateActivationRuleResponse();
response.setActivationRule(activationRule);
response.setQuotaType(quotaType);
response.setValid(isValid);
response.setMessage(message);
return response;
}
} }

View File

@ -0,0 +1,76 @@
//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 com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.BaseResponse;
public class QuotaValidateActivationRuleResponse extends BaseResponse {
@SerializedName("activationrule")
@Param(description = "The validated activation rule.")
private String activationRule;
@SerializedName("quotatype")
@Param(description = "The Quota usage type used to validate the activation rule.")
private String quotaType;
@SerializedName("isvalid")
@Param(description = "Whether the activation rule is valid.")
private Boolean isValid;
@SerializedName("message")
@Param(description = "The reason whether the activation rule is valid or not.")
private String message;
public QuotaValidateActivationRuleResponse() {
super("validactivationrule");
}
public String getActivationRule() {
return activationRule;
}
public void setActivationRule(String activationRule) {
this.activationRule = activationRule;
}
public Boolean isValid() {
return isValid;
}
public void setValid(Boolean valid) {
isValid = valid;
}
public String getQuotaType() {
return quotaType;
}
public void setQuotaType(String quotaType) {
this.quotaType = quotaType;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@ -41,6 +41,7 @@ import org.apache.cloudstack.api.command.QuotaTariffDeleteCmd;
import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd;
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
import org.apache.cloudstack.api.command.QuotaUpdateCmd; import org.apache.cloudstack.api.command.QuotaUpdateCmd;
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.ConfigKey;
@ -121,6 +122,7 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
cmdList.add(QuotaConfigureEmailCmd.class); cmdList.add(QuotaConfigureEmailCmd.class);
cmdList.add(QuotaListEmailConfigurationCmd.class); cmdList.add(QuotaListEmailConfigurationCmd.class);
cmdList.add(QuotaPresetVariablesListCmd.class); cmdList.add(QuotaPresetVariablesListCmd.class);
cmdList.add(QuotaValidateActivationRuleCmd.class);
return cmdList; return cmdList;
} }

View File

@ -0,0 +1,41 @@
// 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;
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.api.response.QuotaValidateActivationRuleResponse;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class QuotaValidateActivationRuleCmdTest {
@Mock
QuotaResponseBuilder responseBuilderMock;
@Test
public void executeTestVerifyCalls() {
QuotaValidateActivationRuleCmd cmd = new QuotaValidateActivationRuleCmd();
cmd.responseBuilder = responseBuilderMock;
Mockito.doReturn(new QuotaValidateActivationRuleResponse()).when(responseBuilderMock).validateActivationRule(cmd);
cmd.execute();
Mockito.verify(responseBuilderMock).validateActivationRule(cmd);
}
}

View File

@ -25,6 +25,9 @@ import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.cloud.domain.DomainVO; import com.cloud.domain.DomainVO;
@ -34,7 +37,10 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
import org.apache.cloudstack.discovery.ApiDiscoveryService;
import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper;
import org.apache.cloudstack.quota.QuotaService; import org.apache.cloudstack.quota.QuotaService;
import org.apache.cloudstack.quota.QuotaStatement; import org.apache.cloudstack.quota.QuotaStatement;
import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition;
@ -55,7 +61,7 @@ import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO;
import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
@ -160,6 +166,12 @@ public class QuotaResponseBuilderImplTest extends TestCase {
return new Calendar[] {calendar, calendar}; return new Calendar[] {calendar, calendar};
} }
@Mock
QuotaValidateActivationRuleCmd quotaValidateActivationRuleCmdMock = Mockito.mock(QuotaValidateActivationRuleCmd.class);
@Mock
JsInterpreterHelper jsInterpreterHelperMock = Mockito.mock(JsInterpreterHelper.class);
private QuotaTariffVO makeTariffTestData() { private QuotaTariffVO makeTariffTestData() {
QuotaTariffVO tariffVO = new QuotaTariffVO(); QuotaTariffVO tariffVO = new QuotaTariffVO();
tariffVO.setUsageType(QuotaTypes.IP_ADDRESS); tariffVO.setUsageType(QuotaTypes.IP_ADDRESS);
@ -645,4 +657,78 @@ public class QuotaResponseBuilderImplTest extends TestCase {
assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock));
} }
@Test
public void validateActivationRuleTestValidateActivationRuleReturnValidScriptResponse() {
Mockito.doReturn("if (account.name == 'test') { true } else { false }").when(quotaValidateActivationRuleCmdMock).getActivationRule();
Mockito.doReturn(QuotaTypes.getQuotaType(30)).when(quotaValidateActivationRuleCmdMock).getQuotaType();
Mockito.doReturn(quotaValidateActivationRuleCmdMock.getActivationRule()).when(jsInterpreterHelperMock).replaceScriptVariables(Mockito.anyString(), Mockito.any());
QuotaValidateActivationRuleResponse response = quotaResponseBuilderSpy.validateActivationRule(quotaValidateActivationRuleCmdMock);
Assert.assertTrue(response.isValid());
}
@Test
public void validateActivationRuleTestUsageTypeIncompatibleVariableReturnInvalidScriptResponse() {
Mockito.doReturn("if (value.osName == 'test') { true } else { false }").when(quotaValidateActivationRuleCmdMock).getActivationRule();
Mockito.doReturn(QuotaTypes.getQuotaType(30)).when(quotaValidateActivationRuleCmdMock).getQuotaType();
Mockito.doReturn(quotaValidateActivationRuleCmdMock.getActivationRule()).when(jsInterpreterHelperMock).replaceScriptVariables(Mockito.anyString(), Mockito.any());
Mockito.when(jsInterpreterHelperMock.getScriptVariables(quotaValidateActivationRuleCmdMock.getActivationRule())).thenReturn(Set.of("value.osName"));
QuotaValidateActivationRuleResponse response = quotaResponseBuilderSpy.validateActivationRule(quotaValidateActivationRuleCmdMock);
Assert.assertFalse(response.isValid());
}
@Test
public void validateActivationRuleTestActivationRuleWithSyntaxErrorsReturnInvalidScriptResponse() {
Mockito.doReturn("{ if (account.name == 'test') { true } else { false } }}").when(quotaValidateActivationRuleCmdMock).getActivationRule();
Mockito.doReturn(QuotaTypes.getQuotaType(1)).when(quotaValidateActivationRuleCmdMock).getQuotaType();
Mockito.doReturn(quotaValidateActivationRuleCmdMock.getActivationRule()).when(jsInterpreterHelperMock).replaceScriptVariables(Mockito.anyString(), Mockito.any());
QuotaValidateActivationRuleResponse response = quotaResponseBuilderSpy.validateActivationRule(quotaValidateActivationRuleCmdMock);
Assert.assertFalse(response.isValid());
}
@Test
public void isScriptVariablesValidTestUnsupportedUsageTypeVariablesReturnFalse() {
Set<String> scriptVariables = new HashSet<>(List.of("value.computingResources.cpuNumber", "account.name", "zone.id"));
List<String> usageTypeVariables = List.of("value.virtualSize", "account.name", "zone.id");
boolean isScriptVariablesValid = quotaResponseBuilderSpy.isScriptVariablesValid(scriptVariables, usageTypeVariables);
Assert.assertFalse(isScriptVariablesValid);
}
@Test
public void isScriptVariablesValidTestSupportedUsageTypeVariablesReturnTrue() {
Set<String> scriptVariables = new HashSet<>(List.of("value.computingResources.cpuNumber", "account.name", "zone.id"));
List<String> usageTypeVariables = List.of("value.computingResources.cpuNumber", "account.name", "zone.id");
boolean isScriptVariablesValid = quotaResponseBuilderSpy.isScriptVariablesValid(scriptVariables, usageTypeVariables);
Assert.assertTrue(isScriptVariablesValid);
}
@Test
public void isScriptVariablesValidTestVariablesUnrelatedToUsageTypeReturnTrue() {
Set<String> scriptVariables = new HashSet<>(List.of("variable1.valid", "variable2.valid.", "variable3.valid"));
List<String> usageTypeVariables = List.of("project.name", "account.id", "domain.path");
boolean isScriptVariablesValid = quotaResponseBuilderSpy.isScriptVariablesValid(scriptVariables, usageTypeVariables);
Assert.assertTrue(isScriptVariablesValid);
}
@Test
public void injectUsageTypeVariablesTestReturnInjectedVariables() {
JsInterpreter interpreter = Mockito.mock(JsInterpreter.class);
Map<String, String> formattedVariables = quotaResponseBuilderSpy.injectUsageTypeVariables(interpreter, List.of("account.name", "zone.name"));
Assert.assertTrue(formattedVariables.containsValue("accountname"));
Assert.assertTrue(formattedVariables.containsValue("zonename"));
}
} }

View File

@ -0,0 +1,240 @@
// 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.jsinterpreter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.openjdk.nashorn.api.scripting.ScriptUtils;
import org.openjdk.nashorn.internal.runtime.Context;
import org.openjdk.nashorn.internal.runtime.ErrorManager;
import org.openjdk.nashorn.internal.runtime.options.Options;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JsInterpreterHelper {
private final Logger logger = LogManager.getLogger(getClass());
private static final String NAME = "name";
private static final String PROPERTY = "property";
private static final String TYPE = "type";
private static final String CALL_EXPRESSION = "CallExpression";
private int callExpressions;
private StringBuilder variable;
private Set<String> variables;
/**
* Returns all variables from the given script.
*
* @param script the script to extract the variables.
* @return A {@link Set<String>} containing all variables in the script.
*/
public Set<String> getScriptVariables(String script) {
String parseTree = getScriptAsJsonTree(script);
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = null;
variables = new HashSet<>();
variable = new StringBuilder();
try {
jsonNode = mapper.readTree(parseTree);
} catch (JsonProcessingException e) {
logger.error("Unable to create the script JSON tree due to: [{}].", e.getMessage(), e);
}
logger.trace("Searching script variables from [{}].", script);
iterateOverJsonTree(jsonNode.fields());
if (StringUtils.isNotBlank(variable.toString())) {
logger.trace("Adding variable [{}] into the variables set.", variable);
removeCallFunctionsFromVariable();
variables.add(variable.toString());
}
logger.trace("Found the following variables from the given script: [{}]", variables);
return variables;
}
private String getScriptAsJsonTree(String script) {
logger.trace("Creating JSON Tree for script [{}].", script);
Options options = new Options("nashorn");
options.set("anon.functions", true);
options.set("parse.only", true);
options.set("scripting", true);
ErrorManager errors = new ErrorManager();
Context context = new Context(options, errors, Thread.currentThread().getContextClassLoader());
Context.setGlobal(context.createGlobal());
return ScriptUtils.parse(script, "nashorn", false);
}
protected void iterateOverJsonTree(Iterator<Map.Entry<String, JsonNode>> iterator) {
while (iterator.hasNext()) {
iterateOverJsonTree(iterator.next());
}
}
protected void iterateOverJsonTree(Map.Entry<String, JsonNode> fields) {
JsonNode node = null;
if (fields.getValue().isArray()) {
iterateOverArrayNodes(fields);
} else {
node = fields.getValue();
}
String fieldName = searchIntoObjectNodes(node);
if (fieldName == null) {
String key = fields.getKey();
if (TYPE.equals(key) && CALL_EXPRESSION.equals(node.textValue())) {
callExpressions++;
}
if (NAME.equals(key) || PROPERTY.equals(key)) {
appendFieldValueToVariable(key, node);
}
}
}
protected void iterateOverArrayNodes(Map.Entry<String, JsonNode> fields) {
for (int count = 0; fields.getValue().get(count) != null; count++) {
iterateOverJsonTree(fields.getValue().get(count).fields());
}
}
protected String searchIntoObjectNodes(JsonNode node) {
if (node == null) {
return null;
}
String fieldName = null;
Iterator<String> iterator = node.fieldNames();
while (iterator.hasNext()) {
fieldName = iterator.next();
if (TYPE.equals(fieldName) && CALL_EXPRESSION.equals(node.get(fieldName).textValue())) {
callExpressions++;
}
if (NAME.equals(fieldName) || PROPERTY.equals(fieldName)) {
appendFieldValueToVariable(fieldName, node.get(fieldName));
}
if (node.get(fieldName).isArray()) {
JsonNode blockStatementContent = node.get(fieldName).get(0);
if (blockStatementContent != null) {
iterateOverJsonTree(blockStatementContent.fields());
}
} else {
iterateOverJsonTree(node.get(fieldName).fields());
}
}
return fieldName;
}
protected void appendFieldValueToVariable(String key, JsonNode node) {
String nodeTextValue = node.textValue();
if (nodeTextValue == null) {
return;
}
if (PROPERTY.equals(key)) {
logger.trace("Appending field value [{}] to variable [{}] as the field name is \"property\".", nodeTextValue, variable);
variable.append(".").append(nodeTextValue);
return;
}
logger.trace("Building new variable [{}] as the field name is \"name\"", nodeTextValue);
if (StringUtils.isNotBlank(variable.toString())) {
logger.trace("Adding variable [{}] into the variables set.", variable);
removeCallFunctionsFromVariable();
variables.add(variable.toString());
variable.setLength(0);
}
variable.append(nodeTextValue);
}
protected void removeCallFunctionsFromVariable() {
String[] disassembledVariable = variable.toString().split("\\.");
variable.setLength(0);
int newVariableSize = disassembledVariable.length - callExpressions;
String[] newVariable = Arrays.copyOfRange(disassembledVariable, 0, newVariableSize);
variable.append(String.join(".", newVariable));
callExpressions = 0;
}
/**
* Replaces all variables in script that matches the key in {@link Map} for their respective values.
*
* @param script the script which the variables will be replaced.
* @param variablesToReplace a {@link Map} which has the key as the variable to be replaced and the value as the variable to replace.
* @return A new script with the variables replaced.
*/
public String replaceScriptVariables(String script, Map<String, String> variablesToReplace) {
String regex = String.format("\\b(%s)\\b", String.join("|", variablesToReplace.keySet()));
Matcher matcher = Pattern.compile(regex).matcher(script);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
matcher.appendReplacement(sb, variablesToReplace.get(matcher.group()));
}
matcher.appendTail(sb);
return sb.toString();
}
public int getCallExpressions() {
return callExpressions;
}
public void setCallExpressions(int callExpressions) {
this.callExpressions = callExpressions;
}
public StringBuilder getVariable() {
return variable;
}
public void setVariable(StringBuilder variable) {
this.variable = variable;
}
public Set<String> getVariables() {
return variables;
}
public void setVariables(Set<String> variables) {
this.variables = variables;
}
}

View File

@ -218,6 +218,8 @@
<bean id="snapshotHelper" class="org.apache.cloudstack.snapshot.SnapshotHelper" /> <bean id="snapshotHelper" class="org.apache.cloudstack.snapshot.SnapshotHelper" />
<bean id="jsInterpreterHelper" class="org.apache.cloudstack.jsinterpreter.JsInterpreterHelper" />
<bean id="uploadMonitorImpl" class="com.cloud.storage.upload.UploadMonitorImpl" /> <bean id="uploadMonitorImpl" class="com.cloud.storage.upload.UploadMonitorImpl" />
<bean id="usageServiceImpl" class="com.cloud.usage.UsageServiceImpl" /> <bean id="usageServiceImpl" class="com.cloud.usage.UsageServiceImpl" />

View File

@ -0,0 +1,228 @@
// 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.jsinterpreter;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RunWith(MockitoJUnitRunner.class)
public class JsInterpreterHelperTest {
@Spy
private JsInterpreterHelper jsInterpreterHelperSpy;
@Mock
private JsonNode jsonNodeMock;
@Mock
private Iterator<String> fieldNamesMock;
@Mock
private Map.Entry<String, JsonNode> fields;
public void setupIterateOverJsonTreeTests() {
JsonNode node = Map.entry("array", jsonNodeMock).getValue();
Mockito.doReturn(true).when(node).isArray();
Mockito.doReturn(node).when(fields).getValue();
}
@Test
public void getScriptVariablesTestReturnVariables() {
String script = "if (account.name == 'test') { domain.id } else { zone.id }";
Set<String> variables = jsInterpreterHelperSpy.getScriptVariables(script);
Assert.assertEquals(variables.size(), 3);
Assert.assertTrue(variables.containsAll(List.of("account.name", "domain.id", "zone.id")));
}
@Test
public void getScriptVariablesTestScriptWithoutVariablesReturnEmptyList() {
String script = "if (4 < 2) { 3 } else if (3 != 3) { 3 } else { 3 > 3 } while (false) { 3 }";
Set<String> variables = jsInterpreterHelperSpy.getScriptVariables(script);
Assert.assertTrue(variables.isEmpty());
}
@Test
public void replaceScriptVariablesTestReturnScriptWithVariablesReplaced() {
String script = "if (account.name == 'test') { domain.id } else { zone.id }";
Map<String, String> newVariables = new HashMap<>();
newVariables.put("account.name", "accountname");
newVariables.put("domain.id", "domainid");
newVariables.put("zone.id", "zoneid");
String newScript = jsInterpreterHelperSpy.replaceScriptVariables(script, newVariables);
Assert.assertEquals("if (accountname == 'test') { domainid } else { zoneid }", newScript);
}
@Test
public void searchIntoObjectNodesTestNullNodeReturnNull() {
String fieldName = jsInterpreterHelperSpy.searchIntoObjectNodes(null);
Assert.assertEquals(null, fieldName);
}
@Test
public void searchIntoObjectNodesTestNonEmptyFieldNamesReturnFieldName() {
Mockito.doReturn(true, false).when(fieldNamesMock).hasNext();
Mockito.doReturn("fieldName").when(fieldNamesMock).next();
Mockito.doReturn(jsonNodeMock).when(jsonNodeMock).get("fieldName");
Mockito.doNothing().when(jsInterpreterHelperSpy).iterateOverJsonTree((Iterator<Map.Entry<String, JsonNode>>) Mockito.any());
Mockito.doReturn(fieldNamesMock).when(jsonNodeMock).fieldNames();
String fieldName = jsInterpreterHelperSpy.searchIntoObjectNodes(jsonNodeMock);
Assert.assertEquals("fieldName", fieldName);
}
@Test
public void searchIntoObjectNodesTestNameFieldAppendFieldValueToVariable() {
Mockito.doReturn(true, false).when(fieldNamesMock).hasNext();
Mockito.doReturn("name").when(fieldNamesMock).next();
Mockito.doReturn(jsonNodeMock).when(jsonNodeMock).get("name");
Mockito.doNothing().when(jsInterpreterHelperSpy).iterateOverJsonTree((Iterator<Map.Entry<String, JsonNode>>) Mockito.any());
Mockito.doReturn(fieldNamesMock).when(jsonNodeMock).fieldNames();
jsInterpreterHelperSpy.searchIntoObjectNodes(jsonNodeMock);
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(1)).appendFieldValueToVariable(Mockito.any(), Mockito.any());
}
@Test
public void searchIntoObjectNodesTestPropertyFieldAppendFieldValueToVariable() {
Mockito.doReturn(true, false).when(fieldNamesMock).hasNext();
Mockito.doReturn("property").when(fieldNamesMock).next();
Mockito.doReturn(jsonNodeMock).when(jsonNodeMock).get("property");
Mockito.doNothing().when(jsInterpreterHelperSpy).iterateOverJsonTree((Iterator<Map.Entry<String, JsonNode>>) Mockito.any());
Mockito.doReturn(fieldNamesMock).when(jsonNodeMock).fieldNames();
jsInterpreterHelperSpy.searchIntoObjectNodes(jsonNodeMock);
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(1)).appendFieldValueToVariable(Mockito.any(), Mockito.any());
}
@Test
public void appendFieldValueToVariableTestPropertyKeyAppendFieldValueAsAttribute() {
jsInterpreterHelperSpy.setVariable(new StringBuilder("account"));
jsInterpreterHelperSpy.setVariables(new HashSet<>());
Mockito.doReturn("name").when(jsonNodeMock).textValue();
jsInterpreterHelperSpy.appendFieldValueToVariable("property", jsonNodeMock);
Assert.assertEquals("account.name", jsInterpreterHelperSpy.getVariable().toString());
}
@Test
public void appendFieldValueToVariableTestNameKeyAppendFieldValueAsNewVariable() {
jsInterpreterHelperSpy.setVariable(new StringBuilder("account"));
jsInterpreterHelperSpy.setVariables(new HashSet<>());
Mockito.doReturn("zone").when(jsonNodeMock).textValue();
jsInterpreterHelperSpy.appendFieldValueToVariable("name", jsonNodeMock);
Assert.assertEquals("zone", jsInterpreterHelperSpy.getVariable().toString());
}
@Test
public void iterateOverJsonTreeTestMethodsCall() {
setupIterateOverJsonTreeTests();
jsInterpreterHelperSpy.iterateOverJsonTree(fields);
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(1)).iterateOverArrayNodes(Mockito.any());
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(1)).searchIntoObjectNodes(Mockito.any());
}
@Test
public void iterateOverJsonTreeTestFieldNameNullAndNameKeyAppendFieldValueToVariable() {
setupIterateOverJsonTreeTests();
Mockito.doReturn("name").when(fields).getKey();
Mockito.doNothing().when(jsInterpreterHelperSpy).appendFieldValueToVariable(Mockito.any(), Mockito.any());
jsInterpreterHelperSpy.iterateOverJsonTree(fields);
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(1)).appendFieldValueToVariable(Mockito.any(), Mockito.any());
}
@Test
public void iterateOverJsonTreeTestFieldNameNullAndNamePropertyAppendFieldValueToVariable() {
setupIterateOverJsonTreeTests();
Mockito.doReturn("property").when(fields).getKey();
Mockito.doNothing().when(jsInterpreterHelperSpy).appendFieldValueToVariable(Mockito.any(), Mockito.any());
jsInterpreterHelperSpy.iterateOverJsonTree(fields);
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(1)).appendFieldValueToVariable(Mockito.any(), Mockito.any());
}
@Test
public void iterateOverArrayNodesTestThreeSizeArrayCallIterateOverJsonTreeThreeTimes() {
Map<String, JsonNode> fields = new HashMap<>();
fields.put("field", jsonNodeMock);
JsonNode root = Mockito.mock(JsonNode.class);
JsonNode node1 = Mockito.mock(JsonNode.class);
JsonNode node2 = Mockito.mock(JsonNode.class);
JsonNode node3 = Mockito.mock(JsonNode.class);
Mockito.doReturn(fields.entrySet().iterator()).when(node1).fields();
Mockito.doReturn(fields.entrySet().iterator()).when(node2).fields();
Mockito.doReturn(fields.entrySet().iterator()).when(node3).fields();
Map<String, JsonNode> childrenMap = new HashMap<>();
childrenMap.put("node1", node1);
childrenMap.put("node2", node2);
childrenMap.put("node3", node3);
Map.Entry<String, JsonNode> rootEntry = Map.entry("rootNode", root);
Mockito.doReturn(node1).when(rootEntry.getValue()).get(0);
Mockito.doReturn(node2).when(rootEntry.getValue()).get(1);
Mockito.doReturn(node3).when(rootEntry.getValue()).get(2);
Mockito.doNothing().when(jsInterpreterHelperSpy).iterateOverJsonTree((Iterator<Map.Entry<String, JsonNode>>) Mockito.any());
jsInterpreterHelperSpy.iterateOverArrayNodes(rootEntry);
Mockito.verify(jsInterpreterHelperSpy, Mockito.times(3)).iterateOverJsonTree((Iterator<Map.Entry<String, JsonNode>>) Mockito.any());
}
@Test
public void removeCallFunctionsFromVariableTestTwoCallExpressionsRemoveTwoLastProperties() {
jsInterpreterHelperSpy.setCallExpressions(2);
jsInterpreterHelperSpy.setVariable(new StringBuilder("value.osName.toLowerCase().indexOf('windows')"));
jsInterpreterHelperSpy.removeCallFunctionsFromVariable();
Assert.assertEquals("value.osName", jsInterpreterHelperSpy.getVariable().toString());
}
}

View File

@ -1803,6 +1803,7 @@
"label.quota.statement.tariff": "Quota tariff", "label.quota.statement.tariff": "Quota tariff",
"label.quota.summary": "Summary", "label.quota.summary": "Summary",
"label.quota.tariff": "Tariff", "label.quota.tariff": "Tariff",
"label.quota.tariff.activationrule": "Activation rule",
"label.quota.tariff.effectivedate": "Effective date", "label.quota.tariff.effectivedate": "Effective date",
"label.quota.tariff.position": "Position", "label.quota.tariff.position": "Position",
"label.quota.tariff.value": "Tariff value", "label.quota.tariff.value": "Tariff value",
@ -1810,6 +1811,7 @@
"label.quota.type.name": "Usage Type", "label.quota.type.name": "Usage Type",
"label.quota.type.unit": "Usage unit", "label.quota.type.unit": "Usage unit",
"label.quota.usage": "Quota consumption", "label.quota.usage": "Quota consumption",
"label.quota.validate.activation.rule": "Validate activation rule",
"label.quota.value": "Quota value", "label.quota.value": "Quota value",
"label.quotastate": "Quota state", "label.quotastate": "Quota state",
"label.quota_enforce": "Enforce Quota", "label.quota_enforce": "Enforce Quota",
@ -3695,6 +3697,7 @@
"migrate.from": "Migrate from", "migrate.from": "Migrate from",
"migrate.to": "Migrate to", "migrate.to": "Migrate to",
"migrationPolicy": "Migration policy", "migrationPolicy": "Migration policy",
"placeholder.quota.tariff.activationrule": "Quota tariff's activation rule",
"placeholder.quota.tariff.description": "Quota tariff's description", "placeholder.quota.tariff.description": "Quota tariff's description",
"placeholder.quota.tariff.enddate": "Quota tariff's end date", "placeholder.quota.tariff.enddate": "Quota tariff's end date",
"placeholder.quota.tariff.name": "Quota tariff's name", "placeholder.quota.tariff.name": "Quota tariff's name",

View File

@ -128,6 +128,9 @@
"label.action.migrate.systemvm.to.ps": "Migrar VM de sistema para outro armazenamento prim\u00e1rio", "label.action.migrate.systemvm.to.ps": "Migrar VM de sistema para outro armazenamento prim\u00e1rio",
"label.action.project.add.account": "Adicionar conta ao projeto", "label.action.project.add.account": "Adicionar conta ao projeto",
"label.action.project.add.user": "Adicionar usu\u00e1rio a um projeto", "label.action.project.add.user": "Adicionar usu\u00e1rio a um projeto",
"label.action.quota.tariff.create": "Criar tarifa",
"label.action.quota.tariff.edit": "Editar tarifa",
"label.action.quota.tariff.remove": "Remover tarifa",
"label.action.reboot.instance": "Reiniciar inst\u00e2ncia", "label.action.reboot.instance": "Reiniciar inst\u00e2ncia",
"label.action.reboot.router": "Reiniciar roteador", "label.action.reboot.router": "Reiniciar roteador",
"label.action.reboot.systemvm": "Reiniciar VM de sistema", "label.action.reboot.systemvm": "Reiniciar VM de sistema",
@ -1283,9 +1286,7 @@
"label.quotastate": "Estado da cota", "label.quotastate": "Estado da cota",
"label.summary": "Sum\u00e1rio", "label.summary": "Sum\u00e1rio",
"label.quota.tariff": "Tarifa", "label.quota.tariff": "Tarifa",
"label.action.quota.tariff.create": "Criar tarifa", "label.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o",
"label.action.quota.tariff.edit": "Editar tarifa",
"label.action.quota.tariff.remove": "Remover tarifa",
"label.quota.tariff.effectivedate": "Data efetiva", "label.quota.tariff.effectivedate": "Data efetiva",
"label.quota.tariff.position": "Posi\u00e7\u00e3o", "label.quota.tariff.position": "Posi\u00e7\u00e3o",
"label.quota.tariff.value": "Valor", "label.quota.tariff.value": "Valor",
@ -1293,6 +1294,7 @@
"label.quota.type.name": "Tipo de uso", "label.quota.type.name": "Tipo de uso",
"label.quota.type.unit": "Unidade do uso", "label.quota.type.unit": "Unidade do uso",
"label.quota.usage": "Consumo da cota", "label.quota.usage": "Consumo da cota",
"label.quota.validate.activation.rule": "Validar regra de ativa\u00e7\u00e3o",
"label.quota.value": "Valor", "label.quota.value": "Valor",
"label.rados.monitor": "Monitor RADOS", "label.rados.monitor": "Monitor RADOS",
"label.rados.pool": "Pool do RADOS", "label.rados.pool": "Pool do RADOS",
@ -2514,6 +2516,7 @@
"migrate.from": "Migrar de", "migrate.from": "Migrar de",
"migrate.to": "Migrar para", "migrate.to": "Migrar para",
"migrationPolicy": "Pol\u00edtica de migra\u00e7\u00e3o", "migrationPolicy": "Pol\u00edtica de migra\u00e7\u00e3o",
"placeholder.quota.tariff.activationrule": "Regra de ativa\u00e7\u00e3o",
"placeholder.quota.tariff.description": "Descri\u00e7\u00e3o", "placeholder.quota.tariff.description": "Descri\u00e7\u00e3o",
"placeholder.quota.tariff.enddate": "Data de t\u00e9rmino", "placeholder.quota.tariff.enddate": "Data de t\u00e9rmino",
"placeholder.quota.tariff.name": "Nome", "placeholder.quota.tariff.name": "Nome",

View File

@ -24,6 +24,14 @@
} }
} }
.border-success {
border-color: #349469;
}
.border-fail {
border-color: #dc3545;
}
.form textarea { .form textarea {
resize: both; resize: both;
min-width: 20vw; min-width: 20vw;

View File

@ -66,6 +66,20 @@
v-model:value="form.value" v-model:value="form.value"
:placeholder="$t('placeholder.quota.tariff.value')" /> :placeholder="$t('placeholder.quota.tariff.value')" />
</a-form-item> </a-form-item>
<a-form-item ref="activationRule" name="activationRule">
<template #label>
<tooltip-label :title="$t('label.quota.tariff.activationrule')" :tooltip="apiParams.activationrule.description"/>
</template>
<a-textarea
v-model:value="form.activationRule"
:placeholder="$t('placeholder.quota.tariff.activationrule')"
:class="stateBorder"
:max-length="65535"
@keydown="isActivationRuleValid = undefined" />
</a-form-item>
<div class="action-button">
<a-button type="primary" @click="handleValidateActivationRule">{{ $t('label.quota.validate.activation.rule') }}</a-button>
</div>
<a-form-item ref="position" name="position"> <a-form-item ref="position" name="position">
<template #label> <template #label>
<tooltip-label :title="$t('label.quota.tariff.position')" :tooltip="apiParams.position.description" /> <tooltip-label :title="$t('label.quota.tariff.position')" :tooltip="apiParams.position.description" />
@ -124,7 +138,16 @@ export default {
data () { data () {
return { return {
loading: false, loading: false,
dayjs dayjs,
isActivationRuleValid: undefined
}
},
computed: {
stateBorder () {
return {
'border-success': this.isActivationRuleValid,
'border-fail': this.isActivationRuleValid === false
}
} }
}, },
beforeCreate () { beforeCreate () {
@ -180,6 +203,33 @@ export default {
this.formRef.value.scrollToField(error.errorFields[0].name) this.formRef.value.scrollToField(error.errorFields[0].name)
}) })
}, },
handleValidateActivationRule (e) {
e.preventDefault()
if (this.loading) return
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
this.loading = true
api('quotaValidateActivationRule', {}, 'POST', {
activationRule: values.activationRule || ' ',
usageType: values?.usageType?.split('-')[0]
}).then(response => {
const shortResponse = response.quotavalidateactivationruleresponse.validactivationrule
if (shortResponse.isvalid) {
this.$message.success(shortResponse.message)
} else {
this.$message.error(shortResponse.message)
}
this.isActivationRuleValid = shortResponse.isvalid
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
},
closeModal () { closeModal () {
this.$emit('close-action') this.$emit('close-action')
}, },

View File

@ -42,6 +42,20 @@
v-model:value="form.value" v-model:value="form.value"
:placeholder="$t('placeholder.quota.tariff.value')" /> :placeholder="$t('placeholder.quota.tariff.value')" />
</a-form-item> </a-form-item>
<a-form-item ref="activationRule" name="activationRule">
<template #label>
<tooltip-label :title="$t('label.quota.tariff.activationrule')" :tooltip="apiParams.activationrule.description"/>
</template>
<a-textarea
v-model:value="form.activationRule"
:placeholder="$t('placeholder.quota.tariff.activationrule')"
:class="stateBorder"
:max-length="65535"
@keydown="isActivationRuleValid = undefined" />
</a-form-item>
<div class="action-button">
<a-button type="primary" @click="handleValidateActivationRule">{{ $t('label.quota.validate.activation.rule') }}</a-button>
</div>
<a-form-item ref="position" name="position"> <a-form-item ref="position" name="position">
<template #label> <template #label>
<tooltip-label :title="$t('label.quota.tariff.position')" :tooltip="apiParams.position.description"/> <tooltip-label :title="$t('label.quota.tariff.position')" :tooltip="apiParams.position.description"/>
@ -93,8 +107,17 @@ export default {
}, },
data: () => ({ data: () => ({
loading: false, loading: false,
dayjs dayjs,
isActivationRuleValid: undefined
}), }),
computed: {
stateBorder () {
return {
'border-success': this.isActivationRuleValid,
'border-fail': this.isActivationRuleValid === false
}
}
},
inject: ['parentFetchData'], inject: ['parentFetchData'],
beforeCreate () { beforeCreate () {
this.apiParams = this.$getApiParams('quotaTariffUpdate') this.apiParams = this.$getApiParams('quotaTariffUpdate')
@ -109,7 +132,8 @@ export default {
description: this.resource.description, description: this.resource.description,
value: this.resource.tariffValue, value: this.resource.tariffValue,
position: this.resource.position, position: this.resource.position,
endDate: parseDateToDatePicker(this.resource.endDate) endDate: parseDateToDatePicker(this.resource.endDate),
activationRule: this.resource.activationRule
}) })
}, },
closeModal () { closeModal () {
@ -143,6 +167,10 @@ export default {
params.enddate = parseDayJsObject({ value: values.endDate }) params.enddate = parseDayJsObject({ value: values.endDate })
} }
if (values.activationRule && this.resource.activationRule !== values.activationRule) {
params.activationRule = values.activationRule
}
if (Object.keys(params).length === 1) { if (Object.keys(params).length === 1) {
this.closeModal() this.closeModal()
return return
@ -178,6 +206,33 @@ export default {
return current < startOfToday || current < lowerEndDateLimit.startOf('day') return current < startOfToday || current < lowerEndDateLimit.startOf('day')
} }
return current < startOfToday || current < lowerEndDateLimit.utc(false).startOf('day') return current < startOfToday || current < lowerEndDateLimit.utc(false).startOf('day')
},
handleValidateActivationRule (e) {
e.preventDefault()
if (this.loading) return
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
this.loading = true
api('quotaValidateActivationRule', {}, 'POST', {
activationRule: values.activationRule || ' ',
usageType: this.resource.usageType
}).then(response => {
const shortResponse = response.quotavalidateactivationruleresponse.validactivationrule
if (shortResponse.isvalid) {
this.$message.success(shortResponse.message)
} else {
this.$message.error(shortResponse.message)
}
this.isActivationRuleValid = shortResponse.isvalid
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
} }
} }
} }