Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2af10c0
framework,api,server,ui: improve error messaging
shwstppr Dec 24, 2025
af19de9
wip
shwstppr Sep 15, 2026
8786931
allow plugin specific error messages files
shwstppr Sep 16, 2026
0e4db7f
allow configuration for behaviour
shwstppr Sep 16, 2026
45efa6d
add smoke test
shwstppr Sep 18, 2026
e669190
Merge remote-tracking branch 'apache/4.22' into improve-error-messages
shwstppr Sep 18, 2026
b99c315
add documentation
shwstppr Sep 18, 2026
eff68e0
fixes and admin only variant
shwstppr Sep 18, 2026
3102fda
fix builds
shwstppr Sep 18, 2026
b4034b8
fix missing license
shwstppr Sep 18, 2026
2fdc492
update version
shwstppr Sep 28, 2026
8602340
Merge remote-tracking branch 'apache/4.22' into improve-error-messages
shwstppr Sep 28, 2026
1805cce
fic build
shwstppr Sep 28, 2026
c063fc4
Merge branch '4.22' into improve-error-messages
shwstppr Oct 5, 2026
f6fc9ad
fix failing test
shwstppr Oct 5, 2026
5c4269f
Merge remote-tracking branch 'apache/4.22' into improve-error-messages
shwstppr Oct 6, 2026
037b968
localization fix for non-async APIs lost in rebase
shwstppr Oct 8, 2026
9447c67
fix il8n error
shwstppr Oct 8, 2026
74771e0
enhance error messages script and fix issues
shwstppr Oct 19, 2026
ea881f7
address comments
shwstppr Oct 20, 2026
c453d03
Merge remote-tracking branch 'apache/4.22' into improve-error-messages
shwstppr Oct 28, 2026
8757e3c
fix
shwstppr Oct 28, 2026
aa3dd38
fix
shwstppr Oct 28, 2026
a989180
api,server,client: fix issues around missing placeholders
shwstppr Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions api/src/main/java/com/cloud/configuration/Resource.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,45 @@ public interface Resource {
String UNLIMITED = "Unlimited";

enum ResourceType { // All storage type resources are allocated_storage and not the physical storage.
user_vm("user_vm", 0),
public_ip("public_ip", 1),
volume("volume", 2),
snapshot("snapshot", 3),
template("template", 4),
project("project", 5),
network("network", 6),
vpc("vpc", 7),
cpu("cpu", 8),
memory("memory", 9),
primary_storage("primary_storage", 10),
secondary_storage("secondary_storage", 11),
backup("backup", 12),
backup_storage("backup_storage", 13),
bucket("bucket", 14),
object_storage("object_storage", 15),
gpu("gpu", 16);
user_vm("user_vm", "Instance", 0),
public_ip("public_ip", "Public IP", 1),
volume("volume", "Volume", 2),
snapshot("snapshot", "Snapshot", 3),
template("template", "Template", 4),
project("project", "Project", 5),
network("network", "Network", 6),
vpc("vpc", "VPC", 7),
cpu("cpu", "CPU", 8),
memory("memory", "Memory", 9),
primary_storage("primary_storage", "Primary Storage", 10),
secondary_storage("secondary_storage", "Secondary Storage", 11),
backup("backup", "Backup", 12),
backup_storage("backup_storage", "Backup Storage", 13),
bucket("bucket", "Bucket", 14),
object_storage("object_storage", "Object Storage", 15),
gpu("gpu", "GPU", 16);

private String name;
private String displayName;
private int ordinal;
public static final long bytesToKiB = 1024;
public static final long bytesToMiB = bytesToKiB * 1024;
public static final long bytesToGiB = bytesToMiB * 1024;

ResourceType(String name, int ordinal) {
ResourceType(String name, String displayName, int ordinal) {
this.name = name;
this.displayName = displayName;
this.ordinal = ordinal;
}

public String getName() {
return name;
}

public String getDisplayName() {
return displayName;
}

public int getOrdinal() {
return ordinal;
}
Expand Down
6 changes: 4 additions & 2 deletions api/src/main/java/com/cloud/user/AccountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
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;
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;
Expand All @@ -35,7 +35,7 @@
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 {

Expand Down Expand Up @@ -85,6 +85,8 @@ User createUser(String userName, String password, String firstName, String lastN

boolean isRootAdmin(Long accountId);

boolean isRootAdmin(Account account);

boolean isDomainAdmin(Long accountId);

boolean isResourceDomainAdmin(Long accountId);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.error.Exceptions;

import com.cloud.configuration.Resource;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientServerCapacityException;
Expand Down Expand Up @@ -157,7 +159,19 @@ public void create() throws ResourceAllocationException {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage());
} catch (ResourceAllocationException ex) {
logger.warn("Exception: ", ex);
throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage());
handleCreateResourceAllocationException(ex);
}
}

protected void handleCreateResourceAllocationException(ResourceAllocationException ex) {
Object cause = CallContext.current().getErrorContextParameters().get("resourceLimitCause");
if (Resource.ResourceOwnerType.Account.equals(cause)) {
throw Exceptions.serverApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR,
"vm.deploy.resourcelimit.exceeded.account");
} else if (Resource.ResourceOwnerType.Domain.equals(cause)) {
throw Exceptions.serverApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR,
"vm.deploy.resourcelimit.exceeded.domain");
}
throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
package org.apache.cloudstack.api.command.user.vm;

import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.user.Account;
import com.cloud.uservm.UserVm;
import com.cloud.utils.StringUtils;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.Dhcp;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
Expand All @@ -43,6 +41,7 @@
import org.apache.cloudstack.api.response.UserDataResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.error.Exceptions;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.lang3.EnumUtils;

Expand Down Expand Up @@ -305,12 +304,7 @@ public long getEntityOwnerId() {
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException {
CallContext.current().setEventDetails("Instance Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()));
UserVm result = null;
try {
result = _userVmService.updateVirtualMachine(this);
} catch (CloudRuntimeException e) {
throw new CloudRuntimeException(String.format("Failed to update Instance, due to: %s", e.getLocalizedMessage()), e);
}
UserVm result = _userVmService.updateVirtualMachine(this);
if (result != null) {
UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", result).get(0);
response.setResponseName(getCommandName());
Expand Down Expand Up @@ -348,8 +342,9 @@ public VMLeaseManager.ExpiryAction getLeaseExpiryAction() {
}
VMLeaseManager.ExpiryAction action = EnumUtils.getEnumIgnoreCase(VMLeaseManager.ExpiryAction.class, leaseExpiryAction);
if (action == null) {
throw new InvalidParameterValueException("Invalid value configured for leaseexpiryaction, valid values are: " +
com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values()));
throw Exceptions.invalidParameterValueException("vm.update.lease.expiry.action.invalid", Map.of(
"value", leaseExpiryAction,
"validValues", com.cloud.utils.EnumUtils.listValues(VMLeaseManager.ExpiryAction.values())));
}
return action;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

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 {

Expand All @@ -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", since = "4.22.2")
private String errorTextKey;

@SerializedName("errormetadata")
@Param(description = "the metadata associated with this error", since = "4.22.2")
private Map<String, String> errorMetadata;

public ExceptionResponse() {
idList = new ArrayList<ExceptionProxyObject>();
}
Expand All @@ -64,6 +72,22 @@ public void setErrorText(String errorText) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ public class ApiServiceConfiguration implements Configurable {
"true", "Are the source checks on API calls enabled (true) or not (false)? See api.allowed.source.cidr.list", true, ConfigKey.Scope.Global);
public static final ConfigKey<String> ApiAllowedSourceCidrList = new ConfigKey<>(String.class, "api.allowed.source.cidr.list", "Advanced",
"0.0.0.0/0,::/0", "Comma separated list of IPv4/IPv6 CIDRs from which API calls can be performed. Can be set on Global and Account levels.", true, ConfigKey.Scope.Account, null, null, null, null, null, ConfigKey.Kind.CSV, null);
public static final ConfigKey<Boolean> ErrorMessageMetadataPreferToString = new ConfigKey<>("Advanced", Boolean.class,
"error.message.metadata.prefer.tostring", "false",
"When true, structured error message metadata values prefer the object's toString() representation " +
"over reflection-based display-name lookup (getDisplayName/getName/getDisplayText). " +
"Requires a management server restart to take effect.", false, ConfigKey.Scope.Global);
public static final ConfigKey<Boolean> ErrorMessageMetadataIncludeIdForAdmins = new ConfigKey<>("Advanced", Boolean.class,
"error.message.metadata.include.id.for.admins", "true",
"When true, root admins additionally see the internal database ID (alongside the UUID) for objects " +
"referenced in structured error message metadata. Requires a management server restart to take effect.",
false, ConfigKey.Scope.Global);


public static void validateEndpointUrl() {
Expand All @@ -55,7 +65,8 @@ public String getConfigComponentName() {

@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {ManagementServerAddresses, ApiServletPath, DefaultUIPageSize, ApiSourceCidrChecksEnabled, ApiAllowedSourceCidrList};
return new ConfigKey<?>[] {ManagementServerAddresses, ApiServletPath, DefaultUIPageSize, ApiSourceCidrChecksEnabled, ApiAllowedSourceCidrList,
ErrorMessageMetadataPreferToString, ErrorMessageMetadataIncludeIdForAdmins};
}

}
44 changes: 42 additions & 2 deletions api/src/main/java/org/apache/cloudstack/context/CallContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@

import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.managed.threadlocal.ManagedThreadLocal;
import org.apache.logging.log4j.Logger;
import org.apache.commons.collections.MapUtils;
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
Expand All @@ -53,6 +57,7 @@ protected Stack<CallContext> initialValue() {
private String contextId;
private Account account;
private long accountId;
private Boolean isAccountRootAdmin = null;
private long startEventId = 0;
private String eventDescription;
private String eventDetails;
Expand All @@ -63,6 +68,7 @@ protected Stack<CallContext> initialValue() {
private User user;
private long userId;
private final Map<Object, Object> context = new HashMap<Object, Object>();
private final Map<String, Object> errorContext = new HashMap<>();
private Project project;
private String apiName;

Expand Down Expand Up @@ -134,6 +140,25 @@ public Account getCallingAccount() {
return account;
}

public boolean isCallingAccountRootAdmin() {
if (isAccountRootAdmin != null) {
return isAccountRootAdmin;
}
if (account == null && s_entityMgr == null) {
return false;
}
Account caller = getCallingAccount();
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());
return caller != null && caller.getType() == Account.Type.ADMIN;
}
isAccountRootAdmin = accountService.isRootAdmin(caller);
return isAccountRootAdmin;
}

public static CallContext current() {
CallContext context = s_currentContext.get();

Expand Down Expand Up @@ -405,6 +430,21 @@ public void putContextParameters(Map<Object, Object> details){
}
}

public Map<String, Object> getErrorContextParameters() {
return errorContext;
}

public void putErrorContextParameter(String key, Object value) {
errorContext.put(key, value);
}

public void putErrorContextParameters(Map<String, Object> details) {
if (MapUtils.isEmpty(details)) {
return;
}
errorContext.putAll(details);
}

public static void setActionEventInfo(String eventType, String description) {
CallContext context = CallContext.current();
if (context != null) {
Expand Down
Loading
Loading