From ee3d2cccef197a076252e4c24b929dc596b9fbdb Mon Sep 17 00:00:00 2001 From: "hanyu.liang" Date: Thu, 14 May 2026 18:35:14 +0800 Subject: [PATCH 1/2] [license]: support license lsclient Support testlib simulator routing, multipart parsing, and request reader cleanup for License Server client API tests. Resolves: ZCF-3300 Change-Id: I4e157503cb58481a9bd2646981889e1477fa15d8 --- .../sdk/RegisterLicenseClientAction.java | 113 ++++++++++++++++++ .../sdk/RegisterLicenseClientResult.java | 38 ++++++ .../java/org/zstack/testlib/EnvSpec.groovy | 74 ++++++++++-- .../org/zstack/testlib/TestLibController.java | 12 +- 4 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientResult.java diff --git a/sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientAction.java b/sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientAction.java new file mode 100644 index 0000000000..83fedbad41 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RegisterLicenseClientAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.RegisterLicenseClientResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = true, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String serverUrl; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String siteName; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String productLine; + + @Param(required = true, nonempty = true, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String bundle; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String bundleName; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.RegisterLicenseClientResult value = res.getResult(org.zstack.sdk.RegisterLicenseClientResult.class); + ret.value = value == null ? new org.zstack.sdk.RegisterLicenseClientResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/licenses/client"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientResult.java b/sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientResult.java new file mode 100644 index 0000000000..f731129c21 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RegisterLicenseClientResult.java @@ -0,0 +1,38 @@ +package org.zstack.sdk; + + + +public class RegisterLicenseClientResult { + public java.lang.String siteUuid; + public void setSiteUuid(java.lang.String siteUuid) { + this.siteUuid = siteUuid; + } + public java.lang.String getSiteUuid() { + return this.siteUuid; + } + + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + + public long licenseCount; + public void setLicenseCount(long licenseCount) { + this.licenseCount = licenseCount; + } + public long getLicenseCount() { + return this.licenseCount; + } + + public java.lang.String config; + public void setConfig(java.lang.String config) { + this.config = config; + } + public java.lang.String getConfig() { + return this.config; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy b/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy index 5491d321f1..d5efa889e0 100755 --- a/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy @@ -4,6 +4,7 @@ import groovy.transform.AutoClone import org.codehaus.groovy.runtime.InvokerHelper import org.springframework.http.* import org.springframework.http.client.HttpComponentsClientHttpRequestFactory +import org.springframework.web.multipart.MultipartHttpServletRequest import org.springframework.web.client.RestTemplate import org.zstack.compute.vm.VmGlobalConfig import org.zstack.configuration.SqlForeignKeyGenerator @@ -987,21 +988,78 @@ class EnvSpec extends ApiHelper implements Node { } HttpEntity getEntityFromRequest(HttpServletRequest req) { - StringBuilder sb = new StringBuilder() - String line - while ((line = req.getReader().readLine()) != null) { - sb.append(line) - } - req.getReader().close() - HttpHeaders header = new HttpHeaders() for (Enumeration e = req.getHeaderNames() ; e.hasMoreElements() ;) { String name = e.nextElement().toString() header.add(name, req.getHeader(name)) } + + StringBuilder sb = new StringBuilder() + if (req.getContentType()?.toLowerCase()?.startsWith("multipart/")) { + sb.append(readMultipartBody(req)) + } else { + def reader = req.getReader() + try { + String line + while ((line = reader.readLine()) != null) { + sb.append(line) + } + } finally { + reader.close() + } + } + return new HttpEntity(sb.toString(), header) } + protected String readMultipartBody(HttpServletRequest req) { + byte[] raw = req.inputStream.bytes + if (raw.length > 0) { + return new String(raw, "UTF-8") + } + + if (req instanceof MultipartHttpServletRequest) { + return readSpringMultipartBody(req as MultipartHttpServletRequest) + } + + StringBuilder sb = new StringBuilder() + try { + req.getParts().each { part -> + appendMultipartPart(sb, part.name, part.submittedFileName, part.contentType, part.inputStream.bytes) + } + } catch (Throwable t) { + logger.debug("failed to read multipart parts for ${req.requestURI}", t) + } + return sb.toString() + } + + protected String readSpringMultipartBody(MultipartHttpServletRequest req) { + StringBuilder sb = new StringBuilder() + req.parameterMap.each { String name, String[] values -> + values.each { value -> + appendMultipartPart(sb, name, null, "text/plain", value == null ? new byte[0] : value.getBytes("UTF-8")) + } + } + req.fileMap.each { String name, file -> + appendMultipartPart(sb, name, file.originalFilename, file.contentType, file.bytes) + } + return sb.toString() + } + + protected void appendMultipartPart(StringBuilder sb, String name, String filename, String contentType, byte[] content) { + sb.append("Content-Disposition: form-data; name=\"").append(name).append("\"") + if (filename != null) { + sb.append("; filename=\"").append(filename).append("\"") + } + sb.append("\n") + if (contentType != null) { + sb.append("Content-Type: ").append(contentType).append("\n") + } + sb.append("\n") + sb.append(new String(content == null ? new byte[0] : content, "UTF-8")) + sb.append("\n") + } + void handleConditionSimulatorHttpRequests(HttpServletRequest req, HttpEntity entity, HttpServletResponse rsp) { def url = req.getRequestURI() if (httpConditionHandlers[url] == null || httpConditionHandlers[url].isEmpty()) { @@ -1222,4 +1280,4 @@ class EnvSpec extends ApiHelper implements Node { void resetAllMessageSize() { messageHandlerCounters.clear() } -} \ No newline at end of file +} diff --git a/testlib/src/main/java/org/zstack/testlib/TestLibController.java b/testlib/src/main/java/org/zstack/testlib/TestLibController.java index 286eae91b0..545acb8b27 100755 --- a/testlib/src/main/java/org/zstack/testlib/TestLibController.java +++ b/testlib/src/main/java/org/zstack/testlib/TestLibController.java @@ -27,7 +27,7 @@ public class TestLibController { private static final ExecutorService pool = Executors.newFixedThreadPool(32); @RequestMapping( - value = "/**", + value = {"/**", "/v1/sites/**", "/v1/quota/**"}, method = { RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.GET, RequestMethod.HEAD, RequestMethod.OPTIONS, RequestMethod.PATCH, RequestMethod.TRACE @@ -39,6 +39,11 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr return; } + if (isMultipartRequest(request)) { + Test.handleHttp(request, response); + return; + } + final AsyncContext asyncContext = request.startAsync(); asyncContext.setTimeout(TestConfigUtils.getMessageTimeoutMillisConfig()); @@ -59,6 +64,11 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr }); } + private boolean isMultipartRequest(HttpServletRequest request) { + String contentType = request.getContentType(); + return contentType != null && contentType.toLowerCase().startsWith("multipart/"); + } + @PreDestroy public void shutdownPool() { logger.info("Shutting down TestLibController pool"); From cc1b5c7b7b7e4b0e2e5317beba09bb93932b0042 Mon Sep 17 00:00:00 2001 From: "hanyu.liang" Date: Fri, 15 May 2026 09:46:33 +0800 Subject: [PATCH 2/2] [longjob]: avoid null longjob start time Resolves: ZCF-3300 Change-Id: Ie4e7ef2564c87c239894bcf82a9ea865a70e73de --- .../main/java/org/zstack/longjob/LongJobManagerImpl.java | 7 ++++--- longjob/src/main/java/org/zstack/longjob/LongJobUtils.java | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java b/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java index 975b0e711d..79a01a76ef 100755 --- a/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java +++ b/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java @@ -613,6 +613,9 @@ private void handle(SubmitLongJobMsg msg) { vo.setTargetResourceUuid(msg.getTargetResourceUuid()); vo.setManagementNodeUuid(Platform.getManagementServerId()); vo.setAccountUuid(msg.getAccountUuid()); + Timestamp now = Timestamp.valueOf(LocalDateTime.now()); + vo.setCreateDate(now); + vo.setLastOpDate(now); vo = dbf.persistAndRefresh(vo); msg.setJobUuid(vo.getUuid()); tagMgr.createTags(msg.getSystemTags(), msg.getUserTags(), vo.getUuid(), LongJobVO.class.getSimpleName()); @@ -831,9 +834,7 @@ public void validateGlobalConfig(String category, String name, String oldValue, dbf.installEntityLifeCycleCallback(LongJobVO.class, EntityEvent.PRE_UPDATE, (evt, o) -> { LongJobVO job = (LongJobVO) o; if (job.getExecuteTime() == null && jobCompleted(job)) { - long time = (System.currentTimeMillis() - job.getCreateDate().getTime()) / 1000; - job.setExecuteTime(Long.max(time, 1)); - logger.info(String.format("longjob [uuid:%s] set execute time:%d", job.getUuid(), time)); + setExecuteTimeIfNeed(job); } }); diff --git a/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java b/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java index 2d6d2fabc8..c4d37f98eb 100644 --- a/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java +++ b/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java @@ -202,9 +202,10 @@ private static boolean isRecoverableError(ErrorCode errorCode) { return recoverable instanceof Boolean && (Boolean) recoverable; } - private static void setExecuteTimeIfNeed(LongJobVO job) { + static void setExecuteTimeIfNeed(LongJobVO job) { if (job.getExecuteTime() == null) { - long time = (System.currentTimeMillis() - job.getCreateDate().getTime()) / 1000; + long startTime = job.getCreateDate() == null ? System.currentTimeMillis() : job.getCreateDate().getTime(); + long time = (System.currentTimeMillis() - startTime) / 1000; job.setExecuteTime(Long.max(time, 1)); logger.info(String.format("longjob [uuid:%s] set execute time:%d.", job.getUuid(), time)); }