From 7c028f4a96577545a2113aa1cbabc88478f6f7fb Mon Sep 17 00:00:00 2001 From: mprokopchuk Date: Mon, 6 Apr 2026 23:49:32 -0700 Subject: [PATCH 1/5] Add resource filtering to async job query commands --- .../command/user/job/ListAsyncJobsCmd.java | 15 +++ .../user/job/QueryAsyncJobResultCmd.java | 18 ++- .../framework/jobs/dao/AsyncJobDao.java | 18 +++ .../framework/jobs/dao/AsyncJobDaoImpl.java | 33 +++++ .../java/com/cloud/api/ApiResponseHelper.java | 36 ++++- .../com/cloud/api/query/QueryManagerImpl.java | 41 +++--- .../cloud/api/query/ResourceIdSupport.java | 123 ++++++++++++++++++ 7 files changed, 260 insertions(+), 24 deletions(-) create mode 100644 server/src/main/java/com/cloud/api/query/ResourceIdSupport.java diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java index b55d1b234f19..85fbb587a4c1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java @@ -19,6 +19,7 @@ import java.util.Date; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; import org.apache.cloudstack.api.Parameter; @@ -40,6 +41,12 @@ public class ListAsyncJobsCmd extends BaseListAccountResourcesCmd { @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = CommandType.UUID, entityType = ManagementServerResponse.class, description = "The id of the management server", since="4.19") private Long managementServerId; + @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the event", since="4.22.1") + private String resourceId; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the event", since="4.22.1") + private String resourceType; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -52,6 +59,14 @@ public Long getManagementServerId() { return managementServerId; } + public String getResourceId() { + return resourceId; + } + + public String getResourceType() { + return resourceType; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java index 93a443757212..db1a1898b2bb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java @@ -16,8 +16,8 @@ // under the License. package org.apache.cloudstack.api.command.user.job; - import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; @@ -34,9 +34,15 @@ public class QueryAsyncJobResultCmd extends BaseCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType = AsyncJobResponse.class, required = true, description = "The ID of the asynchronous job") + @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType = AsyncJobResponse.class, description = "The ID of the asynchronous job") private Long id; + @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the event", since="4.22.1") + private String resourceId; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the event", since="4.22.1") + private String resourceType; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -45,6 +51,14 @@ public Long getId() { return id; } + public String getResourceId() { + return resourceId; + } + + public String getResourceType() { + return resourceType; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java index 9f7a4ad6e058..926280bfeade 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java @@ -23,12 +23,30 @@ import com.cloud.utils.db.GenericDao; +import javax.annotation.Nullable; + public interface AsyncJobDao extends GenericDao { AsyncJobVO findInstancePendingAsyncJob(String instanceType, long instanceId); List findInstancePendingAsyncJobs(String instanceType, Long accountId); + /** + * Finds async job matching the given parameters. + * Non-null parameters are added to search criteria. + * Returns the most recent job by creation date. + *

+ * When searching by resourceId and resourceType, only one active job + * is expected per resource, so returning a single result is sufficient. + * + * @param id job ID + * @param resourceId resource ID (instanceId) + * @param resourceType resource type (instanceType) + * @return matching job or null + */ + @Nullable + AsyncJobVO findJob(Long id, Long resourceId, String resourceType); + AsyncJobVO findPseudoJob(long threadId, long msid); void cleanupPseduoJobs(long msid); diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java index a2f1f36b8637..2f6dcf6b6505 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java @@ -22,6 +22,8 @@ import java.util.List; import org.apache.cloudstack.api.ApiConstants; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.jobs.JobInfo; @@ -45,6 +47,7 @@ public class AsyncJobDaoImpl extends GenericDaoBase implements private final SearchBuilder expiringUnfinishedAsyncJobSearch; private final SearchBuilder expiringCompletedAsyncJobSearch; private final SearchBuilder failureMsidAsyncJobSearch; + private final SearchBuilder byIdResourceIdResourceTypeSearch; private final GenericSearchBuilder asyncJobTypeSearch; private final GenericSearchBuilder pendingNonPseudoAsyncJobsSearch; @@ -95,6 +98,12 @@ public AsyncJobDaoImpl() { failureMsidAsyncJobSearch.and("job_cmd", failureMsidAsyncJobSearch.entity().getCmd(), Op.IN); failureMsidAsyncJobSearch.done(); + byIdResourceIdResourceTypeSearch = createSearchBuilder(); + byIdResourceIdResourceTypeSearch.and("id", byIdResourceIdResourceTypeSearch.entity().getId(), SearchCriteria.Op.EQ); + byIdResourceIdResourceTypeSearch.and("instanceId", byIdResourceIdResourceTypeSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + byIdResourceIdResourceTypeSearch.and("instanceType", byIdResourceIdResourceTypeSearch.entity().getInstanceType(), SearchCriteria.Op.EQ); + byIdResourceIdResourceTypeSearch.done(); + asyncJobTypeSearch = createSearchBuilder(Long.class); asyncJobTypeSearch.select(null, SearchCriteria.Func.COUNT, asyncJobTypeSearch.entity().getId()); asyncJobTypeSearch.and("job_info", asyncJobTypeSearch.entity().getCmdInfo(),Op.LIKE); @@ -140,6 +149,30 @@ public List findInstancePendingAsyncJobs(String instanceType, Long a return listBy(sc); } + @Override + public AsyncJobVO findJob(Long id, Long resourceId, String resourceType) { + SearchCriteria sc = byIdResourceIdResourceTypeSearch.create(); + + if (id == null && resourceId == null && StringUtils.isNotBlank(resourceType)) { + logger.debug("findJob called with all null parameters"); + return null; + } + + if (id != null) { + sc.setParameters("id", id); + } + if (resourceId != null && StringUtils.isNotBlank(resourceType)) { + sc.setParameters("instanceType", resourceType); + sc.setParameters("instanceId", resourceId); + } + Filter filter = new Filter(AsyncJobVO.class, "created", false, 0L, 1L); + List result = searchIncludingRemoved(sc, filter, Boolean.FALSE, false); + if (CollectionUtils.isNotEmpty(result)) { + return result.get(0); + } + return null; + } + @Override public AsyncJobVO findPseudoJob(long threadId, long msid) { SearchCriteria sc = pseudoJobSearch.create(); diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index bd8c32b390b8..1805929ff205 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.function.Consumer; @@ -39,6 +40,7 @@ import javax.inject.Inject; +import com.cloud.api.query.ResourceIdSupport; import com.cloud.bgp.ASNumber; import com.cloud.bgp.ASNumberRange; import com.cloud.configuration.ConfigurationService; @@ -57,6 +59,7 @@ import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants.DomainDetails; import org.apache.cloudstack.api.ApiConstants.HostDetails; @@ -219,6 +222,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.framework.jobs.AsyncJob; import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.dao.AsyncJobDao; import org.apache.cloudstack.gui.theme.GuiThemeJoin; import org.apache.cloudstack.management.ManagementServerHost; import org.apache.cloudstack.network.BgpPeerVO; @@ -447,7 +451,7 @@ import sun.security.x509.X509CertImpl; -public class ApiResponseHelper implements ResponseGenerator { +public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport { protected Logger logger = LogManager.getLogger(ApiResponseHelper.class); private static final DecimalFormat s_percentFormat = new DecimalFormat("##.##"); @@ -529,6 +533,8 @@ public class ApiResponseHelper implements ResponseGenerator { RoutedIpv4Manager routedIpv4Manager; @Inject ResourceIconManager resourceIconManager; + @Inject + AsyncJobDao asyncJobDao; public static String getPrettyDomainPath(String path) { if (path == null) { @@ -2304,16 +2310,26 @@ public TemplatePermissionsResponse createTemplatePermissionsResponse(ResponseVie @Override public AsyncJobResponse queryJobResult(final QueryAsyncJobResultCmd cmd) { - final Account caller = CallContext.current().getCallingAccount(); + ApiCommandResourceType resourceType = getResourceType(cmd.getResourceType()); + String resourceTypeName = Optional.ofNullable(resourceType).map(ApiCommandResourceType::name).orElse(null); + + Long resourceId = getResourceId(resourceType, cmd.getResourceId()); + Long jobId = cmd.getId(); + if(jobId == null && resourceId == null) { + throw new InvalidParameterValueException("Expected parameter job id or parameters resource type and resource id"); + } - final AsyncJob job = _entityMgr.findByIdIncludingRemoved(AsyncJob.class, cmd.getId()); + final AsyncJob job = asyncJobDao.findJob(jobId, resourceId, resourceTypeName); if (job == null) { - throw new InvalidParameterValueException("Unable to find a job by id " + cmd.getId()); + throw new InvalidParameterValueException("Unable to find a job by id " + jobId + " resource type " + + cmd.getResourceType() + " resource id " + cmd.getResourceId()); } + jobId = job.getId(); final User userJobOwner = _accountMgr.getUserIncludingRemoved(job.getUserId()); final Account jobOwner = _accountMgr.getAccount(userJobOwner.getAccountId()); + final Account caller = CallContext.current().getCallingAccount(); //check permissions if (_accountMgr.isNormalUser(caller.getId())) { //regular users can see only jobs they own @@ -2324,7 +2340,7 @@ public AsyncJobResponse queryJobResult(final QueryAsyncJobResultCmd cmd) { _accountMgr.checkAccess(caller, null, true, jobOwner); } - return createAsyncJobResponse(_jobMgr.queryJob(cmd.getId(), true)); + return createAsyncJobResponse(_jobMgr.queryJob(jobId, true)); } public AsyncJobResponse createAsyncJobResponse(AsyncJob job) { @@ -5686,4 +5702,14 @@ public ConsoleSessionResponse createConsoleSessionResponse(ConsoleSession consol consoleSessionResponse.setObjectName("consolesession"); return consoleSessionResponse; } + + @Override + public EntityManager getEntityManager() { + return _entityMgr; + } + + @Override + public AccountManager getAccountManager() { + return _accountMgr; + } } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index ac9f8ee1433c..260a07cbf670 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -31,7 +31,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -369,7 +368,7 @@ import com.cloud.vm.dao.VMInstanceDetailsDao; @Component -public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements QueryService, Configurable { +public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements QueryService, Configurable, ResourceIdSupport { private static final String ID_FIELD = "id"; @@ -868,26 +867,14 @@ private Pair, Integer> searchForEventIdsAndCount(ListEventsCmd cmd) { Integer entryTime = cmd.getEntryTime(); Integer duration = cmd.getDuration(); Long startId = cmd.getStartId(); - final String resourceUuid = cmd.getResourceId(); - final String resourceTypeStr = cmd.getResourceType(); + final String resourceUuid = getResourceUuid(cmd.getResourceId()); + final ApiCommandResourceType resourceType = getResourceType(cmd.getResourceType()); final String stateStr = cmd.getState(); - ApiCommandResourceType resourceType = null; Long resourceId = null; - if (resourceTypeStr != null) { - resourceType = ApiCommandResourceType.fromString(resourceTypeStr); - if (resourceType == null) { - throw new InvalidParameterValueException(String.format("Invalid %s", ApiConstants.RESOURCE_TYPE)); - } - } if (resourceUuid != null) { - if (resourceTypeStr == null) { + if (resourceType == null) { throw new InvalidParameterValueException(String.format("%s parameter must be used with %s parameter", ApiConstants.RESOURCE_ID, ApiConstants.RESOURCE_TYPE)); } - try { - UUID.fromString(resourceUuid); - } catch (IllegalArgumentException ex) { - throw new InvalidParameterValueException(String.format("Invalid %s", ApiConstants.RESOURCE_ID)); - } Object object = entityManager.findByUuidIncludingRemoved(resourceType.getAssociatedClass(), resourceUuid); if (object instanceof InternalIdentity) { resourceId = ((InternalIdentity)object).getId(); @@ -3204,6 +3191,16 @@ private Pair, Integer> searchForAsyncJobsInternal(ListAsync sc.setParameters("executingMsid", msHost.getMsid()); } + ApiCommandResourceType resourceType = getResourceType(cmd.getResourceType()); + if (resourceType != null) { + sc.setParameters("instanceType", resourceType.toString()); + } + + final String resourceId = getResourceUuid(cmd.getResourceId()); + if (resourceId != null) { + sc.setParameters("instanceUuid", resourceId); + } + return _jobJoinDao.searchAndCount(sc, searchFilter); } @@ -6260,4 +6257,14 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[] {AllowUserViewDestroyedVM, UserVMDeniedDetails, UserVMReadOnlyDetails, SortKeyAscending, AllowUserViewAllDomainAccounts, AllowUserViewAllDataCenters, SharePublicTemplatesWithOtherDomains, ReturnVmStatsOnVmList}; } + + @Override + public EntityManager getEntityManager() { + return entityManager; + } + + @Override + public AccountManager getAccountManager() { + return accountMgr; + } } diff --git a/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java b/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java new file mode 100644 index 000000000000..0f91df92bd87 --- /dev/null +++ b/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java @@ -0,0 +1,123 @@ +// 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 com.cloud.api.query; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.db.EntityManager; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang3.StringUtils; + +import java.util.Optional; +import java.util.UUID; +import static org.apache.cloudstack.acl.SecurityChecker.AccessType; + +/** + * Support interface for converting resource UUIDs to internal IDs + * with validation and access control. + * + * @author mprokopchuk + */ +public interface ResourceIdSupport { + + EntityManager getEntityManager(); + + AccountManager getAccountManager(); + + /** + * Converts resource UUID to internal database ID with access control checks. + * + * @param resourceType type of the resource + * @param resourceUuid UUID of the resource + * @return internal resource ID or null if parameters are null + * @throws InvalidParameterValueException if only one parameter provided or resource not found + */ + default Long getResourceId(ApiCommandResourceType resourceType, String resourceUuid) { + String uuid = getResourceUuid(resourceUuid); + + if (resourceType == null || uuid == null) { + return null; + } else if ((resourceType == null) ^ (uuid == null)) { + throw new InvalidParameterValueException(String.format("Both %s and %s required", + ApiConstants.RESOURCE_ID, ApiConstants.RESOURCE_TYPE)); + } + + Object object = getEntityManager().findByUuidIncludingRemoved(resourceType.getAssociatedClass(), resourceUuid); + if (!(object instanceof InternalIdentity)) { + throw new InvalidParameterValueException(String.format("Invalid %s", ApiConstants.RESOURCE_ID)); + } + Long resourceId = ((InternalIdentity) object).getId(); + + Account caller = CallContext.current().getCallingAccount(); + boolean isRootAdmin = getAccountManager().isRootAdmin(caller.getId()); + + if (!isRootAdmin && object instanceof ControlledEntity) { + ControlledEntity entity = (ControlledEntity) object; + boolean sameOwner = entity.getAccountId() == caller.getId(); + getAccountManager().checkAccess(caller, AccessType.ListEntry, sameOwner, entity); + } + + return resourceId; + } + + /** + * Parses and validates resource type string. + * + * @param resourceType resource type as string + * @return parsed resource type or null if not provided + * @throws InvalidParameterValueException if provided type is invalid + */ + default ApiCommandResourceType getResourceType(String resourceType) { + Optional resourceTypeOpt = Optional.ofNullable(resourceType).filter(StringUtils::isNotBlank); + // return null if resource type was not provided + if (resourceTypeOpt.isEmpty()) { + return null; + } + // return value or throw exception if provided resource type is invalid + return resourceTypeOpt + .map(ApiCommandResourceType::fromString) + .orElseThrow(() -> new InvalidParameterValueException(String.format("Invalid %s", + ApiConstants.RESOURCE_TYPE))); + } + + /** + * Validates resource UUID format. + * + * @param resourceUuid UUID string to validate + * @return validated UUID or null if not provided + * @throws InvalidParameterValueException if UUID format is invalid + */ + default String getResourceUuid(String resourceUuid) { + if (StringUtils.isBlank(resourceUuid)) { + return null; + } + + try { + UUID.fromString(resourceUuid); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException(String.format("Invalid %s", ApiConstants.RESOURCE_ID)); + } + + return resourceUuid; + } + +} From ac17a7783612fabbf688ac7cad18304835e80ec3 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Wed, 8 Apr 2026 17:43:19 +0530 Subject: [PATCH 2/5] review changes --- .../cloudstack/api/command/user/job/ListAsyncJobsCmd.java | 4 ++-- .../api/command/user/job/QueryAsyncJobResultCmd.java | 4 ++-- server/src/main/java/com/cloud/api/ApiResponseHelper.java | 2 +- .../test/java/com/cloud/api/query/QueryManagerImplTest.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java index 85fbb587a4c1..2c8401831132 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java @@ -41,10 +41,10 @@ public class ListAsyncJobsCmd extends BaseListAccountResourcesCmd { @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = CommandType.UUID, entityType = ManagementServerResponse.class, description = "The id of the management server", since="4.19") private Long managementServerId; - @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the event", since="4.22.1") + @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the job", since="4.22.1") private String resourceId; - @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the event", since="4.22.1") + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the job", since="4.22.1") private String resourceType; ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java index db1a1898b2bb..5c3b0084574b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java @@ -37,10 +37,10 @@ public class QueryAsyncJobResultCmd extends BaseCmd { @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType = AsyncJobResponse.class, description = "The ID of the asynchronous job") private Long id; - @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the event", since="4.22.1") + @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the job", since="4.22.1") private String resourceId; - @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the event", since="4.22.1") + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the job", since="4.22.1") private String resourceType; ///////////////////////////////////////////////////// diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 1805929ff205..996da6e31d7d 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -2315,7 +2315,7 @@ public AsyncJobResponse queryJobResult(final QueryAsyncJobResultCmd cmd) { Long resourceId = getResourceId(resourceType, cmd.getResourceId()); Long jobId = cmd.getId(); - if(jobId == null && resourceId == null) { + if (jobId == null && resourceId == null) { throw new InvalidParameterValueException("Expected parameter job id or parameters resource type and resource id"); } diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index 892cd1e7def6..3b7589a8a019 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -267,7 +267,7 @@ public void searchForEventsFailResourceTypeInvalid() { public void searchForEventsFailResourceIdInvalid() { ListEventsCmd cmd = setupMockListEventsCmd(); Mockito.when(cmd.getResourceId()).thenReturn("random"); - Mockito.when(cmd.getResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine.toString()); + Mockito.lenient().when(cmd.getResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine.toString()); queryManager.searchForEvents(cmd); } From c1333905d6516ccb932cb1342f259c8bf8d6bca1 Mon Sep 17 00:00:00 2001 From: mprokopchuk Date: Thu, 9 Apr 2026 11:45:08 -0700 Subject: [PATCH 3/5] Fix logical condition in AsyncJobDaoImpl and ResourceIdSupport --- .../apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java | 2 +- server/src/main/java/com/cloud/api/query/ResourceIdSupport.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java index 2f6dcf6b6505..81cc5d4f2a8c 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java @@ -153,7 +153,7 @@ public List findInstancePendingAsyncJobs(String instanceType, Long a public AsyncJobVO findJob(Long id, Long resourceId, String resourceType) { SearchCriteria sc = byIdResourceIdResourceTypeSearch.create(); - if (id == null && resourceId == null && StringUtils.isNotBlank(resourceType)) { + if (id == null && resourceId == null && StringUtils.isBlank(resourceType)) { logger.debug("findJob called with all null parameters"); return null; } diff --git a/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java b/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java index 0f91df92bd87..2c32df22f0c5 100644 --- a/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java +++ b/server/src/main/java/com/cloud/api/query/ResourceIdSupport.java @@ -54,7 +54,7 @@ public interface ResourceIdSupport { default Long getResourceId(ApiCommandResourceType resourceType, String resourceUuid) { String uuid = getResourceUuid(resourceUuid); - if (resourceType == null || uuid == null) { + if (resourceType == null && uuid == null) { return null; } else if ((resourceType == null) ^ (uuid == null)) { throw new InvalidParameterValueException(String.format("Both %s and %s required", From 3581806d96b37debc2e86c29b9d8b3d9ee34d74b Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 10 Apr 2026 19:59:16 +0530 Subject: [PATCH 4/5] resource type case-insensitive validation --- .../org/apache/cloudstack/api/ApiCommandResourceType.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java index 4d33ba859a5b..e2ebb242cbf2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -127,8 +127,8 @@ public String toString() { } public static ApiCommandResourceType fromString(String value) { - if (StringUtils.isNotEmpty(value) && EnumUtils.isValidEnum(ApiCommandResourceType.class, value)) { - return valueOf(value); + if (StringUtils.isNotBlank(value) && EnumUtils.isValidEnumIgnoreCase(ApiCommandResourceType.class, value)) { + return EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, value); } return null; } From 7e43a3efd09764d4d6380ef1c7bb01cb06d2753b Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 10 Apr 2026 20:00:56 +0530 Subject: [PATCH 5/5] fix resource type and id search --- .../com/cloud/api/query/QueryManagerImpl.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 260a07cbf670..89a36e5b4435 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -3191,14 +3191,18 @@ private Pair, Integer> searchForAsyncJobsInternal(ListAsync sc.setParameters("executingMsid", msHost.getMsid()); } - ApiCommandResourceType resourceType = getResourceType(cmd.getResourceType()); - if (resourceType != null) { - sc.setParameters("instanceType", resourceType.toString()); - } + if (cmd.getResourceType() != null) { + ApiCommandResourceType resourceType = getResourceType(cmd.getResourceType()); + sc.addAnd("instanceType", SearchCriteria.Op.EQ, resourceType.toString()); - final String resourceId = getResourceUuid(cmd.getResourceId()); - if (resourceId != null) { - sc.setParameters("instanceUuid", resourceId); + final String resourceId = getResourceUuid(cmd.getResourceId()); + if (resourceId == null) { + throw new InvalidParameterValueException("Invalid resource id for the resource type " + resourceType); + } + + sc.addAnd("instanceUuid", SearchCriteria.Op.EQ, resourceId); + } else if (cmd.getResourceId() != null) { + throw new InvalidParameterValueException("Resource type must be specified for the resource id"); } return _jobJoinDao.searchAndCount(sc, searchFilter);