From 6fb1551b8435aefc121f04bfb0c687d0ee240eea Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 24 Dec 2025 12:53:49 +0530 Subject: [PATCH] framework,api,server,ui: improve error messaging Framwork level improvements for for contextual and localized API error messages More details to be added Signed-off-by: Abhishek Kumar --- .../InvalidParameterValueException.java | 8 + .../java/com/cloud/user/AccountService.java | 6 +- .../api/command/user/vm/BaseDeployVMCmd.java | 3 +- .../api/response/ExceptionResponse.java | 28 +- .../cloudstack/context/CallContext.java | 23 +- .../context/ErrorMessageResolver.java | 241 ++++++++++++++++++ client/conf/error-messages.json.in | 9 + debian/cloudstack-management.install | 1 + .../com/cloud/service/ServiceOfferingVO.java | 2 +- .../java/com/cloud/user/UserAccountVO.java | 2 +- .../src/main/java/com/cloud/user/UserVO.java | 4 +- packaging/el8/cloud.spec | 2 +- .../management/MockAccountManager.java | 23 +- pom.xml | 1 + .../com/cloud/api/ApiAsyncJobDispatcher.java | 5 + .../main/java/com/cloud/api/ApiServer.java | 30 ++- .../com/cloud/user/AccountManagerImpl.java | 31 ++- .../java/com/cloud/vm/UserVmManagerImpl.java | 13 +- .../cloud/storage/StorageManagerImplTest.java | 26 +- .../snapshot/SnapshotManagerImplTest.java | 66 +++-- .../sharedfs/SharedFSServiceImplTest.java | 5 +- ui/src/main.js | 4 +- ui/src/utils/plugins.js | 23 +- .../exception/CloudRuntimeException.java | 19 ++ 24 files changed, 472 insertions(+), 103 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/context/ErrorMessageResolver.java create mode 100644 client/conf/error-messages.json.in diff --git a/api/src/main/java/com/cloud/exception/InvalidParameterValueException.java b/api/src/main/java/com/cloud/exception/InvalidParameterValueException.java index ce2bc31a7f1..7ad37b83a25 100644 --- a/api/src/main/java/com/cloud/exception/InvalidParameterValueException.java +++ b/api/src/main/java/com/cloud/exception/InvalidParameterValueException.java @@ -16,6 +16,10 @@ // under the License. package com.cloud.exception; +import java.util.Map; + +import org.apache.cloudstack.context.ErrorMessageResolver; + import com.cloud.utils.exception.CloudRuntimeException; public class InvalidParameterValueException extends CloudRuntimeException { @@ -26,4 +30,8 @@ public class InvalidParameterValueException extends CloudRuntimeException { super(message); } + public InvalidParameterValueException(String key, Map metadata) { + super(ErrorMessageResolver.getMessage(key, metadata), key, metadata); + } + } diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 09fe5ffc059..c5e5bd751c0 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -19,7 +19,6 @@ package com.cloud.user; import java.util.List; import java.util.Map; -import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -27,6 +26,7 @@ import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import com.cloud.dc.DataCenter; import com.cloud.domain.Domain; @@ -35,7 +35,7 @@ import com.cloud.network.vpc.VpcOffering; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; -import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import com.cloud.utils.Pair; public interface AccountService { @@ -85,6 +85,8 @@ public interface AccountService { boolean isRootAdmin(Long accountId); + boolean isRootAdmin(Account account); + boolean isDomainAdmin(Long accountId); boolean isResourceDomainAdmin(Long accountId); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java index e71b4feea03..2a25ce24da9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java @@ -648,7 +648,8 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme try { networkId = Long.parseLong(networkid); } catch (NumberFormatException e) { - throw new InvalidParameterValueException("Unable to translate and find entity with networkId: " + networkid); + throw new InvalidParameterValueException("vm.deploy.network.not.found.ip.map", + Map.of("networkId", networkid)); } } return networkId; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExceptionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExceptionResponse.java index d475dee0620..eee7890a3a4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ExceptionResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExceptionResponse.java @@ -18,13 +18,13 @@ package org.apache.cloudstack.api.response; import java.util.ArrayList; import java.util.List; - -import com.google.gson.annotations.SerializedName; +import java.util.Map; import org.apache.cloudstack.api.BaseResponse; import com.cloud.serializer.Param; import com.cloud.utils.exception.ExceptionProxyObject; +import com.google.gson.annotations.SerializedName; public class ExceptionResponse extends BaseResponse { @@ -44,6 +44,14 @@ public class ExceptionResponse extends BaseResponse { @Param(description = "The text associated with this error") private String errorText = "Command failed due to Internal Server Error"; + @SerializedName("errortextkey") + @Param(description = "the key for the text associated with this error") + private String errorTextKey = ""; + + @SerializedName("errormetadata") + @Param(description = "the metadata associated with this error") + private Map errorMetadata; + public ExceptionResponse() { idList = new ArrayList(); } @@ -64,6 +72,22 @@ public class ExceptionResponse extends BaseResponse { this.errorText = errorText; } + public String getErrorTextKey() { + return errorTextKey; + } + + public void setErrorTextKey(String errorTextKey) { + this.errorTextKey = errorTextKey; + } + + public Map getErrorMetadata() { + return errorMetadata; + } + + public void setErrorMetadata(Map errorMetadata) { + this.errorMetadata = errorMetadata; + } + public void addProxyObject(ExceptionProxyObject id) { idList.add(id); return; diff --git a/api/src/main/java/org/apache/cloudstack/context/CallContext.java b/api/src/main/java/org/apache/cloudstack/context/CallContext.java index 69376e4f6d7..9e3820876c4 100644 --- a/api/src/main/java/org/apache/cloudstack/context/CallContext.java +++ b/api/src/main/java/org/apache/cloudstack/context/CallContext.java @@ -23,17 +23,20 @@ import java.util.UUID; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.managed.threadlocal.ManagedThreadLocal; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.exception.CloudAuthenticationException; import com.cloud.projects.Project; import com.cloud.user.Account; +import com.cloud.user.AccountService; import com.cloud.user.User; import com.cloud.utils.UuidUtils; +import com.cloud.utils.component.ComponentContext; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; -import org.apache.logging.log4j.ThreadContext; /** * CallContext records information about the environment the call is made. This @@ -53,6 +56,7 @@ public class CallContext { private String contextId; private Account account; private long accountId; + private Boolean isAccountRootAdmin = null; private long startEventId = 0; private String eventDescription; private String eventDetails; @@ -134,6 +138,21 @@ public class CallContext { return account; } + public boolean isCallingAccountRootAdmin() { + if (isAccountRootAdmin == null) { + AccountService accountService; + try { + accountService = ComponentContext.getDelegateComponentOfType(AccountService.class); + } catch (NoSuchBeanDefinitionException e) { + LOGGER.warn("Falling back to account type check for isRootAdmin for account ID: {} as no AccountService bean found: {}", accountId, e.getMessage()); + Account caller = getCallingAccount(); + return caller != null && caller.getType() == Account.Type.ADMIN; + } + isAccountRootAdmin = accountService.isRootAdmin(getCallingAccount()); + } + return Boolean.TRUE.equals(isAccountRootAdmin); + } + public static CallContext current() { CallContext context = s_currentContext.get(); diff --git a/api/src/main/java/org/apache/cloudstack/context/ErrorMessageResolver.java b/api/src/main/java/org/apache/cloudstack/context/ErrorMessageResolver.java new file mode 100644 index 00000000000..da2e7579dfa --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/context/ErrorMessageResolver.java @@ -0,0 +1,241 @@ +// 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 java.io.File; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.api.response.ExceptionResponse; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public final 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 ObjectMapper MAPPER = new ObjectMapper(); + + // volatile for safe publication + private static volatile Map templates = + Collections.emptyMap(); + + private static volatile long lastModified = -1; + + private ErrorMessageResolver() { + } + + public static String getMessage(String errorKey, Map metadata) { + return getMessageUsingStringMap(errorKey, getStringMap(metadata)); + } + + private static String getTemplateForKey(String errorKey) { + if (errorKey == null) { + return null; + } + reloadIfRequired(); + if (!errorKey.endsWith(ERROR_KEY_ADMIN_SUFFIX) && CallContext.current().isCallingAccountRootAdmin()) { + String template = templates.get(errorKey + ERROR_KEY_ADMIN_SUFFIX); + if (template != null) { + return template; + } + } + return templates.get(errorKey); + } + + private static String getMessageUsingStringMap(String errorKey, Map metadata) { + String template = getTemplateForKey(errorKey); + if (template == null) { + return errorKey; + } + return expand(template, metadata); + } + + private static Map getStringMap(Map metadata) { + Map stringMap = new LinkedHashMap<>(); + if (MapUtils.isNotEmpty(metadata)) { + for (Map.Entry entry : metadata.entrySet()) { + Object value = entry.getValue(); + stringMap.put(entry.getKey(), getMetadataObjectStringValue(value)); + } + } + return stringMap; + } + + private 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(); + } + String name = null; + for (String getter : new String[]{"getDisplayText", "getDisplayName", "getName"}) { + name = invokeStringGetter(obj, getter); + if (name != null) { + break; + } + } + if (StringUtils.isEmpty(name)) { + if (StringUtils.isNotEmpty(uuid)) { + return uuid; + } + return obj.toString(); + } + + StringBuilder sb = new StringBuilder(); + sb.append("'").append(name).append("'"); + + Long id = null; + if (CallContext.current().isCallingAccountRootAdmin() && obj instanceof InternalIdentity) { + id = ((InternalIdentity) obj).getId(); + } + + if (id == null && uuid == null) { + return sb.toString(); + } + sb.append(" ("); + if (id != null) { + sb.append("ID: ").append(id); + if (uuid != null) { + sb.append(", "); + } + } + if (uuid != null) { + sb.append("UUID: ").append(uuid); + } + sb.append(")"); + + return sb.toString(); + } + + private static String invokeStringGetter(Object obj, String methodName) { + try { + Class cls = obj.getClass(); + var m = cls.getMethod(methodName); + Object val = m.invoke(obj); + return val == null ? null : val.toString(); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + return null; + } + } + + public static void updateExceptionResponse(ExceptionResponse response, CloudRuntimeException cre) { + String key = cre.getMessageKey(); + Map 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 stringMap = getStringMap(map); + String message = getMessageUsingStringMap(key, stringMap); + if (message != null) { + response.setErrorText(message); + } + response.setErrorMetadata(stringMap); + } + + private static synchronized void reloadIfRequired() { + try { + // log current directory for debugging purposes + LOG.debug("Current working directory: {}", + Paths.get(".").toAbsolutePath().normalize()); + File errorMessagesFile = PropertiesUtil.findConfigFile(ERROR_MESSAGES_FILENAME); + if (errorMessagesFile == null || !errorMessagesFile.exists()) { + if (!templates.isEmpty()) { + LOG.warn("Error messages file disappeared: {}", + errorMessagesFile != null ? errorMessagesFile.getAbsolutePath() : ERROR_MESSAGES_FILENAME); + templates = Collections.emptyMap(); + } + return; + } + + long modified = + Files.getLastModifiedTime(errorMessagesFile.toPath()).toMillis(); + + if (modified == lastModified) { + return; + } + + try (InputStream is = + Files.newInputStream(errorMessagesFile.toPath())) { + + templates = MAPPER.readValue( + is, + new TypeReference<>() { + } + ); + lastModified = modified; + + LOG.info("Reloaded {} error message templates from {}", + templates.size(), errorMessagesFile.toPath()); + } + + } catch (Exception e) { + LOG.warn("Failed to reload error messages from {}", + ERROR_MESSAGES_FILENAME, e); + } + } + + private static String expand(String template, Map metadata) { + if (metadata == null || metadata.isEmpty()) { + return template; + } + String result = template; + for (Map.Entry entry : metadata.entrySet()) { + String placeholder = "{{" + entry.getKey() + "}}"; + Object value = entry.getValue(); + if (value != null) { + result = result.replace(placeholder, value.toString()); + } + } + return result; + } +} diff --git a/client/conf/error-messages.json.in b/client/conf/error-messages.json.in new file mode 100644 index 00000000000..c3f3c276289 --- /dev/null +++ b/client/conf/error-messages.json.in @@ -0,0 +1,9 @@ +{ + "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.not.specified": "Unable to deploy Instance as the required parameter 'serviceofferingid' is missing.", + "vm.deploy.serviceoffering.inactive": "Unable to deploy Instance as the given service offering {{serviceOffering}} is inactive. Specify an active service offering.", + "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.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}}." +} \ No newline at end of file diff --git a/debian/cloudstack-management.install b/debian/cloudstack-management.install index befc7049c30..a1f09003c73 100644 --- a/debian/cloudstack-management.install +++ b/debian/cloudstack-management.install @@ -22,6 +22,7 @@ /etc/cloudstack/management/java.security.ciphers /etc/cloudstack/management/log4j-cloud.xml /etc/cloudstack/management/config.json +/etc/cloudstack/management/error-messages.json /etc/cloudstack/extensions/Proxmox/proxmox.sh /etc/cloudstack/extensions/HyperV/hyperv.py /etc/cloudstack/extensions/MaaS/maas.py diff --git a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java index cfe8049f5b2..261efbb1d30 100644 --- a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java @@ -438,7 +438,7 @@ public class ServiceOfferingVO implements ServiceOffering { @Override public String toString() { - return String.format("Service offering %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "name", "uuid")); + return String.format("Service offering %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "name", "uuid")); } public boolean isDynamicScalingEnabled() { diff --git a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java index e4fcbad6b02..fde9602fbb6 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -376,7 +376,7 @@ public class UserAccountVO implements UserAccount, InternalIdentity { @Override public String toString() { - return String.format("UserAccount %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields + return String.format("UserAccount %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields (this, "id", "uuid", "username", "accountName")); } } diff --git a/engine/schema/src/main/java/com/cloud/user/UserVO.java b/engine/schema/src/main/java/com/cloud/user/UserVO.java index d74aa7ed41b..770b7f18cc5 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java @@ -31,11 +31,11 @@ import javax.persistence.Table; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.lang3.StringUtils; import com.cloud.user.Account.State; import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; -import org.apache.commons.lang3.StringUtils; /** * A bean representing a user @@ -296,7 +296,7 @@ public class UserVO implements User, Identity, InternalIdentity { @Override public String toString() { - return String.format("User %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "username")); + return String.format("User %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "username")); } @Override diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index 13c0a36cb10..34be034a858 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -291,7 +291,7 @@ cp client/target/lib/*jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib/ rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/scripts rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/vms -for name in db.properties server.properties log4j-cloud.xml environment.properties java.security.ciphers +for name in db.properties server.properties log4j-cloud.xml environment.properties java.security.ciphers error-messages.json do cp client/target/conf/$name ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/$name done diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 2ff68b4836f..15c419f1c7a 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -17,30 +17,29 @@ package org.apache.cloudstack.network.contrail.management; +import java.net.InetAddress; import java.util.List; import java.util.Map; -import java.net.InetAddress; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; -import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; -import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; -import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; -import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; -import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; -import org.apache.cloudstack.framework.config.ConfigKey; - import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; +import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.api.query.vo.ControlledViewEntity; import com.cloud.configuration.ResourceLimit; import com.cloud.configuration.dao.ResourceCountDao; @@ -230,6 +229,12 @@ public class MockAccountManager extends ManagerBase implements AccountManager { return false; } + @Override + public boolean isRootAdmin(Account account) { + // TODO Auto-generated method stub + return false; + } + @Override public boolean isDomainAdmin(Long accountId) { // TODO Auto-generated method stub diff --git a/pom.xml b/pom.xml index bd31d107c91..3ebf278b756 100644 --- a/pom.xml +++ b/pom.xml @@ -1033,6 +1033,7 @@ CHANGES.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md + client/conf/error-messages.json.in build/build.number debian/cloudstack-agent.dirs debian/cloudstack-usage.dirs diff --git a/server/src/main/java/com/cloud/api/ApiAsyncJobDispatcher.java b/server/src/main/java/com/cloud/api/ApiAsyncJobDispatcher.java index e70a6b4da63..d45017ef024 100644 --- a/server/src/main/java/com/cloud/api/ApiAsyncJobDispatcher.java +++ b/server/src/main/java/com/cloud/api/ApiAsyncJobDispatcher.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.BaseAsyncCreateCmd; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.ExceptionResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.context.ErrorMessageResolver; import org.apache.cloudstack.framework.jobs.AsyncJob; import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; import org.apache.cloudstack.framework.jobs.AsyncJobManager; @@ -39,6 +40,7 @@ import com.cloud.user.User; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.db.EntityManager; +import com.cloud.utils.exception.CloudRuntimeException; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -131,6 +133,9 @@ public class ApiAsyncJobDispatcher extends AdapterBase implements AsyncJobDispat ExceptionResponse response = new ExceptionResponse(); response.setErrorCode(errorCode); response.setErrorText(errorMsg); + if (e instanceof CloudRuntimeException) { + ErrorMessageResolver.updateExceptionResponse(response, (CloudRuntimeException) e); + } response.setResponseName((cmdObj == null) ? "unknowncommandresponse" : cmdObj.getCommandName()); _asyncJobMgr.completeAsyncJob(job.getId(), JobInfo.Status.FAILED, errorCode, ApiSerializerHelper.toSerializedString(response)); diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 05ea166e13b..5a4cf6847e3 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -16,6 +16,9 @@ // under the License. package com.cloud.api; +import static com.cloud.user.AccountManagerImpl.apiKeyAccess; +import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InterruptedIOException; @@ -31,6 +34,7 @@ import java.security.SecureRandom; import java.security.Security; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -39,7 +43,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Arrays; import java.util.Map; import java.util.Set; import java.util.TimeZone; @@ -58,15 +61,6 @@ import javax.naming.ConfigurationException; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import com.cloud.cluster.ManagementServerHostVO; -import com.cloud.cluster.dao.ManagementServerHostDao; -import com.cloud.user.Account; -import com.cloud.user.AccountManager; -import com.cloud.user.AccountManagerImpl; -import com.cloud.user.DomainManager; -import com.cloud.user.User; -import com.cloud.user.UserAccount; -import com.cloud.user.UserVO; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -106,6 +100,7 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.context.ErrorMessageResolver; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.events.EventDistributor; @@ -156,6 +151,8 @@ import org.springframework.stereotype.Component; import com.cloud.api.dispatch.DispatchChainFactory; import com.cloud.api.dispatch.DispatchTask; import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; @@ -174,11 +171,18 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.UnavailableCommandException; import com.cloud.projects.dao.ProjectDao; import com.cloud.storage.VolumeApiService; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountManagerImpl; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserVO; import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.DateUtil; import com.cloud.utils.HttpUtils; -import com.cloud.utils.HttpUtils.ApiSessionKeySameSite; import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption; +import com.cloud.utils.HttpUtils.ApiSessionKeySameSite; import com.cloud.utils.Pair; import com.cloud.utils.ReflectUtil; import com.cloud.utils.StringUtils; @@ -194,9 +198,6 @@ import com.cloud.utils.exception.ExceptionProxyObject; import com.cloud.utils.net.NetUtils; import com.google.gson.reflect.TypeToken; -import static com.cloud.user.AccountManagerImpl.apiKeyAccess; -import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; - @Component public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService, Configurable { private static final Logger ACCESSLOGGER = LogManager.getLogger("apiserver." + ApiServer.class.getName()); @@ -1670,6 +1671,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer apiResponse.setErrorCode(ex.getErrorCode().getHttpCode()); apiResponse.setErrorText(ex.getDescription()); apiResponse.setResponseName(responseName); + ErrorMessageResolver.updateExceptionResponse(apiResponse, ex); final ArrayList idList = ex.getIdProxyList(); if (idList != null) { for (ExceptionProxyObject exceptionProxyObject : idList) { diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index c485aae87ec..8d56b092c76 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -598,21 +598,26 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public boolean isRootAdmin(Long accountId) { if (accountId != null) { - AccountVO acct = _accountDao.findById(accountId); - if (acct == null) { - return false; //account is deleted or does not exist - } - for (SecurityChecker checker : _securityCheckers) { - try { - if (checker.checkAccess(acct, null, null, "SystemCapability")) { - if (logger.isTraceEnabled()) { - logger.trace("Root Access granted to " + acct + " by " + checker.getName()); - } - return true; + return isRootAdmin(_accountDao.findById(accountId)); + } + return false; + } + + @Override + public boolean isRootAdmin(Account account) { + if (account == null) { + return false; //account is deleted or does not exist + } + for (SecurityChecker checker : _securityCheckers) { + try { + if (checker.checkAccess(account, null, null, "SystemCapability")) { + if (logger.isTraceEnabled()) { + logger.trace("Root Access granted to " + account + " by " + checker.getName()); } - } catch (PermissionDeniedException ex) { - return false; + return true; } + } catch (PermissionDeniedException ex) { + return false; } } return false; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 83cce4351b2..0aa9d91939e 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -6323,18 +6323,23 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private void verifyServiceOffering(BaseDeployVMCmd cmd, ServiceOffering serviceOffering) { if (ServiceOffering.State.Inactive.equals(serviceOffering.getState())) { - throw new InvalidParameterValueException(String.format("Service offering is inactive: [%s].", serviceOffering.getUuid())); + throw new InvalidParameterValueException("vm.deploy.serviceoffering.inactive", + Map.of("serviceOffering", serviceOffering)); } Long overrideDiskOfferingId = cmd.getOverrideDiskOfferingId(); if (serviceOffering.getDiskOfferingStrictness() && overrideDiskOfferingId != null) { - throw new InvalidParameterValueException(String.format("Cannot override disk offering id %d since provided service offering is strictly mapped to its disk offering", overrideDiskOfferingId)); + throw new InvalidParameterValueException("vm.deploy.serviceoffering.override.not.allowed", + Map.of("serviceOffering", serviceOffering)); } if (!serviceOffering.isDynamic()) { for (String detail: cmd.getDetails().keySet()) { if (detail.equalsIgnoreCase(VmDetailConstants.CPU_NUMBER) || detail.equalsIgnoreCase(VmDetailConstants.CPU_SPEED) || detail.equalsIgnoreCase(VmDetailConstants.MEMORY)) { - throw new InvalidParameterValueException("cpuNumber or cpuSpeed or memory should not be specified for static service offering"); + throw new InvalidParameterValueException("vm.deploy.serviceoffering.fixed.parameters.not.allowed", + Map.of("cpuNumberKey", VmDetailConstants.CPU_NUMBER, + "cpuSpeedKey", VmDetailConstants.CPU_NUMBER, + "memoryKey", VmDetailConstants.CPU_NUMBER)); } } } @@ -6382,7 +6387,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Long serviceOfferingId = cmd.getServiceOfferingId(); if (serviceOfferingId == null) { - throw new InvalidParameterValueException("Unable to execute API command deployvirtualmachine due to missing parameter serviceofferingid"); + throw new InvalidParameterValueException("vm.deploy.serviceoffering.not.specified", Collections.emptyMap()); } Long overrideDiskOfferingId = cmd.getOverrideDiskOfferingId(); diff --git a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java index 8f88800d549..9ec4932b8ec 100644 --- a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java @@ -16,6 +16,12 @@ // under the License. package com.cloud.storage; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; + import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; @@ -24,12 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import com.cloud.dc.HostPodVO; -import com.cloud.dc.dao.HostPodDao; -import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; -import com.cloud.resource.ResourceManager; -import com.cloud.storage.dao.StoragePoolAndAccessGroupMapDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd; import org.apache.cloudstack.api.command.admin.storage.ConfigureStorageAccessCmd; @@ -72,9 +72,11 @@ import com.cloud.capacity.CapacityVO; import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; +import com.cloud.dc.HostPodVO; import com.cloud.dc.VsphereStoragePolicyVO; import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.HostPodDao; import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.ConnectionException; @@ -83,8 +85,12 @@ import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.StorageUnavailableException; import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.resource.ResourceManager; +import com.cloud.storage.dao.StoragePoolAndAccessGroupMapDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.user.AccountManagerImpl; import com.cloud.utils.Pair; @@ -93,12 +99,6 @@ import com.cloud.vm.DiskProfile; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.doReturn; - @RunWith(MockitoJUnitRunner.class) public class StorageManagerImplTest { @@ -667,7 +667,7 @@ public class StorageManagerImplTest { final DataCenterVO zone = new DataCenterVO(1L, null, null, null, null, null, null, null, null, null, DataCenter.NetworkType.Advanced, null, null); StoragePoolVO primaryStorage = mockStoragePoolVOForChangeStoragePoolScope(currentScope, status); - Mockito.when(accountMgr.isRootAdmin(Mockito.any())).thenReturn(true); + Mockito.when(accountMgr.isRootAdmin(Mockito.anyLong())).thenReturn(true); Mockito.when(dataCenterDao.findById(1L)).thenReturn(zone); Mockito.when(storagePoolDao.findById(1L)).thenReturn(primaryStorage); } diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java index ff0888c184c..2610c7a1b26 100644 --- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java @@ -16,34 +16,10 @@ // under the License. package com.cloud.storage.snapshot; -import com.cloud.dc.DataCenter; -import com.cloud.dc.DataCenterVO; -import com.cloud.dc.dao.DataCenterDao; -import com.cloud.event.ActionEventUtils; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.exception.ResourceUnavailableException; -import com.cloud.org.Grouping; -import com.cloud.storage.DataStoreRole; -import com.cloud.storage.Snapshot; -import com.cloud.storage.SnapshotPolicyVO; -import com.cloud.storage.SnapshotVO; -import com.cloud.storage.VolumeVO; -import com.cloud.server.TaggedResourceService; -import com.cloud.storage.dao.SnapshotDao; -import com.cloud.storage.dao.SnapshotPolicyDao; -import com.cloud.storage.dao.SnapshotZoneDao; -import com.cloud.storage.dao.VolumeDao; -import com.cloud.user.Account; -import com.cloud.user.AccountManager; -import com.cloud.user.AccountVO; -import com.cloud.user.ResourceLimitService; -import com.cloud.user.User; -import com.cloud.user.dao.AccountDao; -import com.cloud.utils.Pair; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd; import org.apache.cloudstack.context.CallContext; @@ -57,13 +33,11 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService; import org.apache.cloudstack.framework.async.AsyncCallFuture; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; - -import org.junit.Assert; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -71,9 +45,33 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; +import com.cloud.dc.DataCenter; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.event.ActionEventUtils; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.org.Grouping; +import com.cloud.server.TaggedResourceService; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Snapshot; +import com.cloud.storage.SnapshotPolicyVO; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.SnapshotPolicyDao; +import com.cloud.storage.dao.SnapshotZoneDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.ResourceLimitService; +import com.cloud.user.User; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.Pair; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; @RunWith(MockitoJUnitRunner.class) public class SnapshotManagerImplTest { @@ -242,7 +240,7 @@ public class SnapshotManagerImplTest { DataCenterVO zone1 = Mockito.mock(DataCenterVO.class); Mockito.when(zone1.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled); Mockito.when(dataCenterDao.findById(2L)).thenReturn(zone1); - Mockito.when(accountManager.isRootAdmin(Mockito.any())).thenReturn(false); + Mockito.when(accountManager.isRootAdmin(Mockito.anyLong())).thenReturn(false); snapshotManager.validatePolicyZones(List.of(2L), null, volumeVO, Mockito.mock(Account.class)); } diff --git a/server/src/test/java/org/apache/cloudstack/storage/sharedfs/SharedFSServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/storage/sharedfs/SharedFSServiceImplTest.java index 88493d10038..2d54afbf0ea 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/sharedfs/SharedFSServiceImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/storage/sharedfs/SharedFSServiceImplTest.java @@ -18,11 +18,12 @@ package org.apache.cloudstack.storage.sharedfs; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -618,7 +619,7 @@ public class SharedFSServiceImplTest { when(sharedFSJoinDao.searchByIds(List.of(s_sharedFSId).toArray(new Long[0]))).thenReturn(List.of(sharedFSJoinVO)); when(owner.getId()).thenReturn(s_ownerId); - when(accountMgr.isRootAdmin(any())).thenReturn(true); + when(accountMgr.isRootAdmin(anyLong())).thenReturn(true); when(sharedFSJoinDao.createSharedFSResponses(any(), any())).thenReturn(null); ListSharedFSCmd cmd = getMockListSharedFSCmd(); diff --git a/ui/src/main.js b/ui/src/main.js index 7441f801086..69b5dbe4dd4 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -41,7 +41,8 @@ import { cpuArchitectureUtilPlugin, imagesUtilPlugin, extensionsUtilPlugin, - backupUtilPlugin + backupUtilPlugin, + localeErrorUtilPlugin } from './utils/plugins' import { VueAxios } from './utils/request' import directives from './utils/directives' @@ -65,6 +66,7 @@ vueApp.use(cpuArchitectureUtilPlugin) vueApp.use(imagesUtilPlugin) vueApp.use(extensionsUtilPlugin) vueApp.use(backupUtilPlugin) +vueApp.use(localeErrorUtilPlugin) vueApp.use(extensions) vueApp.use(directives) diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js index 729cef84d02..aed74ffd1e4 100644 --- a/ui/src/utils/plugins.js +++ b/ui/src/utils/plugins.js @@ -229,7 +229,8 @@ export const notifierPlugin = { } else if (error.response.data) { const responseKey = _.findKey(error.response.data, 'errortext') if (responseKey) { - desc = error.response.data[responseKey].errortext + const errObj = error.response.data[responseKey] + desc = this.$toLocaleError(errObj.errortext, errObj.errortextkey, errObj.errormetadata) } else if (typeof error.response.data === 'string') { desc = error.response.data } @@ -620,3 +621,23 @@ export const backupUtilPlugin = { } } } + +export const localeErrorUtilPlugin = { + install (app) { + app.config.globalProperties.$toLocaleError = function (msg, key, params) { + if (!key) { + return msg + } + let localeMsg = i18n.global.t(key) + if (!localeMsg || localeMsg === key) { + return msg + } + if (params && params.constructor === Object) { + for (const paramKey in params) { + localeMsg = localeMsg.replace(`{{${paramKey}}}`, params[paramKey]) + } + } + return localeMsg + } + } +} diff --git a/utils/src/main/java/com/cloud/utils/exception/CloudRuntimeException.java b/utils/src/main/java/com/cloud/utils/exception/CloudRuntimeException.java index dd5abc84f5f..5350848944e 100644 --- a/utils/src/main/java/com/cloud/utils/exception/CloudRuntimeException.java +++ b/utils/src/main/java/com/cloud/utils/exception/CloudRuntimeException.java @@ -24,6 +24,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; import com.cloud.utils.Pair; import com.cloud.utils.SerialVersionUID; @@ -42,6 +43,9 @@ public class CloudRuntimeException extends RuntimeException implements ErrorCont protected int csErrorCode; + protected String messageKey = null; + protected Map metadata = null; + public CloudRuntimeException(String message) { super(message); setCSErrorCode(CSExceptionErrorCode.getCSErrCode(this.getClass().getName())); @@ -52,6 +56,13 @@ public class CloudRuntimeException extends RuntimeException implements ErrorCont setCSErrorCode(CSExceptionErrorCode.getCSErrCode(this.getClass().getName())); } + public CloudRuntimeException(String message, String messageKey, Map metadata) { + super(message); + this.messageKey = messageKey; + this.metadata = metadata; + setCSErrorCode(CSExceptionErrorCode.getCSErrCode(this.getClass().getName())); + } + protected CloudRuntimeException() { super(); @@ -138,4 +149,12 @@ public class CloudRuntimeException extends RuntimeException implements ErrorCont uuidList.add(new Pair, String>(Class.forName(clzName), val)); } } + + public String getMessageKey() { + return messageKey; + } + + public Map getMetadata() { + return metadata; + } }