Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2026-01-06 10:43:23 +05:30
parent d535dbcd17
commit 1036df831f
5 changed files with 611 additions and 75 deletions

View File

@ -46,7 +46,7 @@ public class ExceptionResponse extends BaseResponse {
@SerializedName("errortextkey")
@Param(description = "the key for the text associated with this error")
private String errorTextKey = "";
private String errorTextKey;
@SerializedName("errormetadata")
@Param(description = "the metadata associated with this error")

View File

@ -22,9 +22,13 @@ import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
@ -41,14 +45,15 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public final class ErrorMessageResolver {
public class ErrorMessageResolver {
private static final Logger LOG =
LogManager.getLogger(ErrorMessageResolver.class);
private static final String ERROR_MESSAGES_FILENAME = "error-messages.json";
private static final String ERROR_KEY_ADMIN_SUFFIX = ".admin";
private static final boolean INCLUDE_METADATA_ID_IN_MESSAGE = false;
protected static final String ERROR_MESSAGES_FILENAME = "error-messages.json";
protected static final String ERROR_KEY_ADMIN_SUFFIX = ".admin";
protected static final boolean INCLUDE_METADATA_ID_IN_MESSAGE = false;
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*([A-Za-z0-9_]+)\\s*\\}\\}");
private static final ObjectMapper MAPPER = new ObjectMapper();
@ -61,11 +66,43 @@ public final class ErrorMessageResolver {
private ErrorMessageResolver() {
}
public static String getMessage(String errorKey, Map<String, Object> metadata) {
return getMessageUsingStringMap(errorKey, getStringMap(metadata));
protected static List<String> getVariableNamesInErrorKey(String template) {
if (template == null || template.isEmpty()) {
return Collections.emptyList();
}
List<String> variables = new ArrayList<>();
Matcher matcher = VARIABLE_PATTERN.matcher(template);
while (matcher.find()) {
String name = matcher.group(1);
if (name != null && !name.isEmpty()) {
variables.add(name);
}
}
return variables;
}
private static String getTemplateForKey(String errorKey) {
protected static Map<String, Object> getCombinedMetadataFromErrorTemplate(String template, Map<String, Object> metadata) {
List<String> variableNames = getVariableNamesInErrorKey(template);
if (variableNames.isEmpty()) {
return metadata;
}
Map<String, Object> contextMetadata = CallContext.current().getContextStringKeyParameters();
if (MapUtils.isEmpty(contextMetadata)) {
return metadata;
}
Map<String, Object> combinedMetadata = new LinkedHashMap<>();
for (String varName : variableNames) {
if (contextMetadata.containsKey(varName)) {
combinedMetadata.put(varName, contextMetadata.get(varName));
}
}
if (MapUtils.isNotEmpty(metadata)) {
combinedMetadata.putAll(metadata);
}
return combinedMetadata;
}
protected static String getTemplateForKey(String errorKey) {
if (errorKey == null) {
return null;
}
@ -79,15 +116,7 @@ public final class ErrorMessageResolver {
return templates.get(errorKey);
}
private static String getMessageUsingStringMap(String errorKey, Map<String, String> metadata) {
String template = getTemplateForKey(errorKey);
if (template == null) {
return errorKey;
}
return expand(template, metadata);
}
private static Map<String, String> getStringMap(Map<String, Object> metadata) {
protected static Map<String, String> getStringMap(Map<String, Object> metadata) {
Map<String, String> stringMap = new LinkedHashMap<>();
if (MapUtils.isNotEmpty(metadata)) {
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
@ -98,16 +127,35 @@ public final class ErrorMessageResolver {
return stringMap;
}
private static String getMetadataObjectStringValue(Object obj) {
/**
* Converts a metadata object to a human-readable string for error messages.
*
* <p>Behavior:
* <ul>
* <li>If {@code obj} is {@code null}, returns {@code null}.</li>
* <li>Attempts to obtain a display name by invoking one of the getters
* {@code getDisplayText()}, {@code getDisplayName()}, or {@code getName()} via reflection.
* If a name is found, returns it quoted as {@code 'NAME'}.</li>
* <li>When the current calling account is a root admin, the returned value will include
* an identifier suffix in the form {@code (ID: id, UUID: uuid)} when available.
* The ID is included only if {@code INCLUDE_METADATA_ID_IN_MESSAGE} is {@code true}
* and {@code obj} implements {@link InternalIdentity}. The UUID is included when
* {@code obj} implements {@link org.apache.cloudstack.api.Identity}.</li>
* <li>If no display name is available, returns the UUID (if {@code obj} implements
* {@code Identity}); otherwise returns {@code obj.toString()}.</li>
* </ul>
*
* <p>Reflection is used to call getters; invocation failures are silently ignored and treated as
* absence of the corresponding value.
*
* @param obj metadata object
* @return formatted metadata string suitable for inclusion in error messages, or {@code null}
* if {@code obj} is {@code null}
*/
protected static String getMetadataObjectStringValue(Object obj) {
if (obj == null) {
return null;
}
// obj is of primitive type
// String value structure should be 'NAME' (ID: id, UUID: uuid)
// NAME is obtained from obj.getName() or obj.getDisplayText()
// ID is obtained from obj.getId() if obj instanceof InternalIdentity and only for root admin
// UUID is obtained from obj.getUuid() if obj instanceof Identity
// If NAME is not available, fallback to obj.toString() then simply return UUID if available
String uuid = null;
if (obj instanceof Identity) {
uuid = ((Identity) obj).getUuid();
@ -156,7 +204,7 @@ public final class ErrorMessageResolver {
return sb.toString();
}
private static String invokeStringGetter(Object obj, String methodName) {
protected static String invokeStringGetter(Object obj, String methodName) {
try {
Class<?> cls = obj.getClass();
var m = cls.getMethod(methodName);
@ -167,28 +215,7 @@ public final class ErrorMessageResolver {
}
}
public static void updateExceptionResponse(ExceptionResponse response, CloudRuntimeException cre) {
String key = cre.getMessageKey();
Map<String, Object> map = cre.getMetadata();
if (key == null) {
if (cre.getCause() instanceof InvalidParameterValueException) {
key = ((InvalidParameterValueException) cre.getCause()).getMessageKey();
map = ((InvalidParameterValueException) cre.getCause()).getMetadata();
} else {
return;
}
}
response.setErrorTextKey(key);
Map<String, String> stringMap = getStringMap(map);
String message = getMessageUsingStringMap(key, stringMap);
if (message != null) {
response.setErrorText(message);
}
response.setErrorMetadata(stringMap);
}
private static synchronized void reloadIfRequired() {
protected static synchronized void reloadIfRequired() {
try {
// log current directory for debugging purposes
LOG.debug("Current working directory: {}",
@ -230,12 +257,8 @@ public final class ErrorMessageResolver {
}
}
private static String expand(String template, Map<String, String> metadata) {
Map<String, Object> allMetadata = CallContext.current().getContextStringKeyParameters();
if (MapUtils.isNotEmpty(allMetadata)) {
allMetadata.putAll(metadata);
}
if (MapUtils.isEmpty(allMetadata)) {
protected static String expand(String template, Map<String, String> metadata) {
if (MapUtils.isEmpty(metadata)) {
return template;
}
String result = template;
@ -248,4 +271,39 @@ public final class ErrorMessageResolver {
}
return result;
}
public static String getMessage(String errorKey, Map<String, Object> metadata) {
String template = getTemplateForKey(errorKey);
if (template == null) {
return errorKey;
}
Map<String, Object> combinedMetadata = getCombinedMetadataFromErrorTemplate(template, metadata);
return expand(template, getStringMap(combinedMetadata));
}
public static void updateExceptionResponse(ExceptionResponse response, CloudRuntimeException cre) {
String key = cre.getMessageKey();
Map<String, Object> map = cre.getMetadata();
if (key == null) {
Throwable cause = cre.getCause();
if (!(cause instanceof InvalidParameterValueException)) {
return;
}
InvalidParameterValueException ipve = (InvalidParameterValueException) cause;
key = ipve.getMessageKey();
map = ipve.getMetadata();
}
response.setErrorTextKey(key);
String template = getTemplateForKey(key);
if (template == null) {
response.setErrorText(key);
response.setErrorMetadata(getStringMap(map));
return;
}
Map<String, Object> combinedMetadata = getCombinedMetadataFromErrorTemplate(template, map);
Map<String, String> stringMap = getStringMap(combinedMetadata);
response.setErrorText(expand(template, stringMap));
response.setErrorMetadata(stringMap);
}
}

View File

@ -0,0 +1,460 @@
// 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.context;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.response.ExceptionResponse;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.dc.DataCenter;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(MockitoJUnitRunner.class)
public class ErrorMessageResolverTest {
@Mock
CallContext callContextMock;
MockedStatic<CallContext> callContextMocked;
MockedStatic<PropertiesUtil> propertiesUtilMocked;
Path tmpFile;
@Before
public void setup() {
callContextMocked = Mockito.mockStatic(CallContext.class);
callContextMocked.when(CallContext::current).thenReturn(callContextMock);
propertiesUtilMocked = Mockito.mockStatic(PropertiesUtil.class);
propertiesUtilMocked.when(() -> PropertiesUtil.findConfigFile(anyString())).thenReturn(null);
}
@After
public void tearDown() throws Exception {
callContextMocked.close();
propertiesUtilMocked.close();
if (tmpFile != null) {
try {
Files.deleteIfExists(tmpFile);
} catch (Exception ignored) {
}
}
}
@Test
public void getVariableNamesInErrorKey_shouldReturnEmptyListForTemplateWithoutVariables() {
String template = "This is a template without variables.";
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
Assert.assertTrue("Result should be an empty list", result.isEmpty());
}
@Test
public void getVariableNamesInErrorKey_shouldExtractSingleVariable() {
String template = "This is a template with one variable: {{variable}}.";
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
Assert.assertEquals("Result should contain one variable", 1, result.size());
Assert.assertEquals("Variable name should be 'variable'", "variable", result.get(0));
}
@Test
public void getVariableNamesInErrorKey_shouldExtractMultipleVariables() {
String template = "This template has {{var1}} and {{var2}}.";
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
Assert.assertEquals("Result should contain two variables", 2, result.size());
Assert.assertEquals("First variable name should be 'var1'", "var1", result.get(0));
Assert.assertEquals("Second variable name should be 'var2'", "var2", result.get(1));
}
@Test
public void getVariableNamesInErrorKey_shouldIgnoreMalformedVariables() {
String template = "This template has {{var1} and {{var2}}.";
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
Assert.assertEquals("Result should contain one valid variable", 1, result.size());
Assert.assertEquals("Variable name should be 'var2'", "var2", result.get(0));
}
@Test
public void getVariableNamesInErrorKey_shouldHandleEmptyTemplate() {
String template = "";
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
Assert.assertTrue("Result should be an empty list", result.isEmpty());
}
@Test
public void getVariableNamesInErrorKey_shouldHandleTemplateWithOnlyVariables() {
String template = "{{var1}}{{var2}}{{var3}}";
List<String> result = ErrorMessageResolver.getVariableNamesInErrorKey(template);
Assert.assertEquals("Result should contain three variables", 3, result.size());
Assert.assertEquals("First variable name should be 'var1'", "var1", result.get(0));
Assert.assertEquals("Second variable name should be 'var2'", "var2", result.get(1));
Assert.assertEquals("Third variable name should be 'var3'", "var3", result.get(2));
}
@Test
public void getCombinedMetadataFromErrorTemplate_shouldReturnMetadataWhenNoVariablesInTemplate() {
String template = "This is a template without variables.";
Map<String, Object> metadata = Map.of("key1", "value1");
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
Assert.assertEquals("Result should match the input metadata", metadata, result);
}
@Test
public void getCombinedMetadataFromErrorTemplate_shouldReturnEmptyMapWhenContextMetadataIsEmpty() {
String template = "This template has {{var1}}.";
Map<String, Object> metadata = Map.of();
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of());
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
Assert.assertTrue("Result should be an empty map", result.isEmpty());
}
@Test
public void getCombinedMetadataFromErrorTemplate_shouldCombineContextAndProvidedMetadata() {
String template = "This template has {{var1}} and {{var2}}.";
Map<String, Object> metadata = Map.of("key1", "value1");
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of("var1", "valueVar1", "var2", "valueVar2"));
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
Assert.assertEquals("Result should contain combined metadata", 3, result.size());
Assert.assertEquals("Result should contain context metadata for var1", "valueVar1", result.get("var1"));
Assert.assertEquals("Result should contain context metadata for var2", "valueVar2", result.get("var2"));
Assert.assertEquals("Result should contain provided metadata", "value1", result.get("key1"));
}
@Test
public void getCombinedMetadataFromErrorTemplate_shouldIgnoreVariablesNotInContextMetadata() {
String template = "This template has {{var1}} and {{var2}}.";
Map<String, Object> metadata = Map.of("key1", "value1");
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of("var1", "valueVar1"));
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
Assert.assertEquals("Result should contain combined metadata", 2, result.size());
Assert.assertEquals("Result should contain context metadata for var1", "valueVar1", result.get("var1"));
Assert.assertEquals("Result should contain provided metadata", "value1", result.get("key1"));
}
@Test
public void getCombinedMetadataFromErrorTemplate_shouldReturnProvidedMetadataWhenTemplateIsEmpty() {
String template = "";
Map<String, Object> metadata = Map.of("key1", "value1");
Map<String, Object> result = ErrorMessageResolver.getCombinedMetadataFromErrorTemplate(template, metadata);
Assert.assertEquals("Result should match the input metadata", metadata, result);
}
@Test
public void getStringMap_shouldReturnEmptyMapWhenMetadataIsEmpty() {
Map<String, Object> metadata = Map.of();
Map<String, String> result = ErrorMessageResolver.getStringMap(metadata);
Assert.assertTrue("Result should be an empty map", result.isEmpty());
}
@Test
public void getStringMap_shouldConvertAllMetadataValuesToStrings() {
Map<String, Object> metadata = Map.of("key1", 123, "key2", true, "key3", "value");
Map<String, String> result = ErrorMessageResolver.getStringMap(metadata);
Assert.assertEquals("Result should contain all keys", 3, result.size());
Assert.assertEquals("Value for key1 should be '123'", "123", result.get("key1"));
Assert.assertEquals("Value for key2 should be 'true'", "true", result.get("key2"));
Assert.assertEquals("Value for key3 should be 'value'", "value", result.get("key3"));
}
@Test
public void getStringMap_shouldHandleNullValuesInMetadata() {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("key1", null);
metadata.put("key2", "value");
Map<String, String> result = ErrorMessageResolver.getStringMap(metadata);
Assert.assertEquals("Result should contain all keys", 2, result.size());
Assert.assertNull("Value for key1 should be null", result.get("key1"));
Assert.assertEquals("Value for key2 should be 'value'", "value", result.get("key2"));
}
@Test
public void getStringMap_shouldReturnEmptyMapWhenMetadataIsNull() {
Map<String, String> result = ErrorMessageResolver.getStringMap(null);
Assert.assertTrue("Result should be an empty map", result.isEmpty());
}
@Test
public void getMetadataObjectStringValue_shouldReturnNullWhenObjectIsNull() {
Assert.assertNull(ErrorMessageResolver.getMetadataObjectStringValue(null));
}
@Test
public void getMetadataObjectStringValue_shouldReturnUuidWhenNameIsUnavailable() {
Identity identityMock = Mockito.mock(Identity.class);
when(identityMock.getUuid()).thenReturn("uuid-1234");
Assert.assertEquals("uuid-1234", ErrorMessageResolver.getMetadataObjectStringValue(identityMock));
}
@Test
public void getMetadataObjectStringValue_shouldReturnNameWhenAvailable() {
DataCenter identityMock = Mockito.mock(DataCenter.class);
when(identityMock.getUuid()).thenReturn("uuid-1234");
when(identityMock.getName()).thenReturn("TestName");
Assert.assertEquals("'TestName'", ErrorMessageResolver.getMetadataObjectStringValue(identityMock));
}
@Test
public void getMetadataObjectStringValue_shouldIncludeIdAndUuidForRootAdmin() {
DataCenter internalIdentityMock = Mockito.mock(DataCenter.class);
when(internalIdentityMock.getUuid()).thenReturn("uuid-5678");
if (ErrorMessageResolver.INCLUDE_METADATA_ID_IN_MESSAGE) {
when(internalIdentityMock.getId()).thenReturn(42L);
}
when(internalIdentityMock.getName()).thenReturn("AdminName");
when(CallContext.current().isCallingAccountRootAdmin()).thenReturn(true);
String expected = String.format("'AdminName' (%sUUID: uuid-5678)", ErrorMessageResolver.INCLUDE_METADATA_ID_IN_MESSAGE ? "ID: 42, " : "");
Assert.assertEquals(expected, ErrorMessageResolver.getMetadataObjectStringValue(internalIdentityMock));
}
@Test
public void getMetadataObjectStringValue_shouldFallbackToToStringWhenNameAndUuidAreUnavailable() {
Object obj = new Object();
Assert.assertEquals(obj.toString(), ErrorMessageResolver.getMetadataObjectStringValue(obj));
}
@Test
public void getMetadataObjectStringValue_shouldReturnNameOnlyForNonRootAdmin() {
DataCenter internalIdentityMock = Mockito.mock(DataCenter.class);
when(internalIdentityMock.getName()).thenReturn("UserName");
when(CallContext.current().isCallingAccountRootAdmin()).thenReturn(false);
Assert.assertEquals("'UserName'", ErrorMessageResolver.getMetadataObjectStringValue(internalIdentityMock));
}
@Test
public void expand_shouldReturnTemplateWhenMetadataIsEmpty() {
String template = "This is a template with no placeholders.";
Map<String, String> metadata = Map.of();
String result = ErrorMessageResolver.expand(template, metadata);
Assert.assertEquals("This is a template with no placeholders.", result);
}
@Test
public void expand_shouldReplaceSinglePlaceholderWithMetadataValue() {
String template = "Hello, {{name}}!";
Map<String, String> metadata = Map.of("name", "World");
String result = ErrorMessageResolver.expand(template, metadata);
Assert.assertEquals("Hello, World!", result);
}
@Test
public void expand_shouldReplaceMultiplePlaceholdersWithMetadataValues() {
String template = "Hello, {{name}}! Today is {{day}}.";
Map<String, String> metadata = Map.of("name", "Alice", "day", "Monday");
String result = ErrorMessageResolver.expand(template, metadata);
Assert.assertEquals("Hello, Alice! Today is Monday.", result);
}
@Test
public void expand_shouldIgnorePlaceholdersWithoutMatchingMetadata() {
String template = "Hello, {{name}}! Your age is {{age}}.";
Map<String, String> metadata = Map.of("name", "Bob");
String result = ErrorMessageResolver.expand(template, metadata);
Assert.assertEquals("Hello, Bob! Your age is {{age}}.", result);
}
@Test
public void expand_shouldHandleNullMetadataValues() {
String template = "Hello, {{name}}!";
Map<String, String> metadata = new LinkedHashMap<>();
metadata.put("name", null);
String result = ErrorMessageResolver.expand(template, metadata);
Assert.assertEquals("Hello, {{name}}!", result);
}
@Test
public void expand_shouldReturnTemplateWhenMetadataIsNull() {
String template = "This is a template with no placeholders.";
String result = ErrorMessageResolver.expand(template, null);
Assert.assertEquals("This is a template with no placeholders.", result);
}
@Test
public void expand_shouldHandleTemplateWithNoPlaceholders() {
String template = "This template has no placeholders.";
Map<String, String> metadata = Map.of("key", "value");
String result = ErrorMessageResolver.expand(template, metadata);
Assert.assertEquals("This template has no placeholders.", result);
}
@Test
public void getMessage_shouldReturnErrorKeyWhenTemplateIsNull() {
String errorKey = "error.key";
String result = ErrorMessageResolver.getMessage(errorKey, Map.of());
Assert.assertEquals(errorKey, result);
}
private void createTempFileWithTemplate(String errorKey, String template) {
try {
tmpFile = Files.createTempFile("error-template-", ".txt");
Files.writeString(tmpFile, String.format("{ \"%s\": \"%s\" }", errorKey, template));
propertiesUtilMocked.when(() -> PropertiesUtil.findConfigFile(anyString()))
.thenReturn(tmpFile.toFile());
} catch (IOException e) {
Assert.fail("Failed to create temporary file for testing: " + e.getMessage());
}
}
@Test
public void getMessage_shouldReturnExpandedMessageWhenTemplateExists() {
String errorKey = "error.key";
String template = "Error occurred: {{field}}";
Map<String, Object> metadata = Map.of("field", "value");
createTempFileWithTemplate(errorKey, template);
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of());
String result = ErrorMessageResolver.getMessage(errorKey, metadata);
Assert.assertEquals("Error occurred: value", result);
}
@Test
public void getMessage_shouldHandleEmptyMetadata() {
String errorKey = "error.key";
String template = "Error occurred.";
createTempFileWithTemplate(errorKey, template);
String result = ErrorMessageResolver.getMessage(errorKey, Map.of());
Assert.assertEquals("Error occurred.", result);
}
@Test
public void getMessage_shouldHandleNullMetadata() {
String errorKey = "error.key";
String template = "Error occurred.";
createTempFileWithTemplate(errorKey, template);
String result = ErrorMessageResolver.getMessage(errorKey, null);
Assert.assertEquals("Error occurred.", result);
}
@Test
public void getMessage_shouldCombineContextAndProvidedMetadata() {
String errorKey = "error.key";
String template = "Error in {{field1}} and {{field2}}.";
Map<String, Object> metadata = Map.of("field1", "value1");
createTempFileWithTemplate(errorKey, template);
when(callContextMock.getContextStringKeyParameters()).thenReturn(Map.of("field2", "value2"));
String result = ErrorMessageResolver.getMessage(errorKey, metadata);
Assert.assertEquals("Error in value1 and value2.", result);
}
@Test
public void updateExceptionResponse_shouldSetErrorTextAndMetadataWhenKeyAndTemplateExist() {
ExceptionResponse response = new ExceptionResponse();
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
String key = "error.key";
String template = "Error occurred: {{field}}";
Map<String, Object> metadata = Map.of("field", "value");
when(cre.getMessageKey()).thenReturn(key);
when(cre.getMetadata()).thenReturn(metadata);
createTempFileWithTemplate(key, template);
ErrorMessageResolver.updateExceptionResponse(response, cre);
Assert.assertEquals(key, response.getErrorTextKey());
Assert.assertEquals("Error occurred: value", response.getErrorText());
Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata());
}
@Test
public void updateExceptionResponse_shouldSetErrorTextKeyOnlyWhenTemplateDoesNotExist() {
ExceptionResponse response = new ExceptionResponse();
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
String key = "error.key";
Map<String, Object> metadata = Map.of("field", "value");
when(cre.getMessageKey()).thenReturn(key);
when(cre.getMetadata()).thenReturn(metadata);
ErrorMessageResolver.updateExceptionResponse(response, cre);
Assert.assertEquals(key, response.getErrorTextKey());
Assert.assertEquals(key, response.getErrorText());
Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata());
}
@Test
public void updateExceptionResponse_shouldHandleNullKeyAndCauseIsNotInvalidParameterValueException() {
ExceptionResponse response = new ExceptionResponse();
String originalErrorText = response.getErrorText();
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
when(cre.getMessageKey()).thenReturn(null);
when(cre.getCause()).thenReturn(new RuntimeException());
ErrorMessageResolver.updateExceptionResponse(response, cre);
Assert.assertNull(response.getErrorTextKey());
Assert.assertNull(response.getErrorMetadata());
Assert.assertEquals(originalErrorText, response.getErrorText());
}
@Test
public void updateExceptionResponse_shouldUseCauseMetadataWhenKeyIsNullAndCauseIsInvalidParameterValueException() {
ExceptionResponse response = new ExceptionResponse();
InvalidParameterValueException cause = Mockito.mock(InvalidParameterValueException.class);
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
String key = "error.key";
String template = "Error occurred: {{field}}";
Map<String, Object> metadata = Map.of("field", "value");
when(cre.getMessageKey()).thenReturn(null);
when(cre.getCause()).thenReturn(cause);
when(cause.getMessageKey()).thenReturn(key);
when(cause.getMetadata()).thenReturn(metadata);
createTempFileWithTemplate(key, template);
ErrorMessageResolver.updateExceptionResponse(response, cre);
Assert.assertEquals(key, response.getErrorTextKey());
Assert.assertEquals("Error occurred: value", response.getErrorText());
Assert.assertEquals(Map.of("field", "value"), response.getErrorMetadata());
}
@Test
public void updateExceptionResponse_shouldHandleNullMetadata() {
ExceptionResponse response = new ExceptionResponse();
CloudRuntimeException cre = Mockito.mock(CloudRuntimeException.class);
String key = "error.key";
String template = "Error occurred.";
when(cre.getMessageKey()).thenReturn(key);
when(cre.getMetadata()).thenReturn(null);
createTempFileWithTemplate(key, template);
ErrorMessageResolver.updateExceptionResponse(response, cre);
Assert.assertEquals(key, response.getErrorTextKey());
Assert.assertEquals("Error occurred.", response.getErrorText());
Assert.assertTrue(response.getErrorMetadata().isEmpty());
}
}

View File

@ -1,9 +1,21 @@
{
"vm.deploy.network.not.found.ip.map": "The network selected {{networkId}} in IP to network map could not be found. It may have been removed or is no longer accessible.",
"vm.deploy.serviceoffering.fixed.parameters.not.allowed": "Unable to deploy the instance because the selected service offering {{serviceOffering}} does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
"vm.deploy.serviceoffering.fixed.parameters.not.allowed.admin": "Unable to deploy the instance because the selected service offering {{serviceOffering}} is not a dynamic offering and it does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
"vm.deploy.serviceoffering.inactive": "Unable to deploy Instance as the given service offering {{serviceOffering}} is inactive. Specify an active service offering.",
"vm.deploy.serviceoffering.not.specified": "Unable to deploy Instance as the required parameter 'serviceofferingid' is missing.",
"vm.deploy.serviceoffering.override.not.allowed": "Unable to deploy Instance as the selected service offering {{serviceOffering}} does not allow changing the disk offering.",
"vm.deploy.serviceoffering.override.not.allowed.admin": "Unable to deploy Instance as the selected service offering {{serviceOffering}} uses disk offering strictness and does not allow changing the disk offering."
"vm.deploy.diskoffering.compute.only": "Unable to deploy Instance as the specified disk offering {{diskOffering}} is mapped to a service offering. Specify a disk offering that is not mapped to any service offering.",
"vm.deploy.diskoffering.local.storage.zone.unsupported": "Unable to deploy Instance because the zone is not configured to use local storage, but the specified disk offering {{diskOffering}} requires it.",
"vm.deploy.diskoffering.not.found": "Unable to deploy Instance as the specified disk offering is not found.",
"vm.deploy.diskoffering.with.diskoffering.details": "Unable to deploy Instance as both a disk offering ID and data disk offering details were provided. Specify only one.",
"vm.deploy.hypervisor.volume.snapshot.not.supported": "Unable to deploy Instance because deployment from an existing volume or snapshot is supported only on the KVM hypervisor.",
"vm.deploy.network.not.found.ip.map": "The network selected {{networkId}} in IP to network map could not be found. It may have been removed or is no longer accessible.",
"vm.deploy.serviceoffering.fixed.parameters.not.allowed": "Unable to deploy the instance because the selected service offering {{serviceOffering}} does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
"vm.deploy.serviceoffering.fixed.parameters.not.allowed.admin": "Unable to deploy the instance because the selected service offering {{serviceOffering}} is not a dynamic offering and it does not allow specifying {{cpuNumberKey}}, {{cpuSpeedKey}}, or {{memory}}.",
"vm.deploy.serviceoffering.inactive": "Unable to deploy Instance as the given service offering {{serviceOffering}} is inactive. Specify an active service offering.",
"vm.deploy.serviceoffering.local.storage.zone.unsupported": "Unable to deploy Instance because the zone is not configured to use local storage, but the disk offering mapped to service offering {{serviceOffering}} requires it.",
"vm.deploy.serviceoffering.not.specified": "Unable to deploy Instance as the required parameter 'serviceofferingid' is missing.",
"vm.deploy.serviceoffering.not.found": "Unable to deploy Instance as the specified service offering is not found.",
"vm.deploy.serviceoffering.override.not.allowed": "Unable to deploy Instance as the selected service offering {{serviceOffering}} does not allow changing the disk offering.",
"vm.deploy.serviceoffering.override.not.allowed.admin": "Unable to deploy Instance as the selected service offering {{serviceOffering}} uses disk offering strictness and does not allow changing the disk offering.",
"vm.deploy.snapshot.not.found": "Unable to deploy Instance as the specified Snapshot is not found.",
"vm.deploy.template.associated.not.usable": "Unable to deploy Instance as the associated Template cannot be used.",
"vm.deploy.template.not.found": "Unable to deploy Instance as the specified Template is not found.",
"vm.deploy.volume.not.found": "Unable to deploy Instance as the specified Volume is not found.",
"vm.deploy.zone.not.found": "Unable to deploy Instance as the specified zone is not found."
}

View File

@ -6383,7 +6383,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
DataCenter zone = _entityMgr.findById(DataCenter.class, zoneId);
if (zone == null) {
throw new InvalidParameterValueException("Unable to find zone by id=" + zoneId);
throw new InvalidParameterValueException("vm.deploy.zone.not.found", Collections.emptyMap());
}
Long serviceOfferingId = cmd.getServiceOfferingId();
@ -6394,7 +6394,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
ServiceOffering serviceOffering = _entityMgr.findById(ServiceOffering.class, serviceOfferingId);
if (serviceOffering == null) {
throw new InvalidParameterValueException("Unable to find service offering: " + serviceOffering.getId());
throw new InvalidParameterValueException("vm.deploy.serviceoffering.not.found", Collections.emptyMap());
}
verifyServiceOffering(cmd, serviceOffering);
@ -6408,14 +6408,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
if (cmd.getVolumeId() != null) {
volume = getVolume(cmd.getVolumeId(), templateId, false);
if (volume == null) {
throw new InvalidParameterValueException("Could not find volume with id=" + cmd.getVolumeId());
throw new InvalidParameterValueException("vm.deploy.volume.not.found", Collections.emptyMap());
}
_accountMgr.checkAccess(caller, null, true, volume);
templateId = volume.getTemplateId();
} else if (cmd.getSnapshotId() != null) {
snapshot = _snapshotDao.findById(cmd.getSnapshotId());
if (snapshot == null) {
throw new InvalidParameterValueException("Could not find snapshot with id=" + cmd.getSnapshotId());
throw new InvalidParameterValueException("vm.deploy.snapshot.not.found", Collections.emptyMap());
}
_accountMgr.checkAccess(caller, null, true, snapshot);
VolumeInfo volumeOfSnapshot = getVolume(snapshot.getVolumeId(), templateId, true);
@ -6431,16 +6431,18 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
VirtualMachineTemplate template = null;
if (volume != null || snapshot != null) {
template = _entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, templateId);
if (template == null) {
throw new InvalidParameterValueException("vm.deploy.template.associated.not.usable", Collections.emptyMap());
}
} else {
template = _entityMgr.findById(VirtualMachineTemplate.class, templateId);
if (template == null) {
throw new InvalidParameterValueException("vm.deploy.template.not.found", Collections.emptyMap());
}
}
if (cmd.isVolumeOrSnapshotProvided() &&
(!(HypervisorType.KVM.equals(template.getHypervisorType()) || HypervisorType.KVM.equals(cmd.getHypervisor())))) {
throw new InvalidParameterValueException("Deploying a virtual machine with existing volume/snapshot is supported only from KVM hypervisors");
}
// Make sure a valid template ID was specified
if (template == null) {
throw new InvalidParameterValueException("Unable to use template " + templateId);
throw new InvalidParameterValueException("vm.deploy.hypervisor.volume.snapshot.not.supported", Collections.emptyMap());
}
verifyTemplate(cmd, template, serviceOfferingId);
@ -6449,25 +6451,29 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
if (diskOfferingId != null) {
diskOffering = _entityMgr.findById(DiskOffering.class, diskOfferingId);
if (diskOffering == null) {
throw new InvalidParameterValueException("Unable to find disk offering " + diskOfferingId);
throw new InvalidParameterValueException("vm.deploy.diskoffering.not.found", Collections.emptyMap());
}
if (diskOffering.isComputeOnly()) {
throw new InvalidParameterValueException(String.format("The disk offering %s provided is directly mapped to a service offering, please provide an individual disk offering", diskOffering));
throw new InvalidParameterValueException("vm.deploy.diskoffering.compute.only",
Map.of("diskOffering", diskOffering));
}
}
List<VmDiskInfo> dataDiskInfoList = cmd.getDataDiskInfoList();
if (dataDiskInfoList != null && diskOfferingId != null) {
new InvalidParameterValueException("Cannot specify both disk offering id and data disk offering details");
throw new InvalidParameterValueException("vm.deploy.diskoffering.with.diskoffering.details",
Collections.emptyMap());
}
if (!zone.isLocalStorageEnabled()) {
DiskOffering diskOfferingMappedInServiceOffering = _entityMgr.findById(DiskOffering.class, serviceOffering.getDiskOfferingId());
if (diskOfferingMappedInServiceOffering.isUseLocalStorage()) {
throw new InvalidParameterValueException("Zone is not configured to use local storage but disk offering " + diskOfferingMappedInServiceOffering.getName() + " mapped in service offering uses it");
throw new InvalidParameterValueException("vm.deploy.serviceoffering.local.storage.zone.unsupported",
Map.of("serviceOffering", serviceOffering));
}
if (diskOffering != null && diskOffering.isUseLocalStorage()) {
throw new InvalidParameterValueException("Zone is not configured to use local storage but disk offering " + diskOffering.getName() + " uses it");
throw new InvalidParameterValueException("vm.deploy.diskoffering.local.storage.zone.unsupported",
Map.of("diskOffering", diskOffering));
}
}