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 <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2025-12-24 12:53:49 +05:30
parent 8e933b735e
commit 6fb1551b84
24 changed files with 472 additions and 103 deletions

View File

@ -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<String, Object> metadata) {
super(ErrorMessageResolver.getMessage(key, metadata), key, metadata);
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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<String, String> errorMetadata;
public ExceptionResponse() {
idList = new ArrayList<ExceptionProxyObject>();
}
@ -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<String, String> getErrorMetadata() {
return errorMetadata;
}
public void setErrorMetadata(Map<String, String> errorMetadata) {
this.errorMetadata = errorMetadata;
}
public void addProxyObject(ExceptionProxyObject id) {
idList.add(id);
return;

View File

@ -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();

View File

@ -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<String, String> templates =
Collections.emptyMap();
private static volatile long lastModified = -1;
private ErrorMessageResolver() {
}
public static String getMessage(String errorKey, Map<String, Object> 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<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) {
Map<String, String> stringMap = new LinkedHashMap<>();
if (MapUtils.isNotEmpty(metadata)) {
for (Map.Entry<String, Object> 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<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() {
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<String, String> metadata) {
if (metadata == null || metadata.isEmpty()) {
return template;
}
String result = template;
for (Map.Entry<String, String> entry : metadata.entrySet()) {
String placeholder = "{{" + entry.getKey() + "}}";
Object value = entry.getValue();
if (value != null) {
result = result.replace(placeholder, value.toString());
}
}
return result;
}
}

View File

@ -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}}."
}

View File

@ -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

View File

@ -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() {

View File

@ -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"));
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1033,6 +1033,7 @@
<exclude>CHANGES.md</exclude>
<exclude>ISSUE_TEMPLATE.md</exclude>
<exclude>PULL_REQUEST_TEMPLATE.md</exclude>
<exclude>client/conf/error-messages.json.in</exclude>
<exclude>build/build.number</exclude>
<exclude>debian/cloudstack-agent.dirs</exclude>
<exclude>debian/cloudstack-usage.dirs</exclude>

View File

@ -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));

View File

@ -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<ExceptionProxyObject> idList = ex.getIdProxyList();
if (idList != null) {
for (ExceptionProxyObject exceptionProxyObject : idList) {

View File

@ -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;

View File

@ -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();

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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();

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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<String, Object> 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<String, Object> 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<Class<?>, String>(Class.forName(clzName), val));
}
}
public String getMessageKey() {
return messageKey;
}
public Map<String, Object> getMetadata() {
return metadata;
}
}